This post is inspired by another community post from Maik Skoddow, especially the part with themes available for new code editor introduced with the Utah release – monaco
.
I really like the new code editor. It’s a step forward in the direction I like. Maik posted some great tips and covered the basic usage. But there’s one thing where I can extend his article – the user themes.
Why the code editor themes?
Well, it’s simple – developers love themes. And since we are talking about code editor for developers, having the ability to change them, update them and personalize them seems like something basic. Especially when working with monaco
, an editor that makes theming as simple as possible. You can quickly check out the most popular themes here and get the configuration you like here. Now when you have your favourite theme, is anything stopping you from applying it on ServiceNow platform?
It’s pretty simple
If you understand one thing – the global variables available in client scripts. I won’t write much about it here, it’s just important to know that they are there. That’s why we were given the Isolate script checkbox on the client script for, right?
With the introduction of new code editor, there is also new global variable – monaco
. As far as I can tell, some out of the box methods from the API are hidden, but we can work with what we are given. To change the theme we only need defineTheme
, setTheme
, and maybe addAction
, to make it look better. And make sure you have the SN Utils installed, it enables to editor’s context menu. You can’t set a theme without it.
Step 1 – save the theme data somewhere
First things first – we need themes if we want to change them. Pick something from the repository I linked earlier and store it somewhere, where it would be easily accessible for the scripts. I used system properties.

You may add as many as you want.
Step 2 – build logic to retrieve theme data
Sounds like a big task, but it’s really simple. I used a client callable script include in scoped application and a client script. The SI contains some additional user firendly functions like loading the previously selected theme and saving new selection in a separate property.
var BetterDevUtils = Class.create();
BetterDevUtils.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
getMonacoColorThemes: () => {
let ret = [];
new global.GlideQuery('sys_properties')
.where('name', 'STARTSWITH', 'monaco.theme')
.where('name', '!=', 'monaco.theme.userMap')
.select('name', 'description', 'value')
.forEach((prop) => {
ret.push({
label: prop.description,
id: prop.name,
data: prop.value
});
});
return JSON.stringify(ret);
},
updateUserTheme: function() {
let userSysId = gs.getUserID();
let userThemeMap = JSON.parse(gs.getProperty('monaco.theme.userMap'));
let themeName = this.getParameter('sysparm_themeName') || 'vs-dark';
userThemeMap[userSysId] = themeName;
new global.GlideQuery('sys_properties')
.getBy({name: 'monaco.theme.userMap'})
.ifPresent((updateMe) => {
new global.GlideQuery('sys_properties')
.where('sys_id', updateMe.sys_id)
.update({value: JSON.stringify(userThemeMap)});
});
},
getUserTheme: () => {
let ret = '';
let userSysId = gs.getUserID();
let userThemeMap = JSON.parse(gs.getProperty('monaco.theme.userMap'));
if (userThemeMap.hasOwnProperty(userSysId)) ret = userThemeMap[userSysId];
return ret;
},
type: 'BetterDevUtils'
});
Interesting thing about the above code. You may notice that I have a mix of arrow functions and regular functions. As I have learned, arrow functions are not the best choice for script includes, because the change the scope. Read more on MDN. I left it like that just to show you the difference.
The client script is used to retrieve the data and add actions to the editor.
function onLoad() {
addAfterPageLoadedEvent(
function() {
var gaAjax = new GlideAjax('scope.BetterDevUtils');
gaAjax.addParam('sysparm_name', 'getMonacoColorThemes');
gaAjax.getXMLAnswer(function(ans) {
var ansJSON = JSON.parse(ans);
var myEd = GlideEditorMonaco.getAll()[0];
var ediRef = monaco.editor;
var order = 1;
ansJSON.forEach(function(theme) {
var themeName = theme.id.substring(theme.id.lastIndexOf('.') + 1);
ediRef.defineTheme(themeName, JSON.parse(theme.data));
myEd.editor.addAction({
'contextMenuGroupId': '10_monaco_theme',
'contextMenuOrder': order++,
id: theme.id,
label: 'Set theme: ' + theme.label,
run: function() {
ediRef.setTheme(themeName);
var gaUpdateTheme = new GlideAjax('scope.BetterDevUtils');
gaUpdateTheme.addParam('sysparm_name', 'updateUserTheme');
gaUpdateTheme.addParam('sysparm_themeName', themeName);
gaUpdateTheme.getXMLAnswer(function(ans){
console.log('User theme updated to ' + themeName);
});
}
});
});
var gaUserTheme = new GlideAjax('scope.BetterDevUtils');
gaUserTheme.addParam('sysparm_name', 'getUserTheme');
gaUserTheme.getXMLAnswer(function(ans){
if (ans) ediRef.setTheme(ans);
});
});
}
);
}
Some explanation here:
addAfterPageLoadEvent
is a client script handler that should be used if you want to make sure your code runs after the page loads, as described here- lines 8 and 15 are responsible for adding the context menu actions with themes. I took them by reading the code from SN Utils repo, here, between lines 1545 and 1622.
- line 14 is required if you want to have your new theme available
- line 21 actually sets the theme
- Isolate script has to be turned off on client script
- you need a separate client script for every table you want to add this feature to
And that’s it!
Yes, it’s that simple. See it in action:
Hi Cezary! Is it possible to do the it (using script) on the Background Script page (sys.scripts.modern.do)? Whitch table should I use?
Hi Thiago, I don’t think you can do it like that for the background script. However the SNUtils browser plugin somehow manages to overwrite the default theme, so maybe there is a hope for your use case 🙂