UI Page Theming
Horizon Design System theming reference for ServiceNow UI Pages, covering design foundations, layout patterns, controls, and component styling.
Theming: Horizon Design System Foundations
The Horizon Design System defines foundational rules for generating ServiceNow-compliant UIs using HTML, CSS, and design tokens (CSS custom properties).
Core Principles
- Fluid: UIs MUST adapt seamlessly to varying contexts. Use relative units (
rem,%,auto) and spacing tokens, not fixed pixels. - Symphonic: Components MUST visually harmonize. Consistent radius, shadows, and typography ensure unified experience.
- Accessible: All UIs MUST comply with WCAG 2.1 AA: text contrast >= 4.5:1, visible focus states, keyboard navigation, semantic HTML.
Color Roles
primary-- intended for primary actions or important UI elementssecondary-- complementary color to the primary colorsprimary background-- default theme backgroundsecondary background-- strong division between sections (use sparingly)textvariables -- palette for written content like sentences, paragraphs, or captions- Semantic colors:
alert,warning,positive-- convey feedback, status, or urgency
Accessibility Standards
- All interactive elements MUST have distinct states:
base,hover,active,focus-visible,disabled - Focus rings MUST use inset shadows or outlines (no layout reflow)
- Screen-reader labels MUST exist for all actionable elements
- Landmarks (
<header>,<nav>,<main>,<footer>) SHOULD be used
Foundation Token Patterns
| Property | Token Pattern | Available Values |
|---|---|---|
| Spacing | --now-static-space--{size} | xxs (0.125rem), xs (0.25rem), sm (0.5rem), md (0.75rem), lg (1rem), xl (1.5rem), xxl (2rem), 3xl (2.5rem) |
| Drop Shadows | --now-static-drop-shadow--{size} | sm, md, lg, xl, xxl |
| Border Radius | --now-static-border-radius--{size} | sm (0.125rem), md (0.25rem), lg (0.5rem) |
| Font Sizes | --now-static-font-size--{size} | sm (0.75rem), md (1rem), lg (1.25rem), xl (1.5rem) |
| Line Height | --now-static-line-height | 1.25 (unitless) |
| Font Family | --now-font-family | Lato, Arial, sans-serif |
Semantic Token Categories
| Element Type | Token Category | Example |
|---|---|---|
| Buttons, CTAs | actionable | --now-actionable--primary--background-color |
| Inputs, Checkboxes | form-control | --now-form-control-input--primary--border-color |
| Containers, Cards | container | --now-container-card--background-color-alpha |
| Windows, Modals | window | --now-window--border-color |
| Menus, Lists | menu | --now-menu-list--primary--background-color |
| Navigation | navigation | --now-navigation-page_tabs--primary--background-color |
| Alerts, Banners | messaging | --now-messaging--primary_warning--border-color |
| Status Indicators | indicator | --now-indicator--primary_critical--background-color |
| Typography | display-type | --now-display-type_label--font-weight |
Token Integrity Rules
- Each visual property MUST use a valid design token
- Tokens MUST belong to correct semantic category
- Fallbacks MUST follow chained
var()structure - Colors MUST be wrapped with
rgb()orrgba() - No hard-coded color values -- use tokens
/* Correct fallback chain */
var(--now-actionable--primary--background-color, var(--now-color--primary-1, 0,128,163))
/* Correct color wrapping */
background-color: rgb(var(--now-color_background--primary, 255, 255, 255));
Alias Layer
Provide stable, reusable names mapping to instance tokens:
:root {
/* Surfaces & borders */
--snx-color-surface: rgb(var(--now-container--color, 255, 255, 255));
--snx-color-surface-alt: rgb(var(--now-heading--header-primary--color, 245, 247, 249));
--snx-color-border: rgb(var(--now-container--border-color, 207, 213, 215));
/* Text */
--snx-color-text: rgb(var(--now-color_text--primary, 16, 23, 26));
--snx-color-text-muted: rgb(var(--now-color_text--secondary, 75, 85, 89));
/* Actions & focus */
--snx-color-primary: rgb(var(--now-actionable--primary--background-color, 0, 128, 163));
--snx-color-on-primary: rgb(var(--now-actionable_label--primary--color, 255, 255, 255));
--snx-color-focus: rgb(var(--now-color_focus-ring, 53, 147, 37));
/* Spacing */
--snx-space-inner: var(--now-static-space--md, 0.75rem);
--snx-space-outer: var(--now-static-space--lg, 1rem);
/* Border radius - component-specific */
--snx-radius-button: var(--now-actionable--border-radius, 6px);
--snx-radius-input: var(--now-form-control-input--primary--border-radius, 2px);
--snx-radius-container: var(--now-container--border-radius, 8px);
/* Typography */
--snx-font-body: var(--now-font-family, system-ui, sans-serif);
--snx-line-height: var(--now-static-line-height, 1.25);
}
Rules for aliases: NEVER create generic aliases that ignore component requirements. ALWAYS use component-specific aliases. Aliases MUST remain stable between generations.
Dark Mode
Each token has light and dark mode values. The theme system handles switching automatically:
/* Token automatically switches between light/dark */
background-color: rgb(var(--now-container--color, 255, 255, 255));
color: rgb(var(--now-color_text--primary, 16, 23, 26));
Browser Default Resets
When using base HTML elements, ALWAYS reset browser defaults that conflict with design tokens:
.form {
display: grid;
margin: 0;
padding: 0;
border: none;
grid-template-columns: 1fr 1fr;
gap: var(--now-static-space--lg, 1rem);
}
Theming: Layout Patterns
Cards
Cards present grouped content in elevated surfaces. They MUST contain header, body, and optional footer slots.
.card {
background-color: rgb(var(--now-color_background--secondary, 245, 246, 247));
border: 1px solid rgb(var(--now-container-card--border-color, 207, 213, 215));
border-radius: var(--now-container-card--border-radius, 8px);
box-shadow: var(--now-static-drop-shadow--sm);
display: flex;
flex-direction: column;
overflow: hidden;
}
.card__header {
padding: var(--now-static-space--lg, 1rem);
border-bottom: 1px solid rgb(var(--now-container-card--border-color, 207, 213, 215));
font-weight: 600;
color: rgb(var(--now-color_text--primary, 16, 23, 26));
background-color: rgb(var(--now-container--color, 255, 255, 255));
}
.card__body {
padding: var(--now-static-space--lg, 1rem);
color: rgb(var(--now-color_text--primary, 16, 23, 26));
flex: 1;
}
.card__footer {
padding: var(--now-static-space--lg, 1rem);
border-top: 1px solid rgb(var(--now-container-card--border-color, 207, 213, 215));
display: flex;
justify-content: flex-end;
gap: var(--now-static-space--sm, 0.5rem);
}
Modals and Dialogs
Modals use window category tokens. They MUST include header, body, and optional footer regions. Focus MUST trap within modal.
.modal-overlay {
position: fixed;
inset: 0;
background-color: rgba(
var(--now-window_overlay--background-color, 0, 0, 0),
var(--now-window_overlay--background-color-alpha, 0.5)
);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background-color: rgb(var(--now-window--background-color, 255, 255, 255));
border: 1px solid rgb(var(--now-window--border-color, 207, 213, 215));
border-radius: var(--now-window--border-radius, 8px);
width: 90%;
max-width: 600px;
max-height: 90vh;
display: flex;
flex-direction: column;
box-shadow: var(--now-static-drop-shadow--lg);
}
.modal__header {
padding: var(--now-static-space--lg, 1rem);
border-bottom: 1px solid rgb(var(--now-window--border-color, 207, 213, 215));
font-weight: var(--now-window_header--font-weight, 600);
color: rgb(var(--now-color_text--primary, 16, 23, 26));
display: flex;
justify-content: space-between;
align-items: center;
}
.modal__body {
padding: var(--now-static-space--lg, 1rem);
overflow-y: auto;
flex: 1;
color: rgb(var(--now-color_text--primary, 16, 23, 26));
}
.modal__footer {
padding: var(--now-static-space--lg, 1rem);
border-top: 1px solid rgb(var(--now-window--border-color, 207, 213, 215));
display: flex;
justify-content: flex-end;
gap: var(--now-static-space--sm, 0.5rem);
}
Forms Layout
.form {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--now-static-space--lg, 1rem);
}
@media (max-width: 768px) {
.form {
grid-template-columns: 1fr;
}
}
.form__label {
font-weight: var(--now-form-control_label--primary--font-weight, 600);
color: rgb(var(--now-form-control_label--primary--color, 16, 23, 26));
margin-bottom: var(--now-static-space--sm, 0.5rem);
font-size: 0.875rem;
}
.form__input {
height: calc(1rem + var(--now-form-field--scale-size-block, 1) * 2rem / 2);
padding: 0 var(--now-static-space--md, 0.75rem);
border: 1px solid rgb(var(--now-form-control-input--primary--border-color, 207, 213, 215));
border-radius: var(--now-form-control-input--primary--border-radius, 2px);
background-color: rgb(var(--now-input--background-color, 255, 255, 255));
color: rgb(var(--now-color_text--primary, 16, 23, 26));
}
Lists
.list {
border: 1px solid rgb(var(--now-container--border-color, 207, 213, 215));
border-radius: var(--now-container--border-radius, 8px);
overflow: hidden;
}
.list__header {
background-color: rgb(var(--now-menu-list--primary--background-color, 245, 247, 249));
font-weight: 600;
color: rgb(var(--now-color_text--primary, 16, 23, 26));
border-bottom: 1px solid rgb(var(--now-container--border-color, 207, 213, 215));
}
.list__row {
border-bottom: 1px solid rgb(var(--now-container--border-color, 207, 213, 215));
background-color: rgb(var(--now-container--color, 255, 255, 255));
color: rgb(var(--now-color_text--primary, 16, 23, 26));
}
.list__row:hover {
background-color: rgb(var(--now-menu-list--primary--background-color--hover, 235, 237, 239));
}
Tables
Tables MUST use semantic HTML (<table>, <thead>, <tbody>, <tr>, <td>, <th>).
.table {
width: 100%;
border-collapse: collapse;
border: 1px solid rgb(var(--now-container--border-color, 207, 213, 215));
}
.table th, .table td {
padding: var(--now-static-space--md, 0.75rem) var(--now-static-space--lg, 1rem);
border-bottom: 1px solid rgb(var(--now-container--border-color, 207, 213, 215));
text-align: left;
}
.table thead {
background-color: rgb(var(--now-color_background--tertiary, 226, 229, 231));
font-weight: 600;
}
.table tbody tr {
background-color: rgb(var(--now-color_background--primary, 255, 255, 255));
}
.table tbody tr:hover {
background-color: rgba(var(--now-color--primary-1, 245, 246, 247), var(--now-opacity--least, 0.1));
}
Theming: Controls
Buttons
Buttons use the actionable token category. All controls MUST support states: base, hover, active, focus-visible, disabled.
.btn {
display: inline-flex;
align-items: center;
gap: var(--now-static-space--sm, 0.5rem);
min-block-size: calc(1rem + var(--now-button--scale-size-block, 1) * 2rem / 2);
padding-inline: calc(1rem * var(--now-button--scale-size-inline, 1));
border: 1px solid transparent;
border-radius: var(--now-actionable--border-radius, 6px);
font-family: var(--now-font-family, system-ui, sans-serif);
font-size: 1rem;
cursor: pointer;
transition: background-color 100ms linear;
}
.btn--primary {
background-color: rgb(var(--now-actionable--primary--background-color, 0, 128, 163));
color: rgb(var(--now-actionable_label--primary--color, 255, 255, 255));
border-color: rgb(var(--now-actionable--primary--border-color, 0, 128, 163));
}
.btn--primary:hover {
background-color: rgb(var(--now-actionable--primary--background-color--hover, 0, 138, 173));
}
.btn--primary:focus-visible {
box-shadow: inset 0 0 0 2px rgb(var(--now-color_focus-ring_shadow, 53, 147, 37));
}
.btn--primary:disabled {
opacity: 0.4;
cursor: not-allowed;
}
Secondary/Tertiary Button Alpha Channel (CRITICAL): Secondary/tertiary buttons use RGBA with alpha for transparency, NOT solid RGB:
.btn--secondary {
background-color: rgba(
var(--now-actionable--secondary--background-color, 0, 113, 143),
var(--now-actionable--background-color-alpha, 0)
);
}
.btn--secondary:hover {
background-color: rgba(
var(--now-actionable--secondary--background-color--hover, 0, 76, 97),
var(--now-actionable--background-color-alpha--hover, 0.08)
);
}
Input Fields
input[type="text"], input[type="email"], input[type="number"], textarea {
height: calc(1rem + var(--now-form-field--scale-size-block, 1) * 2rem / 2);
padding: 0 var(--now-static-space--md, 0.75rem);
border: 1px solid rgb(var(--now-form-control-input--primary--border-color, 115, 127, 132));
border-radius: var(--now-form-control-input--primary--border-radius, 2px);
background-color: rgb(var(--now-input--background-color, 255, 255, 255));
color: rgb(var(--now-color_text--primary, 16, 23, 26));
}
input:focus-visible {
outline: none;
border-color: rgb(var(--now-form-control-input--primary--border-color--focus, 53, 147, 37));
box-shadow: inset 0 0 0 1px rgb(var(--now-form-control_focus-ring--primary--color, 53, 147, 37));
}
Focus states MUST use inset box-shadow or outline (no border width changes that cause layout reflow).
Checkboxes and Radio Buttons
Checkboxes use form-control-input-selection--primary tokens. Radio buttons use form-control-input-selection--secondary tokens.
Toggles
Track and handle derive from distinct subcategories (form-control_track, form-control_handle).
Pills and Chips
Background and border derive from pill tokens. Remove icons MUST inherit text color via currentColor.
Theming: Components
Severity Variant Reference
All messaging, alert, and indicator components use these semantic variants:
| Display Name | Token Variant | Use For |
|---|---|---|
| Success | positive | Successful operations, valid states |
| Error | critical | Errors, failures, urgent issues |
| Warning | warning | Warnings, cautions |
| Info | info | Informational messages |
ALWAYS use positive, critical, warning, info -- NEVER "success", "error", or "informational".
Alerts and Banners
Alert components use alert tokens for backgrounds and borders. Text color fallback chain: --now-alert--color -> --now-messaging_label--primary--color -> --now-color_text--primary.
.alert--warning {
background-color: rgb(var(--now-alert--warning--background-color, 239, 224, 176));
border: 1px solid rgb(var(--now-alert--warning--border-color, 221, 182, 101));
color: rgb(var(
--now-alert--color,
var(--now-messaging_label--primary--color, var(--now-color_text--primary, 16, 23, 26))
));
}
Toast Notifications MUST use --now-container-card--background-color (NOT --now-container--color) for proper dark mode inversion.
Tabs
Tabs use --now-tabs--* design tokens. Active tabs show bottom border only (not full border). Tabs MUST NOT have gaps between them. Tabs MUST NEVER have vertical scrolling.
| State | Border | Font Weight |
|---|---|---|
| Base | transparent bottom | normal |
| Hover | focus-ring bottom | normal |
| Active | focus-ring bottom | 600 |
.tabs {
display: flex;
border-bottom: 1px solid rgb(var(--now-container--border-color, 207, 213, 215));
overflow-x: auto;
overflow-y: hidden;
}
.tab {
padding: 0.75rem 1rem;
color: rgb(var(--now-tabs--color, 16, 23, 26));
cursor: pointer;
border: none;
border-bottom: 0.125rem solid transparent;
background-color: rgba(var(--now-tabs--background-color, 255, 255, 255), var(--now-tabs--background-color-alpha, 0));
border-radius: 0;
margin-bottom: -1px;
}
.tab--active {
border-bottom-color: rgb(var(--now-color_focus-ring, var(--now-color--secondary-1, 0, 128, 163)));
font-weight: 600;
}
Side Navigation
Use content-tree tokens (NOT navigation-sidebar) for proper contrast and dark mode support.
.sidenav {
background-color: rgb(var(--now-content-tree--background-color, 255, 255, 255));
width: 240px;
border: 1px solid rgb(var(--now-container-card--border-color, 207, 213, 215));
border-radius: 6px;
}
.sidenav__item {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
color: rgb(var(--now-content-tree--color, 16, 23, 26));
border-left: 4px solid transparent;
}
.sidenav__item--active {
background-color: rgb(var(--now-content-tree--background-color--selected, 224, 234, 222));
border-left-color: rgb(var(--now-color--primary-1, 0, 128, 163));
font-weight: 600;
}
Badges and Indicators
Badge text MUST use --now-indicator_label--{variant}--color (note _label subcategory), NOT base indicator color.
.badge--positive {
background-color: rgb(var(--now-indicator--primary_positive--background-color, 62, 134, 0));
color: rgb(var(--now-indicator_label--primary_positive--color, 255, 255, 255));
border: 1px solid rgb(var(--now-indicator--primary_positive--border-color, 48, 103, 0));
}
.badge--critical {
background-color: rgb(var(--now-indicator--primary_critical--background-color, 229, 34, 57));
color: rgb(var(--now-indicator_label--primary_critical--color, 255, 255, 255));
}
Progress Indicators
.progress {
width: 100%;
height: 0.5rem;
background-color: rgb(var(--now-progress-bar--background-color, 240, 242, 243));
border-radius: var(--now-static-border-radius--lg, 0.5rem);
overflow: hidden;
}
.progress__fill {
height: 100%;
background-color: rgb(var(--now-progress-bar_path--initial--background-color, 0, 128, 163));
transition: width 200ms ease;
}
.spinner {
border: 2px solid rgba(var(--now-loading_indicator--primary--color, 0, 128, 163), 0.2);
border-top-color: rgb(var(--now-loading_indicator--primary--color, 0, 128, 163));
border-radius: 50%;
width: 1.5rem;
height: 1.5rem;
animation: spin 1s linear infinite;
}
@keyframes spin {
100% { transform: rotate(360deg); }
}
Stepper (Progress Indicator)
Step indicators use circular shapes with consistent sizing (2rem diameter). Completed steps use checkmark SVG icon (never unicode). Icons MUST use fill: currentColor.
| State | Tokens |
|---|---|
| Completed | stepper_step--done (green background, green border) |
| Active | stepper_step--partial (white background, green border) |
| Incomplete | stepper_step--none (white background, gray border) |
Pagination
Pagination buttons use bare/borderless style. Navigation arrows MUST use SVG icons, never unicode characters. Active page MUST have distinct background and bold font weight.
Expandable/Collapsible UI
Icons MUST use SVG, never unicode characters. Icon size MUST be 1rem. Rotation animation uses CSS transforms. Icons use fill: currentColor to inherit text color.
Extrapolation Guidelines
When components are not explicitly defined in the Horizon system, extrapolate using established philosophies, token categories, spacing rhythm, and state logic. Select tokens from the closest semantic category. Extrapolated interactive elements MUST implement the standard state set: base, hover, active, focus-visible, disabled.
| New Element | Recommended Analogue | Token Category |
|---|---|---|
| Split button | Button + Menu | actionable for trigger; menu for list |
| Command palette | Menu/Dialog hybrid | menu for list; window for container |
| Inline tag editor | Pill/Chip | form-control-pill |
| Empty state panel | Card + Messaging | container for surface; messaging for accent |