This project serves as a robust, production-ready template for integrating React (v17) and Tailwind CSS within the ServiceNow UI Builder.
It addresses the specific build constraints of the ServiceNow CLI, ensures seamless communication between the Shadow DOM and the platform, and provides a modern Developer Experience (DX).
Before you clone the repository, ensure you have the following installed and configured:
-
Node.js & npm
- Recommended: Node v20 or v22.
-
ServiceNow CLI (
snc)- You must have the CLI installed and authorized to your instance.
- Official Installation Guide
-
UI Component Extension
- The core CLI is not enough; you need the component extension.
- Run:
snc extension add --name ui-component - Official Setup Guide
-
A ServiceNow Instance
- Targeting Zurich or newer.
Use a unified command to handle both the ServiceNow server and the Tailwind JIT compiler simultaneously.
-
Install Dependencies:
npm install
-
Start Development:
npm start
- This command runs
concurrently:- Tailwind Watcher: Monitors
.js/.jsxfiles and recompiles CSS instantly. - ServiceNow Server: Serves the widget locally with Hot Reload.
- Tailwind Watcher: Monitors
- This command runs
The project is currently configured with a demo vendor prefix (x-513106). Before deploying, you must update the vendor prefix and scope name to match your environment.
Example Scenario:
- Current Prefix:
x-513106 - Your Prefix:
x-999999(Check sys_propertyglide.appcreator.company.code)
- Rename the component folder:
- Rename
src/x-513106-ui-framework-react-wrappertosrc/x-999999-ui-framework-react-wrapper.
- Rename
- Global Find & Replace:
- Open "Search" in VS Code (
Ctrl+Shift+F). - Search for:
x-513106 - Replace with:
x-999999 - Note: This handles file imports, tag names, and the config key.
- Open "Search" in VS Code (
- Update Scope Name (Underscores):
- Search for:
x_513106 - Replace with:
x_999999 - Note: This updates the
scopeNameinnow-ui.json.
- Search for:
If you prefer to make changes manually to ensure precision, follow these steps:
Locate the now-ui.json file in the root directory.
- Component Key: Change the property key inside
componentsfromx-513106-ui-framework-react-wrappertox-999999-ui-framework-react-wrapper. - Scope Name: Change
"scopeName": "x_513106_ufrw"to"scopeName": "x_999999_ufrw".
- Navigate to the
srcfolder. - Rename the folder
x-513106-ui-framework-react-wrappertox-999999-ui-framework-react-wrapper.
- Open
src/x-999999-ui-framework-react-wrapper/index.js. - Update the
createCustomElementcall:createCustomElement('x-999999-ui-framework-react-wrapper', ...
- Open
src/index.js. - Update the import path:
import './x-999999-ui-framework-react-wrapper';
- Open
example/element.js. - Update the import and the HTML tag:
import '../src/x-999999-ui-framework-react-wrapper'; const el = document.createElement('DIV'); document.body.appendChild(el); el.innerHTML = ` <x-999999-ui-framework-react-wrapper></x-999999-ui-framework-react-wrapper> `;
Once you have updated the vendor prefix, you can deploy to your instance. Due to strict version requirements, always perform a clean install if you encounter build errors.
rm -rf node_modules package-lock.jsonnpm installsnc ui-component deploy --profile <profile> --force
The core philosophy is isolation and compatibility.
- The Gatekeeper: The only file that communicates directly with the ServiceNow
createCustomElementAPI. - Shadow DOM Manager: It manually injects the compiled Tailwind CSS + SCSS styles into the Shadow Root to ensure proper styling.
- Event Retargeting: Implements manual event retargeting logic. Since React events (like
onClick) struggle to bubble out of Shadow DOM, this layer fixes the event path. - Error Boundary: Acts as a safety net. If the React application crashes, the Gate catches the error and displays a fallback UI instead of breaking the entire UI Builder page.
- The Translator: Decouples the UI Framework from the React Application.
- Prop Normalization: Receives raw properties from ServiceNow and transforms them into clean props for the React app.
- Action Handling: It defines the specific callback functions. The React App calls
onAction(data), and the Bridge translates this into a specific ServiceNowdispatch('EVENT_NAME', data)call.
- Pure React 17: A standard React application that knows nothing about ServiceNow,
snc, or Shadow DOM. - Reusable: Because it is isolated, this component could theoretically be moved to a standard web app and would still work (just by replacing the Bridge).
- State Management: Uses standard hooks (
useState,useEffect) and Context API (ThemeProvider) to demonstrate robust state management across the component tree.
To ensure the stability of the bridge between ServiceNow and React, the architecture relies on three mandatory layers that should remain structurally unchanged.
Think of this as an "Onion Architecture":
[ UI Builder ]
β¬ (Props) β¬ (Events)
-------------------------------------------------------
1. π‘οΈ UI Framework Wrapper (index.js)
| Communicates with UI Builder
| Uses Snabbdom Renderer
| Handles Shadow DOM & Style Injection
-------------------------------------------------------
β¬ (UI Framework props) β¬ (Dispatch)
-------------------------------------------------------
2. πͺ Root React Component (ReactBridge.js)
| Communicates with UI Framework Wrapper
| Receives 'dispatch' & 'properties'
| Communicates with other React components
-------------------------------------------------------
β¬ (Your props) β¬ (Callbacks)
-------------------------------------------------------
3. β¨ Developer Land (Your Components)
| Pure React World
| Hooks, State, Effects, Routing...
-------------------------------------------------------
You do not need to modify index.js to add new events. The ReactBridge.js receives a universal dispatch function via props.
-
React Bridge:
const handleHover = (data) => { // Just call dispatch with any event name dispatch('CHART_HOVER', { id: data.id }); };
-
ServiceNow Config (
now-ui.json):"actions": [ { "name": "CHART_HOVER", "label": "On Chart Hover", "description": "Fires when user hovers over a chart", "payload": [{ "name": "id", "label": "ID" }] } ]
-
Result: The event appears in UI Builder's "Events" tab automatically.
To pass data FROM ServiceNow TO React:
-
ServiceNow Config (
now-ui.json):"properties": [ { "name": "reportTitle", "fieldType": "string", "default": "My Report" } ]
-
React Bridge: The property is automatically available in
ReactBridge.jsprops.// ReactBridge.js export const ReactBridge = ({ reportTitle, dispatch }) => { return <h1>{reportTitle}</h1>; };
For this wrapper to build successfully, these configurations are mandatory (already included):
- Babel Patch: Enforces "classic" JSX runtime (ServiceNow Webpack limitation).
- Tailwind Config: Configured to scan
./src/**/*.{js,jsx}for JIT generation. - NPM Scripts: Utilizes
concurrentlyfor a smooth dev experience.
This project uses React 17 for maximum stability with the ServiceNow build system.
- β No Concurrent Mode (
useTransition). - β No Automatic Batching in async/timeouts.
- β Uses
ReactDOM.render()instead ofcreateRoot().
Note: For 99% of standard UI components (dashboards, forms, charts), React 17 is perfectly sufficient.
- Shadow DOM Isolation: Blocks global styles.
- Legacy Build System: The internal Webpack config often fails to compile modern libraries using CSS-in-JS engines or complex ESM exports.
Do not install these libraries, as they break the build or render incorrectly:
- Material UI (MUI v5/v6) - Build fails (Emotion dependency).
- Chakra UI - Build fails (Emotion dependency).
- Ant Design - Global styles do not penetrate Shadow DOM.
- Bootstrap (JS/CSS) - Relies on global CSS.
The only 100% guaranteed approach is to build components yourself using standard React and Tailwind CSS.
- Layouts/Buttons/Cards: Build them as pure React components. Style them with Tailwind utility classes.
- Logic: Use standard React Hooks (
useState,useEffect). - Charts:
Chart.js/react-chartjs-2(Verified). - Data Fetching: Native
fetchoraxios.
"Headless" libraries (like Radix UI, Headless UI, or React Aria) theoretically should work better because they don't rely on CSS-in-JS engines. However, they have NOT been verified in this specific wrapper build chain.
Recommendation: If you need complex UI (like a Datepicker or Combobox), try to find a lightweight, zero-dependency implementation or build a custom one. If you decide to try a library, install and deploy it immediately to verify compatibility before building features on top of it.
Unlike traditional setups, we do not use a CDN. Instead, we compile Tailwind CSS directly into the component to ensure Shadow DOM isolation (styles do not bleed out, global styles do not bleed in).
- Source:
src/tailwind-input.css(Entry point for Tailwind directives and custom CSS). - Build: The watcher compiles your React usage into
src/tailwind-generated.scss. - Injection:
index.jsinjects the generated string into the component's Shadow Root<style>tag.
- Utility Classes: Just use them in JSX:
<div className="p-4 bg-blue-500 hover:opacity-50">. The watcher detects them and generates the CSS automatically. - Custom CSS: If you need specific styles (fonts, animations) that Tailwind can't handle, add them to
src/tailwind-input.cssinside the@layerdirective:@layer components { .my-custom-btn { background-color: red !important; } }
This project includes a technically functional MyReactModal component that uses React Portals to break out of the Shadow DOM and render into document.body (while maintaining style isolation via a nested Shadow Root).
While the React Modal implementation proves it can be done, the recommended best practice is:
- React: Dispatch an event (e.g.,
OPEN_MODAL_REQUESTED). - UI Builder: Handle the event and open a native ServiceNow Modal.
Why?
- Consistency with the rest of the platform UI.
- Better Accessibility (a11y) support out-of-the-box.
- Less code to maintain within the custom widget.
This wrapper was born out of frustration and countless hours of fighting the ServiceNow build system.
If this template saved you time, headache, or prevented a nervous breakdown while fighting the Shadow DOM, consider buying me a coffee. It keeps the motivation high!
Feel free to fork it, break it, and fix it. If you find a way to make Material UI work or have any other improvements, pull requests are more than welcome.
This project is licensed under the MIT License - see the LICENSE file for details.

