UserCustomAction-ConfigPage adding CSS file links

I've had several questions in comments about how to add CSS files using the Simplest-Safest-Most-Future-Proof-Way to customize your SharePoint and SharePoint Online using UserCustomActionConfigPage

This is something I wanted to talk about from day 1 of this series - as I did cover you can add more than 1 CSS files, and specify their ranking via the Sequence number.

Because UserCustomAction doesn't create a link for CSS references, the way to do this is via the ScriptBlock and create a LINK element and attach it on page creation.

How to use this?

Grab the latest configure-page.aspx from Github.

Reference a CSS file instead of a JS file.  The ID is optional, but will be respected if specified.

The Sequence is honoured - 1001 will follow 1000.

Result - the CSS reference is loaded into the page.

Happy branding!

SP2013/2016 Responsive-UI and UserCustomActionsConfigPage

I just pushed an update to my Simplest-Safest-Most-Future-Proof-Way to customize your SharePoint and SharePoint Online using UserCustomActionsConfigPage (UCACP) github repo.  This is an important update because of a BIG thing that has just happened over at PnP side.

The PnP team has just released a SP2013/2016 Responsive UI pack

I jumped in a had a read, it uses the ScripLink way to attach a single javascript file, the file then looks for script id="PnPResponsiveUI"  element on the page, and attaches a CSS file.

My single page UserCustomActionsConfigPage does very similar things, except I use the ScriptSrc element in a UserCustomAction and that doesn't set the Script ID.

So this update to UserCustomActionConfigPage adds an additional textbox.  You can leave it blank.  But if you specify an ID, it will use ScriptBlock instead to insert the UserCustomAction.

New ID field

Installing 2013/2016 Responsive-UI pack with UCACP:

Copy three files into your Site Assets.   NOTE some browsers will save github files as HTML (because Github doesn't want to serve the files so it tells browsers these are HTML) - please double check the files are saved correctly as raw CSS or JS.



ID: PnPResponsiveUI
Url: SiteAssets/PnP-Responsive-UI.js

Usually, you can leave the ID blank.  But PnP-Responsive-UI.js needs it to be PnPResponsiveUI

Install to Current Web.  The Site User Custom Actions list should reflect there is now a SiteAssets/PnP-Responsive-UI.js inserted.  There is no ScriptSrc, because it is inserted via ScriptBlock.

The ScriptBlock is inserted using a Script tag attached to the head element.  This is identical to how PnP is doing it via PowerShell

One note - also, switch off your Mobile View feature.

What does it look like?

The Debugger shows both CSS and JS are injected correctly.  Note, I'm injecting from my own SiteAssets library.  PnP creates a folder under Style Library.

See the Responsive-UI in action with the Hamburger menus.

PnP PowerShell Installation

I want to reference drisgill's post on this Responsive-UI

He talks about the first impressions of the Responsive-UI, as well as a detailed section on how to install the Responsive-UI via PowerShell (as prescribed by PnP tools).

Installation with PowerShell is fine and dandy, but it assumes you are some sort of master magician.  Multiple downloads, and a dependency on 2015-CU.  (Which BTW you should have installed already).

I consider the PowerShell install perhaps a bit daunting and a barrier for people to try this customization on their site.

The UCACP way is 3 files.

Customization with the User Custom Actions is magical.  But we can leave out the PowerShell here this time :-)

Uninstall Responsive-UI Pack

And the Document Library view is back to normal.

Update 2018-05

Fixed some github broken links

Related Posts

Publishing UserCustomActionConfigPage on GitHub

I wrote previously about the Simplest way to add script to your SharePoint and SharePoint Online.

Although this is a simple, stand-alone page, it is something I can see being updated going forward, and with some minor prodding from Marc Anderson, I moved forward my plans to put it out there publically.

It also means I can now just link to that one place, and as I make updates to the page, people won't be going to the old version 1. 

It's here on GitHub. 

I still have a few more articles on branding that I wanted to write under this banner and I think Marc is going to link it up with requirejs. 


The easiest way to add Script and Brand your SharePoint and SharePoint Online

This is the first of a few blog posts I wanted to cover in my 2015 Christmas break on the future of branding. 

I'll probably devote the next article to compare the various different ways to brand your SharePoint site - masterpage?  CustomCss?  On-premises or Online?  Would the technique work in SharePoint 2010?  What about display templates or JSLink?

But I want to march forward and cover what I think is the safest way to brand your SharePoint right now:

Inject JavaScript to any Site or Site Collection via a User Custom Action's ScriptLink property.

This is a great technique because it lets you:

  • Add JavaScript anywhere - scoped to Site Collection or Site.
  • Add CSS via JavaScript
  • You can add more than 1 CSS file
  • Order them the way you want via Sequence
  • You can combine this to load your initial JavaScript file which can be a RequireJS setup and then hand off the controls to RequireJS config
  • Does not modify MasterPage
  • Works in SharePoint 2010, 2013, 2016 and SharePoint Online
  • Only need Site Collection permissions to set up - you don't need to have a Farm Solution or Add-In Model.  The permission is only required to set up the ScriptLink.
  • The object model provides a way for an administrator to check all the User Custom Actions attached to any site/site collection, so there's a level of oversight available if you want to check if your customizations are ready for migration.

There are various ways to attach a script via User Custom Actions.

  • Remote Provisioning (Pattern and Practice) uses it via C# CSOM
  • PowerShell remote provisioning
  • Farm Solution can invoke the API
  • Sandbox Solution can invoke the Client Side Object Model API (*with permission)
  • Add-In can invoke the CSOM API as well (with Site Collection - Full Control permission)

The unfortunate part is, there's no UI for a power user to add or view ScriptLinks directly.  You need to spin up SharePoint Manager or read it via PowerShell.

And that brings me to today's post. 

I build a simple config page in JavaScript. 

Then I did a load of work to make sure everything runs from One Page.

How does it work?

  1. Drop it into a SiteAssets or SitePages library.
  2. he JavaScript on the page detects and loads some dependencies (jQuery, SP.js etc). 
  3. Provided you have site collection permissions, it'll list all existing User Custom Actions
  4. You can specify a filename (including any subfolders like spg/hello.js) and give it a sequence number (default to 1000).  Then you can install a Custom Action to Site Collection or Current Web.  All via the magic of JavaScript.

I also brand it to look a bit like SharePoint.  Just a bit.

This is still a developer blog.  So we're going to talk about code:

function listUserCustomAction(siteOrWeb) {
    siteOrWeb = (siteOrWeb=="site"? "site":"web");
    // ajax call to userCustomActions and order by Sequence
    // this function can do either _api/site or _api/web
    var p1 = $.ajax({
        url: hostweburl + "/_api/"+siteOrWeb+"/userCustomActions?$orderby=Sequence",
        dataType: "json",
        contentType: 'application/json',
        headers: { "Accept": "application/json; odata=verbose" },
        method: "GET",
        cache: false
    p1.then(function (response) {
        // use jQuery to do a bit of simple UI update
        $.each(response.d.results, function (i, result) {
                "<li>" +
                    " [" + result.Location + "] " +
                    (result.Title || result.Name || "") +
                    " ScriptSrc=" + result.ScriptSrc +
                    " Sequence=" + result.Sequence +
    return p1;

First function.  listUserCustomAction will show existing Custom Actions attached to either the site collection or the current web.  Interestingly - this will also list custom actions attached by other solutions you may have installed in your farm.


spg.installUserCustomAction = function(siteOrWeb) {
    // switch to JSOM to install userCustomActions
    var webContext = SP.ClientContext.get_current();
    var userCustomActions;
    if (siteOrWeb == "site") {
        userCustomActions = webContext.get_site().get_userCustomActions();
    else {
        userCustomActions = webContext.get_web().get_userCustomActions();

    // read srcurl and sequence from textboxes
    srcurl = $("#scriptlink-name").val();
    srcsequence = parseInt($("#scriptlink-sequence").val()) || 1000;

    var action = userCustomActions.add();
    // my tool always attach script from ~sitecollection/SiteAssets/ 
    // you can use subfolders
    // but if you want to use Style Library or some other
    // folder you'll have to change this.
    action.set_scriptSrc("~sitecollection/SiteAssets/" + srcurl);

    // my SP.ClientContext has a special promise enabled callback
    // lets me pipe one promise to the next
    return webContext.executeQueryPromise().pipe(function () {
        return spg.listUserCustomActions();

Reminder: I wrote previously how to change SharePoint JSOM's ExecuteQueryAsync to just return a Promise.  This is important, because you can do stupidly fun things like chaining one asynchronous callback to another one and don't even need to pull out your hair.


spg.uninstallUserCustomAction = function(siteOrWeb) {
    var webContext = SP.ClientContext.get_current();
    var userCustomActions = webContext.get_site().get_userCustomActions();

    if (siteOrWeb == "site") {
        userCustomActions = webContext.get_site().get_userCustomActions();
    else {
        userCustomActions = webContext.get_web().get_userCustomActions();

    srcurl = $("#scriptlink-name").val();
    var p1 = webContext.executeQueryPromise();
    var p2 = p1.pipe(function () {
        var i = 0, count = userCustomActions.get_count(), action = null;
        for (i = count - 1; i >= 0; i--) {
            action = userCustomActions.get_item(i);
            // look for any script that has the same url as yours
            // and delete only those
            if (action.get_scriptSrc() == "~sitecollection/SiteAssets/" + srcurl) {
        // more chaining promise fun
        return webContext.executeQueryPromise().pipe(function () {
            return spg.listUserCustomActions();
     return p2;

So here we are.  A page that you can use to install and uninstall ScriptLinks from SharePoint Online and SharePoint 2013/2016.

* SharePoint 2010 needs a few tweaks with the _layout/15 path.  Also, the SP.UI.ChromeControl doesn't work so the page will have to look simpler.

What does the result look like?

Here's what it looks like in a browser Network tab

They are not found because they aren't really there!  But you can see it's looking for the files in ~sitecollection/siteassets/hello.js

And the Sequence is important.  I have hello1.js at 999 and hello.js at 1000, here's what they look like in the <head> tag of the SharePoint page.

SharePoint inserted them into the page at runtime, and I never touched the MasterPage.  And that's a big win.


Related Posts