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.