We're reclaiming inactive PDIs to keep them available for active builders. Learn what's changing, who's affected, and how to protect your work. Read More

Intro

ServiceNow does not provide an out-of-the-box slider variable type for Catalog Items. However, the platform gives us a flexible way to build our own by using a Custom or Custom with Label variable and rendering it with either a UI Macro or a Service Portal widget.

In this article, we want to show how to build a fully custom numeric slider widget for the Service Portal and use it inside a Catalog Item. My example is the Server Tuning Catalog Item, where a requester can define the disk size in GB using a slider.

 

The key idea is simple:

  • use a Custom / Custom with Label variable
  • assign a Service Portal widget
  • make the widget reusable by driving it through configurable widget options
  • store the selected value in a dependent catalog variable

This approach works very well if you want to introduce custom HTML controls into the Service Catalog experience.

 

Use Case

In my example, the user should define the size of a disk like this:

  • minimum: 10 GB
  • maximum: 500 GB
  • step size: 5 GB
  • initial value: 20 GB

The slider displays the selected value and writes it into another catalog variable, for example:

size_disk_1

That dependent field is important, because in this implementation the custom widget itself is used for rendering and interaction, while the actual value is persisted through a standard variable.

 

Solution Overview

The solution consists of a few building blocks:

  1. A custom field on Variable [item_option_new]

    • u_widget_options
    • type: Name-Value Pairs
  2. A reusable Service Portal widget

    • name: Service Portal Numeric Slide
  3. A Script Include

    • DBOR_WidgetVariableUtils
    • reads the widget’s option schema and prepares it for the Variable form
  4. Two client scripts on the Variable table

    • one to populate the widget options automatically
    • one to copy the option values into the Variable’s default_value
  5. A dependent standard variable

    • e.g. size_disk_1
    • typically a Single Line Text
    • ideally Read only or even hidden

 

1) Add a configuration field to the Variable table

To make the widget reusable, we added a field to the Variable [item_option_new] table:

  • Column name: u_widget_options
  • Type: Name-Value Pairs

This field stores the configuration for the widget on the variable itself.

That means you do not have to clone the widget for every slider we want to use. Instead, we can configure each variable individually with values like:

  • start value
  • minimum value
  • maximum value
  • step size
  • unit
  • dependent variable name

Dictionary Entry for u_widget_optionsDictionary Entry for u_widget_options

 

3) Client Scripts on the Variable form

To make the Variable form user-friendly, we added two client scripts.


Client Script 1: populate u_widget_options from the selected widget

Name: DBOR Set Widget Options

This script runs when the widget field changes. It calls the Script Include, reads the widget schema, and builds a JSON object with the default values.

 

function onChange(control, oldValue, newValue, isLoading, isTemplate) {
    if (isLoading || newValue === '') {
        if (newValue == '') {
            g_form.setValue('u_widget_options', "");
        }
        return;
    }

    if (!g_form.getElement('u_widget_options')) {
        g_form.addErrorMessage(getMessage('dbor_sc_missing_widget_options_on_form'));
    }

    var ga = new GlideAjax('DBOR_WidgetVariableUtils');
    ga.addParam('sysparm_name', 'getOptions');
    ga.addParam('widgetSysId', newValue);
    ga.getXMLAnswer(getResponse);

    function getResponse(response) {
        var options = JSON.parse(response);
        var values = {};
        for (var o in options) {
            var option = options[o];
            if (option.key) {
                var v = option.default_value;
                values[option.key] = (v) ? v : "";
            }
        }
        g_form.setValue('u_widget_options', JSON.stringify(values));
    }
}

 

What it does

  • clears the field if no widget is selected
  • warns the admin if u_widget_options is not on the form
  • fetches the option schema via GlideAjax
  • creates a default JSON payload
  • stores it in u_widget_options

 

Client Script 2: mirror widget options into default_value

Name: DBOR Set Widget Options in Default Value

function onChange(control, oldValue, newValue, isLoading, isTemplate) {
    if (isLoading) {
        return;
    }
    g_form.setValue('default_value', newValue);
}

 

Why this helps

This small script copies the widget option JSON into the variable’s default_value. In this setup, that is a practical way to keep the configuration stored directly with the variable.

 

4) Script Include: reading the widget option schema

To support the administration experience on the Variable form, I created a Script Include called:

DBOR_WidgetVariableUtils

Its purpose is to read the widget’s option_schema from the sp_widget record and return it as JSON

 

 

var DBOR_WidgetVariableUtils = Class.create();
DBOR_WidgetVariableUtils.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {

    /**
     * Return Options of Widget as JSON string. String includes array of options. Option format:
     *  {
     *      'key': option.name,
     *      'label': option.label,
     *      'hint': option.hint,
     *      'default': option.default_value
     * }
     */
    getOptions: function() {
        var result = [];
        var sys_id_widget = this.getParameter('widgetSysId');
        var gr = new GlideRecord('sp_widget');
        if (gr.get(sys_id_widget)) {
            var options_schema = gr.getValue('option_schema');
            var options = JSON.parse(options_schema);
            for (var i in options) {
                var option = options[i];
                var value = option.default_value;
                var retOption = {
                    'key': option.name,
                    'label': option.label,
                    'hint': option.hint,
                    'default_value': (value) ? value : "",
                    'type': option.type
                };
                result.push(retOption);
            }
        }
        return JSON.stringify(result);
    },

    type: 'DBOR_WidgetVariableUtils'
});

 

Why this Script Include matters

This is the heart of the configuration model.

Instead of hardcoding all available widget options somewhere else, the Script Include reads them directly from the widget definition. That keeps the solution clean and reduces duplication.

If I later add another option to the widget schema, I only need to update the widget once.

 

 

5) Define the widget option schema

The widget record contains an Option Schema that describes which options are available.

For this slider, the schema contains:

  • start_value
  • minimum_value
  • maximum_value
  • step_value
  • unit
  • with_value_label
  • dependent_variable_name

Formatted, it looks like this:

[
  {
    "hint": "Value to start with",
    "name": "start_value",
    "section": "Data",
    "default_value": "0",
    "label": "Start Value",
    "type": "integer"
  },
  {
    "hint": "Minimum value of slider",
    "name": "minimum_value",
    "section": "Data",
    "default_value": "0",
    "label": "Minimum Value",
    "type": "integer"
  },
  {
    "hint": "Maximum value of slider",
    "name": "maximum_value",
    "section": "Data",
    "default_value": "10",
    "label": "Maximum Value",
    "type": "integer"
  },
  {
    "hint": "Steps of slider",
    "name": "step_value",
    "section": "Data",
    "default_value": "1",
    "label": "Step Value",
    "type": "integer"
  },
  {
    "hint": "Unit which should be shown behind the value",
    "name": "unit",
    "section": "other",
    "default_value": "",
    "label": "Unit",
    "type": "string"
  },
  {
    "hint": "Defines if the selected value should be shown on right bottom of slider",
    "name": "with_value_label",
    "section": "Data",
    "default_value": "true",
    "label": "With Value Label",
    "type": "boolean"
  },
  {
    "hint": "Name of variable which should be populated by the widget.",
    "name": "dependent_variable_name",
    "section": "Behavior",
    "label": "Dependent Variable Name",
    "type": "string"
  }
]

This is the foundation that makes the widget configurable and reusable.

A real example for the Variable default_value / widget option JSON looks like this:

{
  "start_value": "20",
  "minimum_value": "10",
  "maximum_value": "500",
  "step_value": "5",
  "unit": "GB",
  "with_value_label": "true",
  "dependent_variable_name": "size_disk_1"
}

 

6) The widget: Service Portal Numeric Slide

Now to the actual widget.

The widget is intentionally simple:

  • the Server Script prepares the data
  • the Client Script / Controller reacts to slider movement
  • the HTML template renders a standard <input type="range">
  • the CSS provides basic styling

Service Portal Numeric Slide WidgettService Portal Numeric Slide Widgett

 

Server Script

The Server Script reads the widget options and prepares the values for the template and controller.

(function() {
    /* populate the 'data' object */
    /* e.g., data.table = $sp.getValue('table'); */

    data.min = options.minimum_value;
    data.max = options.maximum_value;
    data.step = options.step_value;
    data.unit = options.unit;

    var middle = (data.min + data.max) / 2;
    data.default_val = (options.start_value) ? options.start_value : middle;

    data.with_value_label = options.with_value_label == "true" ? true : false;
    data.dependent_variable = options.dependent_variable_name;

    var id = (data.dependent_variable) ? data.dependent_variable : "";
    data.slidername = id + "_wgt_slider";
    data.sliderId = id + '_wgt_slider';
    data.valueId = id + '_wgt_value';
})();

What the Server Script does

This is the part that “prepares” the widget.

It reads:

  • minimum value
  • maximum value
  • step size
  • unit
  • start value
  • dependent variable name
  • whether the value label should be shown

It also creates unique IDs:

  • slidername
  • sliderId
  • valueId

This is especially important if you place multiple sliders on the same Catalog Item. Without unique names and IDs, one slider could interfere with another.

A particularly useful detail is this fallback:

var middle = (data.min + data.max) / 2;
data.default_val = (options.start_value) ? options.start_value : middle;

If no start value is defined, the widget uses the midpoint between min and max.

 

Client Script / Controller

The widget logic lives in the client controller.

api.controller = function($rootScope, $scope) {
    /* widget controller */

    var c = this;

    $rootScope.$on('spModel.gForm.rendered', function() { // page fully loaded
        var slider = document.getElementById(c.data.sliderId);
        var output = document.getElementById(c.data.valueId);
        var dep_var = c.data.dependent_variable;

        if (dep_var && dep_var != "") {
            $scope.page.g_form.setValue(dep_var, c.data.default_val);
        }

        slider.oninput = function() {
            output.innerHTML = this.value;
            if (dep_var && dep_var != "") {
                $scope.page.g_form.setValue(dep_var, this.value);
            }
        };
    });
};

 

What the controller does

This is the interactive part of the widget.

Once the catalog form is fully rendered:

  1. it finds the slider element
  2. it finds the label element showing the current value
  3. it reads the configured dependent variable
  4. it initializes that dependent variable with the start value
  5. on every slider movement, it:
    • updates the displayed value
    • writes the current value into the dependent variable

This design makes the slider behave like a real form control, while still using a standard backend field to persist the selected value.

 

Why the dependent variable is needed

In this implementation, the selected slider value is written into another catalog variable, for example:

size_disk_1

That is the field you can later read in workflows, flows, request processing, or fulfillment logic.

This is also the main trade-off of this approach: the custom widget is great for rendering and interaction, but you still need a standard field to reliably persist and consume the value downstream.

 

HTML Template

The template is intentionally minimal and uses a native HTML range input.

<div class="numeric_slider">
    <input type="range"
           id="{{c.data.sliderId}}"
           class="slider"
           name="{{c.data.slidername}}"
           value="{{c.data.default_val}}"
           step="{{c.data.step}}"
           min="{{c.data.min}}"
           max="{{c.data.max}}" />

    <div class="slider_text" ng-show="c.data.with_value_label">
        <span id="{{c.data.valueId}}">{{c.data.default_val}}</span> {{c.data.unit}}
    </div>
</div>

This is a good reminder that a custom catalog variable can render any HTML control you want.

 

CSS tips

Styling is not the most important part of this article, but a little CSS helps make the control look cleaner.

.numeric_slider {
    width: 100%;
}

.slider {
    width: 100%;
    height: 25px;
    outline: none;
    opacity: 0.7;
    border-width: 1px;
    border-radius: 15px;
    transition: opacity .2s;
}

.slider:hover {
    opacity: 1;
}

.slider_text {
    width: 100%;
    margin-top: 0.2em;
    padding-right: 0.5em;
    text-align: right;
}

 

Small styling recommendations

A few practical tips:

  • keep the slider at width: 100%
  • right-align the value label if you want a clean catalog layout
  • use subtle hover effects only
  • avoid over-styling unless there is a real UX need

For a technical article like this one, basic styling is enough.

 

7) Variable configuration in the Catalog Item

On the Catalog Item, I configured the slider variable like this:

  • Type: Custom with Label
  • Widget: Service Portal Numeric Slide

     

Variable using Slider WidgetVariable using Slider Widget

 

The option values are then stored/configured in the widget options as key-value pairs, for example:

Widget selection and Slider OptionsWidget selection and Slider Options

This makes the widget universal and reusable.

 

😎The dependent field

To persist the value, we create a normal variable:

  • Type: Single Line Text
  • Name: size_disk_1

I recommend making this variable Read only.
If the field is visible and editable, users could change it manually, and then the slider and the field might get out of sync.

At the moment, the implementation only updates the field from the slider.

Dependend Variable which is updated by the slideDependend Variable which is updated by the slide

 


If you want two-way synchronization, you would need an additional script that updates the slider when the field value changes.

 

9) Important limitation

This solution works in the Service Portal only.

Because the rendering relies on a Service Portal widget, the same variable will not automatically work in the classic backend catalog UI.

 

10) Final result

The result is a clean, reusable slider variable for the Service Catalog:

  • easy for users to interact with
  • configurable per variable
  • reusable across multiple Catalog Items
  • based on standard web technology
  • flexible enough to support many other custom controls

The GIF below shows the final behavior: the slider moves, the visible value updates, and the dependent field is populated at the same time.

 

Slider in actionSlider in action

11) Conclusion

If you need a catalog control that does not exist out of the box, using a Custom variable with a Service Portal widget is a very effective pattern.

 

Version history
Last update:
51m ago
Updated by:
Contributors