The Zurich release has arrived! Interested in new features and functionalities? Click here for more

MGOPW
ServiceNow Employee
ServiceNow Employee

Kanban Board Component - Part 1

Introduction

This series of articles will walk you through an example configuration of the Kanban component. They will provide you with hard coded example configurations, explanations of properties, and a general overview of how to set up the component. It's important to keep in mind that the Kanban Board Component is highly customizable and uses other components for its configuration.

 

In this article we will cover prerequisites, go over the base configuration of the component, and showcase how to configure two example features (drag and drop & swimlanes). In the next article, linked in the top Article Directory and in the bottom of this article, we will  provide some configuration information and examples for cards, lanes, and swimlanes.

 

This article assumes that you have some experience with UI Builder, we suggest that you first leverage one of these resources to learn more about configuring workspaces and experiences in UI Builder:

 

Prerequisites

The Kanban Board component comes pre-installed in your instance, but for the purposes of this tutorial you need to go to the Application Manager and install the sn-vtb component library. Make sure that when you install or update this library, you click the "Load demo data" checkbox. This step is crucial and required for proper configuration of the Kanban Board component as part of this tutorial, as it installs the declarative actions we will be using.

 

MGOPW_52-1727364796487.png
MGOPW_53-1727364809347.png

 

 

Initial Configuration

  1. Go to UI Builder and add the Kanban Board component to a page. You can create a new page or utilize an already existing page in any workspace or experience.
  2. Create two Client State Parameters:
    1. Name: cards
      1. Type: JSON
      2. Initial value: []
    2. Name: lanes
      1. Type: JSON
      2. Initial value: []
  3. Configure the Kanban Board component by setting the following component properties as follows:
    1. Vertical lane header template: sn-vtb-lane-header
    2. Vertical lanes: Dynamically data bind to the lanes Client State Parameter. (@STate.lanes)
    3. Card template: sn-vtb-card
    4. Cards: Dynamically data bind to the cards Client State Parameter. (@STate.cards)
  4. Create a new "GlideRecord Collection Query" Data Resource and configure as follows:
    1. Table: Incident
    2. Return fields: Number, Priority, Description, State, Short Description
    3. Order by: Updated
    4. Sort type: desc
    5. Max results: 20
  5. Create a new Client Script and name it "Format data". Copy the following code in to the script field:
            function handler({api, event, helpers, imports}) {
                    // Grabbing incidents from the GlideRecord Collection Query
                    const incidents = api.data.gliderecord_collection_query_1.output.data.GlideRecord_Query.incident._results;
                    const cards = [];
                    const laneObject = {};
                    
                    // For each incident, create a card and push to the array
                    incidents.forEach((incident, index) => {
                        cards.push({
                            id: incident._row_data.uniqueValue,
                            title: incident.number.displayValue,
                            order: index,
                            
                            // Sets what lane the card is assigned to
                            lane_id: incident.priority.value,
                            
                            // Options, Attachments and Record properties are the bare minimum required to get the card template to render
                            options: {
                                "board_type": "FREEFORM",
                                "card_type": "classic",
                            },
                            attachments: [],
                            record: {
                                short_description: { display_value: incident.short_description.displayValue },
                                description: { display_value: incident.description.displayValue },
                            }
                        });
                
                        // Starts setting up the lanes 
                        laneObject[incident.priority.value] = {
                            value: incident.priority.value,
                            displayValue: incident.priority.displayValue
                        };
                    });
                  
                    const lanes = [];
                    Object.keys(laneObject).forEach(key => {
                        
                        // Creates the lanes
                        lanes.push({
                            id: key,
                            title: laneObject[key].displayValue,
                            name: laneObject[key].displayValue,
                            options: {
                                "board_type": "FREEFORM"
                            },
                            // Styles for the lane and lane header, CSS
                            "_style_" : {
                                "sn-lane": {
                                    width: "300px",
                                    padding: "5px",
                                },
                                "sn-lane-header": {
                                    width: "300px",
                                    padding: "5px",
                                },
                            }
                        });
                    });
                
                    // Sets the client state parameters
                    api.setState('lanes', lanes);
                    api.setState('cards', cards);
                }​
  6. Navigate back to the GlideRecord Collection Query Data Resource and click on the Events tab.
  7. Add the event mapping "Data Fetch Succeeded" and then add an event handler to it that will execute the Format data client script.
  8. Save and test; you should see the Kanban Board render with your fetched data. Currently the cards will do nothing when clicked and they can't be dragged or dropped in any other lane. We will configure this in the remainder of the steps. If you don't see any cards, or only see the default data, try refreshing your browser after clearing the cache and try again.

MGOPW_54-1727365344869.png

 

Configuring Events

We will add an event for when the card gets clicked. When the sn-vtb-card component is clicked, it generates the event VTB#CONFIRMATION_MODAL_SELECTED. It has a payload consisting of two fields:

  • modalID - String - Defines which modal should be used.
  • modalData - JSON - Returns the record data of the card.

To add the event:

  1. Create a new Client script and title it "Card is clicked", copy the following code into it:
    function handler({api, event, helpers, imports}) {
             console.log("Card clicked event", event);
        } ​
  2. Click on the Body section of the page and, in the Page configuration panel to the right, click on Events.
  3. Go to the bottom to the "Handled events" section and click the + Add button.
  4. Add the following information to the "Create an event" modal:
    1. Event label: VTB#CONFIRMATION_MODAL_SELECTED
    2. Click the +Add button next to the Payload fields heading and add two fields:
      1. Name: modalID
        1. Label: Modal ID
        2. Type: String
      2. Name: modalData
        1. Label: Modal Data
        2. Type: JSON
    3. Your modal should look like this when you're done:
       


      MGOPW_56-1727365426874.png

  5. Navigate back to the top of the Events tab in the Page configuration panel, and click "Add event mapping".
  6. Select the event mapping we just created.
  7. When prompted to select an event handler, select the Client Script we just created titled "Card is clicked".
  8. Now that we have the event on the page, we need to add it to the Kanban Board. Click on the Kanban Board component in the component tree to the left.
  9. Scroll to the bottom of the component properties, and enter the following JSON in to the "Declarative-action map" field. This will pull in the declarative action we loaded through the demo data in the Prerequisites section of this article.
       "VTB#CONFIRMATION_MODAL_SELECTED": {
                "actionDispatch": "VTB#CONFIRMATION_MODAL_SELECTED",
                "actionPayload": "{\r\n \"modalId\": \"{{modalId}}\",\r\n        \"modalData\": \"{{modalData}}\"\r\n}",
                "actionType": "uxf_client_action",
                "assignmentId": "0c8e127453922010c5e2ddeeff7b125f",
                "label": "Confirmation Modal Selected",
                "name": "VTB#CONFIRMATION_MODAL_SELECTED"
            }
        }​
  10. Now that the declarative action has been mapped to the Kanban Board component, click on the "Configure declarative action event mappings" button at the very bottom of the component's configuration panel.
  11. Under "Confirmation Modal Selected", click "Add a new event handler".
  12. Fill in the fields as follow:
    1. Name: Card Clicked Mapping
    2. Dynamically data bind the following fields by clicking on the Data binding icon, double clicking on the empty space at the top where it says "Add a data output to this area", then typing in the information below:
      1. Modal ID: @payload.modalID
      2. Modal Data: @payload.modalData
    3. This is what it will look like when you have successfully double clicked, typed in the information, then clicked out of the box:

      MGOPW_58-1727365646525.png

      MGOPW_59-1727365650501.png
    4. Click "Save" once you've filled in both fields, then click "Done".


      MGOPW_63-1727365789611.png

  13.  Save the page and test. Open the browser console (F12) and observe the event firing the logging statement in the Client Script when you click on a card. You can use this payload information to configure a modal to be fired with the information from the record you clicked.
    MGOPW_61-1727365733380.png
That was the basic configuration for the Kanban Board component. If you would like to explore what other events are available, you can check out the Action Assignments table on your instance by using the following URL and replacing the [INSTANCENAME] part with your instance name.
  • URL:
    https://[INSTANCENAME].service-now.com/now/nav/ui/classic/params/target/sys_declarative_action_assignment_list.do%3Fsysparm_query%3Dmodel%253D45757de50fa52010ad4437a98b767e33%26sysparm_first_row%3D1%26sysparm_view%3D

MGOPW_1-1727366087678.png

 

These action assignments can be added by going through the steps we just outlined above to add them to the Declarative action event mappings on the component, the body's handled events section, and the page event mappings section.

Below are some of the actions dispatched by sn-vtb sub-components:
"VTB#CARD_MOVED" 
"VTB#ADD_CARD_CLICKED"
"VTB#FREEFORM_CARD_ADDED"
"VTB#DATA_DRIVEN_CARD_ADDED"
"VTB#FREEFORM_PLACEHOLDER_CARD_REMOVED"
"VTB#CARD_ARCHIVED"
"VTB#LANE_MOVED"
"VTB#LANE_DELETED"
"VTB#CREATE_LANE_ACTION_PERFORMED"
"VTB#LANE_HIDE_SELECTED"
"VTB#LANE_HEADER_UPDATED"
"VTB#SWIMLANE_MOVED"
"VTB#SWIMLANE_ACTION_PERFORMED"
"VTB#AMB_MESSAGE_RECEIVED"
"VTB#CONFIRMATION_MODAL_SELECTED"

Drag and Drop

Using the same Kanban Board component and page we were already working on, here are the steps to enable the drag and drop functionality.
 
To set up lanes drag and drop:
  1. On the Kanban Board component properties panel, set the following properties as follows:
    • Vertical lane drag-and-drop option: true
    • Vertical lane-drag source display template: sn-vtb-lane-src-placeholder
    • Vertical lane-drop destination display template: sn-vtb-lane-dest-placeholder
This creates a placeholder component for where you want to move the lane to, and where it previously existed.
MGOPW_2-1727366172286.png
To set up cards drag and drop:
  1. On the Kanban Board component properties panel, set the following properties as follows:
    • Card drag-and-drop option: true
    • Card-drag source display template: sn-vtb-card-src-placeholder
    • Card-drop destination display template: sn-vtb-card-dest-placeholder
This creates a placeholder component for where you want to move the card to, and where it previously existed.
MGOPW_3-1727366199072.png
Save the page, and test. You will be able to see and use the drag and drop functionality, but you will see your changes aren't obeyed and the card/lane just reverts to its original location. We can fix this by listening for and configuring the events.
 
Configuring the events
  1. Create a new Client Script, title it "Lane Moved".
  2. Add the following code to swap the position of the lane object in the lanes array:
    function handler({api, event, helpers, imports}) {
            const {state: { lanes } } = api;
            const { payload : {fromPosition, toPosition} } = event;
            const temp = lanes[fromPosition];
            const tempLanes = lanes.toSpliced(fromPosition, 1);
            const newLanes = tempLanes.toSpliced(toPosition, 0, temp);
        
            api.setState('lanes', newLanes);
        }
  3. Create a new Client Script, title it "Card Moved".
  4. Add the following code to change the lane ID of the card, and then update the order of all the cards in the lane to match the new order:
    function handler({api, event, helpers, imports}) {
            const {state: { cards } } = api;
            const { payload: { id, toLane, fromPosition, toPosition } } = event;
        
            const newCards = JSON.parse(JSON.stringify(cards));
            const movedCard = newCards.find((card) => card.id === id);
            if (movedCard) {
                movedCard.lane_id = toLane;
                const cardsInLane = newCards
                    .filter(card => card.lane_id === toLane)
                    .sort((a,b) => a.order - b.order);
               
                movedCard.order = toPosition;
                cardsInLane.forEach((card, idx) => {
                    if (card.id !== movedCard.id) {
                        if (idx < toPosition) card.order = idx;
                        else if (idx > toPosition) card.order = idx + 1;
                        else card.order = toPosition - 1 >= 0 ? toPosition - 1 : toPosition + 1;
                    }
                });
        
                api.setState('cards', newCards);
            }
        }
    NOTE: If changing the lane updates a value for the record, you will need to include some type of script to validate the change and update the record on the backend. This Client Script does not update the record.
  5. Navigate to the Kanban Board's events and add the "Now Visual Board - Card Moved" event mapping.
  6. Add the Client Script "Card Moved" to the event handler when prompted.
  7. Add the "Now Visual Board - Lane Moved" event mapping.
  8. Add the Client Script "Lane Moved" to the event handler when prompted.
  9. Save and test. Your lane and card changes should now reflect visually on the board. If you refresh, it will revert back to the data we set in the configuration above, as we have not created any scripts to edit the record data. 

Swimlanes

Using the same Kanban Board component and page we were already working on, here are the steps to enable the Swimlane functionality.
  1. Create a new Client State Parameter:
    1. Name: swimlanes
    2. Type: JSON
  2. Go to the Kanban Board configuration panel, and set the following properties:
    1. Swimlane header template: sn-vtb-swimlane-header
    2. Swimlanes: Dynamically data bind to the swimlanes Client State Parameter. (@STate.swimlanes)
  3. In the Kanban Board configuration panel, go to the Styles tab.
  4. Set the Height to 1200 px.
  5. Update the "Format data" Client script by copy and pasting the following code:
    function handler({api, event, helpers, imports}) {
            // Grabbing incidents from the GlideRecord Collection Query
            const incidents = api.data.gliderecord_collection_query_1.output.data.GlideRecord_Query.incident._results;
            const cards = [];
            const laneObject = {};
            const swimLaneObject = {};
        
            
            // For each incident, create a card and push to the array
            incidents.forEach((incident, index) => {
                cards.push({
                    id: incident._row_data.uniqueValue,
                    title: incident.number.displayValue,
                    order: index,
                    
                    // Sets what lane the card is assigned to
                    lane_id: incident.priority.value,
        
                    // Adding the swimlane ID and setting it to the incident's state
                    swim_lane_id: incident.state.value,
        
                    // Options, Attachments and Record properties are the bare minimum required to get the card template to render
                    options: {
                        "board_type": "FREEFORM",
                        "card_type": "classic",
                    },
                    attachments: [],
                    record: {
                        short_description: { display_value: incident.short_description.displayValue },
                        description: { display_value: incident.description.displayValue },
                    }
                });
        
                // Starts setting up the lanes 
                laneObject[incident.priority.value] = {
                    value: incident.priority.value,
                    displayValue: incident.priority.displayValue
                };
        
                // Adding the swimlane object configuration
                swimLaneObject[incident.state.value] = {
                    value: incident.state.value,
                    displayValue: incident.state.displayValue
                };
        
            });
          
            const lanes = [];
            Object.keys(laneObject).forEach(key => {
                
                // Creates the lanes
                lanes.push({
                    id: key,
                    title: laneObject[key].displayValue,
                    name: laneObject[key].displayValue,
                    options: {
                        "board_type": "FREEFORM"
                    },
                    // Styles for the lane and lane header, CSS
                    "_style_" : {
                        "sn-lane": {
                            width: "300px",
                            padding: "5px",
                        },
                        "sn-lane-header": {
                            width: "300px",
                            padding: "5px",
                        },
                    }
                });
            });
            // Configuring the swimlanes object
            const swimlanes = [];
                Object.keys(swimLaneObject).forEach((key, index) => {
        
                    // Creates the swimlanes
                    swimlanes.push({
                        id: key,
                        order: index,
                        cardsCount: cards.filter(
                                (c) => c.swim_lane_id === key
                            ).length,
                        title: swimLaneObject[key].displayValue,
                        name: swimLaneObject[key].displayValue,
                        addLaneButton: false,
                        options: {
                            "board_type": "FREEFORM"
                        },
                        "is_swim_lane": true,
        
                        // Styles for the swimlane and the swimlane body, CSS
                        "_style_": {
                            'sn-swimlane': {
                            },
                            'swimlane-body': {
                                padding: '3px 3px',
                            }
                        },
                    });
                });
            // Sets the client state parameters
            api.setState('lanes', lanes);
            api.setState('cards', cards);
            // Adding the swimlanes client state parameter
            api.setState('swimlanes', swimlanes);
        
        }
  6.  Save and test. You should now be able to see the swimlanes configured according to the state of the incident. Note you cannot collapse the swimlanes, and that the swimlane drag and drop has yet to be configured.
    MGOPW_0-1727366550287.png

Enabling swimlane drag and drop

Swimlane actions are configured a bit differently, there is only one event action to handle almost any action performed, but otherwise they are set up similarly to how we added an event handler on the card clicked event earlier in the article.
Most of the events done on the swimlane generate the event VTB#SWIMLANE_ACTION_PERFORMED. It has a payload consisting of two fields:
  • payload - JSON - Action and payload data from the action generated by the swimlane.
  • type - String - Action type (ex. VTB#TOGGLE_SWIMLANE_BODY)
To add the event:
  1. Create a new Client script and title it "Swimlane action", copy the following code into it:
    function handler({api, event, helpers, imports}) {
            console.log("Swimlane event", event);
        }
  2. Click on the Body section of the page and, in the Page configuration panel to the right, click on Events.
  3. Go to the bottom to the "Handled events" section and click the Add button.
  4. Add the following information to the "Create an event" modal:
    1. Event label: VTB#SWIMLANE_ACTION_PERFORMED
    2. Click the "+Add" button next to the Payload fields heading and add two fields:
      1. Name: payload
        1. Label: Payload
        2. Type: JSON
      2. Name: type
        1. Label: Type
          1. Type: String
  5. Navigate back to the top of the Events tab in the Page configuration panel and click "Add event mapping".
  6. Select the event mapping we just created.
  7. When prompted to select an event handler, select the Client Script we just created titled "Swimlane action".
  8. Now that we have the event on the page, we need to add it to the Kanban Board. Click on the Kanban Board component in the component tree to the left.
  9. Scroll to the bottom of the component properties and enter the following JSON in to the "Declarative-action map" field, completely replacing what was already there.
    {
            "VTB#CONFIRMATION_MODAL_SELECTED": {
                "actionDispatch": "VTB#CONFIRMATION_MODAL_SELECTED",
                "actionPayload": "{\r\n        \"modalId\": \"{{modalId}}\",\r\n        \"modalData\": \"{{modalData}}\"\r\n}",
                "actionType": "uxf_client_action",
                "assignmentId": "0c8e127453922010c5e2ddeeff7b125f",
                "label": "Confirmation Modal Selected",
                "name": "VTB#CONFIRMATION_MODAL_SELECTED"
            },
            "VTB#SWIMLANE_ACTION_PERFORMED": {
            "actionDispatch": "VTB#SWIMLANE_ACTION_PERFORMED",
            "actionPayload": "{\r\n        \"payload\": \"{{payload}}\",\r\n        \"type\": \"{{type}}\"\r\n}",
            "actionType": "uxf_client_action",
            "assignmentId": "f518127053922010c5e2ddeeff7b125a",
            "label": "Swimlane Action Performed",
            "name": "VTB#SWIMLANE_ACTION_PERFORMED"
          }
        }
  10. Now that the declarative action has been mapped to the Kanban Board component, click on the "Configure declarative action event mappings" button at the very bottom of the component's configuration panel.
  11. Under "Swimlane Action Performed", click "Add a new event handler".
  12. Fill in the fields as follow:
    1. Name: Swimlane Action Mapping
    2. Click the dropdown where you see the name of the event we created earlier, and select the VTB#SWIMLANE_ACTION_PERFORMED event instead.
    3. Dynamically data bind the following fields by clicking on the Data binding icon, double clicking on the empty space at the top where it says "Add a data output to this area", then typing in the information below:
      1. Payload: @payload.payload
      2. Type: @payload.type
    4. Click "Save" once you've filled in both fields, then click "Done".
  13. Save the page and test by trying to toggle a swimlane closed. Open the browser console (F12) and observe the event firing the logging statement in the Client Script.

 

Conclusion

This article walked you through the initial configuration of the Kanban Board component. Feel free to post any questions you may have in the comments below.

 

Check out Part 2 of this article series for more information and reference material for further configuring these components.

 

You can also check out the You and I Builder Live! livestream where we covered this component.

Check out the Next Experience Center of Excellence for more resources

Comments
mmyers103343997
Tera Explorer

Hello! 
The link to "Part 2 of this series " also links to the YouTube video and not another article.

 

Amazing job on this! Thank you SO MUCH!

MGOPW
ServiceNow Employee
ServiceNow Employee
KarthikeyanR
Tera Contributor

HI @MGOPW / @Brad Tilton 

How to enable Quick Panel (User Names, Labels) on Top in Kanban Board. I don't find the details in document as well.
 KarthikeyanR_0-1735136255874.png

Regards,
Karthikeyan R

ivsonsilva
Tera Explorer

But did anyone have the same problem during development? My modalID and modalData are coming in null.image.png

spike
Mega Sage

I'm having the same problem. What I've found is that the Save in Step 12 of Configuring events is not actually saving the values. If you look the record up in the table "UX Add-on Event Mapping" you will see this:

spike_0-1758296731048.png

As you can see, nothing saved. I've manually updated the record so it looks like this:

{
    "container": {
        "modalData": {
            "binding": {
                "address": [
                    "modalData"
                ]
            },
            "type": "EVENT_PAYLOAD_BINDING"
        },
        "modalId": {
            "binding": {
                "address": [
                    "modalId"
                ]
            },
            "type": "EVENT_PAYLOAD_BINDING"
        }
    },
    "type": "MAP_CONTAINER"
}

And that seems to have done the trick..

 

@MGOPW is this something you need to know about?



Version history
Last update:
‎07-30-2025 07:00 AM
Updated by:
Contributors