Tokens
Edit tokens.json, then run pnpm tokens. In CSS use the custom property
(var(--canvas)); in StyleX import the constant (color.canvas from src/tokens.stylex.ts),
which compiles to the same var().
Colour
Each swatch is drawn inside a data-color-scheme box, so both color schemes show whatever the page is set to.
| Token | Light | Dark | Usage |
|---|---|---|---|
--canvascolor.canvas | oklch(100% 0 none) | oklch(18.22% 0 none) | Page background; the CSS system color Canvas. Every text token below is checked against it. |
--canvas-textcolor.canvasText | oklch(0% 0 none) | oklch(100% 0 none) | Body copy and headings on canvas. 21:1 light, 18.7:1 dark. |
--surface-softcolor.surfaceSoft | oklch(97.91% 0 none) | oklch(22.64% 0 none) | The faintest tint, for light grouping (a soft card). Everything on canvas keeps its contrast here: gray-text 4.87:1 light, 7.59:1 dark; control-border 4.28:1, 6.06:1. |
--surface-mutedcolor.surfaceMuted | oklch(96.12% 0 none) | oklch(26.86% 0 none) | A quieter panel for secondary content: muted and filled cards, recessed wells, separated bands. The darkest light tint that keeps gray-text at 4.5:1 (4.62:1; dark 6.74:1); control-border 4.06:1, 5.38:1. |
--surface-raisedcolor.surfaceRaised | oklch(100% 0 none) | oklch(30.92% 0 none) | A panel set on a tinted surface (the body of a muted card): white in light, one step lighter in dark. gray-text 5.17:1 light, 5.88:1 dark; control-border 4.54:1, 4.69:1. |
--linkcolor.link | oklch(42.9015% 0.2973 264.05) | oklch(73.9% 0.139 282.5) | Unvisited links on canvas, always underlined. 9.4:1 light, 7.8:1 dark. The browser's LinkText, unchanged. |
--link-visitedcolor.linkVisited | oklch(37.84% 0.172 302.2) | oklch(80.13% 0.1 308.1) | Visited links on canvas. 11:1 light, 9.7:1 dark. The browser's VisitedText, unchanged. |
--link-activecolor.linkActive | oklch(59.6% 0.2446 29.2) | oklch(79.78% 0.116 20) | A link while it is being pressed, on canvas. 4.53:1 light (passes AA by a hair; never use it for resting text), 9.5:1 dark. |
--button-facecolor.buttonFace | oklch(95.21% 0 none) | oklch(46.76% 0 none) | Button fill. Dark is darkened from Chromium's #6b6b6b so white labels reach 6.9:1. |
--button-textcolor.buttonText | oklch(0% 0 none) | oklch(100% 0 none) | Button labels on button-face. 18.3:1 light, 6.9:1 dark. |
--fieldcolor.field | oklch(100% 0 none) | oklch(35.23% 0 none) | Fill of text inputs, textareas and selects. |
--field-textcolor.fieldText | oklch(0% 0 none) | oklch(100% 0 none) | Typed text on field. 21:1 light, 11.2:1 dark. |
--control-bordercolor.controlBorder | oklch(56.58% 0 none) | oklch(68.62% 0 none) | 1px border of buttons, fields, selects and fieldsets. At least 3:1 against canvas, field and button-face in both themes (light 4.5:1 on canvas, 3.95:1 on button-face; dark 6.7:1 on canvas, 4:1 on field). |
--button-bordercolor.buttonBorder | {control-border} | {control-border} | Border of buttons. Same as control-border in Normal UI; a theme whose buttons are a solid fill may set it to button-face, as long as the face itself holds 3:1 against canvas. |
--button-face-hovercolor.buttonFaceHover | {button-face} | {button-face} | Button fill under the pointer. Same as button-face in Normal UI, where hover only darkens the border. |
--button-border-hovercolor.buttonBorderHover | {canvas-text} | {canvas-text} | Button border under the pointer: full-strength text colour. |
--button-border-disabledcolor.buttonBorderDisabled | {button-border} | {button-border} | Dashed border of a disabled button. Same as button-border; a theme whose buttons have no border gives it one here so the button still has an edge. |
--button-edgecolor.buttonEdge | transparent | transparent | Colour of the solid edge under a button (button-edge-width tall). None in Normal UI. |
--button-pressed-facecolor.buttonPressedFace | {canvas-text} | {canvas-text} | Fill of a toggle button that is on (aria-pressed): inverted. |
--button-pressed-textcolor.buttonPressedText | {canvas} | {canvas} | Label of a toggle button that is on, on button-pressed-face. |
--button-primary-facecolor.buttonPrimaryFace | {button-face} | {button-face} | Fill of the one button a form leads with (variant="primary"). Normal UI draws every variant the same: order and wording carry the emphasis. A theme may set it apart. |
--button-primary-textcolor.buttonPrimaryText | {button-text} | {button-text} | Label on button-primary-face. |
--button-primary-bordercolor.buttonPrimaryBorder | {button-border} | {button-border} | Border of a primary button. |
--button-primary-face-hovercolor.buttonPrimaryFaceHover | {button-face-hover} | {button-face-hover} | Primary button fill under the pointer. |
--button-primary-edgecolor.buttonPrimaryEdge | {button-edge} | {button-edge} | Edge under a primary button. |
--button-warning-facecolor.buttonWarningFace | {button-face} | {button-face} | Fill of a button that destroys or cannot be undone (variant="warning"). Identical to a plain button in Normal UI; the label says what it does. |
--button-warning-textcolor.buttonWarningText | {button-text} | {button-text} | Label on button-warning-face. |
--button-warning-bordercolor.buttonWarningBorder | {button-border} | {button-border} | Border of a warning button. |
--button-warning-face-hovercolor.buttonWarningFaceHover | {button-face-hover} | {button-face-hover} | Warning button fill under the pointer. |
--button-warning-edgecolor.buttonWarningEdge | {button-edge} | {button-edge} | Edge under a warning button. |
--placeholdercolor.placeholder | oklch(53.48% 0 none) | oklch(74.76% 0 none) | Placeholder text on field. 5.2:1 light, 5:1 dark. Replaces opacity-faded placeholders that drop under 4.5:1. |
--gray-textcolor.grayText | oklch(53.48% 0 none) | oklch(74.76% 0 none) | Disabled labels and secondary notes on canvas. 5.2:1 light, 8.4:1 dark: disabled text stays readable; the dashed border says it is disabled. |
--accentcolor.accent | oklch(53.51% 0.199 258.1) | oklch(81.97% 0.093 252.8) | accent-color for checkboxes, radios, range, progress and the switch track. A non-text mark, but kept at text strength: 5.3:1 on white (light), 10.8:1 on dark canvas. Deepened from Chromium's #0075ff (4.2:1). |
--focus-ringcolor.focusRing | oklch(50.69% 0.185 257.7) | oklch(81.97% 0.093 252.8) | The 2px solid :focus-visible outline, offset 2px onto canvas. 6:1 light, 10.8:1 dark; also 5.2:1 on light button-face. |
--focus-insetcolor.focusInset | transparent | transparent | A second ring drawn just inside a focused field or list item, for themes whose focus-ring is light (a yellow ring needs a dark inset to hold 3:1 on white). None in Normal UI. |
--markcolor.mark | oklch(96.798% 0.211 109.8) | oklch(96.798% 0.211 109.8) | <mark> highlight fill, the same yellow in both themes. |
--mark-textcolor.markText | oklch(0% 0 none) | oklch(0% 0 none) | Text on mark, black in both themes (19.6:1). Never let <mark> inherit white dark-theme text. |
--highlightcolor.highlight | oklch(86.28% 0.067 254) | oklch(41.84% 0.083 250.4) | ::selection background. |
--highlight-textcolor.highlightText | oklch(0% 0 none) | oklch(100% 0 none) | Selected text on highlight, links included. 13.9:1 light, 8.5:1 dark. |
--rulecolor.rule | {control-border} | {control-border} | <hr> and table cell borders. Same as control-border, so dividers stay visible at 3:1+. |
--status-errorcolor.statusError | {canvas-text} | {canvas-text} | Border and lead word of error messages and invalid fields. Text colour in Normal UI: the words say it is an error, never colour alone. A theme may give it a hue at 4.5:1. |
--status-warningcolor.statusWarning | {canvas-text} | {canvas-text} | Border and lead word of warnings. |
--status-successcolor.statusSuccess | {canvas-text} | {canvas-text} | Border and lead word of success messages. |
--status-infocolor.statusInfo | {canvas-text} | {canvas-text} | Border and lead word of notes. |
--pagecolor.page | {canvas} | {canvas} | Behind a .pd-sheet page. Same as canvas in Normal UI, so a sheet is just the page. |
--mastheadcolor.masthead | {canvas} | {canvas} | Fill of a .pd-masthead band. Same as canvas in Normal UI; a theme may make it a brand colour. |
--masthead-textcolor.mastheadText | {canvas-text} | {canvas-text} | Text and links on masthead. |
Type
--font-serif / font.serif: "Times New Roman", Times, serif
--font-sans / font.sans: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"
--font-mono / font.mono: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, "Courier New", monospace
Headings (content)
| Style | Sample | Usage |
|---|---|---|
h132px / 42px · 700 | The quick brown fox | Page title in long-form content. 2rem on a 42px line (snapped to quarter lines). No space above; half a line below. |
h224px / 30px · 700 | Section heading | Section heading. 1.5rem; 1.5 lines above, half a line below. |
h318.72px / 24px · 700 | Subsection heading | Subsection. 1.17rem; one line above, half a line below. |
h416px / 24px · 700 | Minor heading | Minor heading, same size as body. 1em. |
h514px / 18px · 700 | Small heading | Raised from the default 13.28px (0.83em) to 14px. |
h614px / 18px · 700 | SMALLEST HEADING | Raised from the default 10.72px (0.67em) to 14px; set apart from h5 by uppercase and letter-spacing. |
Headings (app)
| Style | Sample | Usage |
|---|---|---|
app-h124px / 32px · 700 | Freight performance | Page title inside .pd-app. No margins; the layout gap spaces it. |
app-h218px / 24px · 700 | Exceptions | Panel and section titles inside .pd-app. |
app-h316px / 20px · 700 | Customs hold | Group titles inside a panel. |
app-h415px / 20px · 700 | Sea | Minor labels that are still headings. |
app-h514px / 20px · 700 | Notes | Rare; prefer fewer levels in UI. |
Text
| Style | Sample | Usage |
|---|---|---|
body16px / 1.5 · 400 | Plain text on a plain page, readable by everyone. | All running text, in the system UI font. 16px on a 24px line: the rhythm unit. Paragraphs, lists and tables leave one line below them in content, none in app UI. |
small13.28px / 1.5 · 400 | Fine print and captions | <small>, captions and notes. 0.83em. Never smaller. |
Controls
| Style | Sample | Usage |
|---|---|---|
control16px / 1.25 · 400 | Submit order | Buttons, inputs, selects, textareas. 16px, same family as the page. |
Code
| Style | Sample | Usage |
|---|---|---|
code16px / 1.5 · 400 | font: inherit; | <code>, <kbd>, <samp>, <pre>. 1em of the surrounding text, in the platform monospace. |
Spacing
| Token | Value | Usage |
|---|---|---|
--space-2xsspace.xxs | 2px | Focus-ring offset; legend side padding. |
--space-xsspace.xs | 4px | Vertical padding in buttons, fields and table cells. |
--space-smspace.sm | 8px | Body margin (the browser's own 8px); horizontal padding in buttons and table cells. |
--space-mdspace.md | 16px | Default gap in .pd-stack and between panels; panel padding. |
--space-lgspace.lg | 24px | Minimum target size for any control (WCAG 2.5.8). |
--space-indentspace.indent | 40px | List indent and blockquote side margin, as the browser sets them. |
Radius
| Token | Value | Usage |
|---|---|---|
--radius-noneradius.none | 0 | Everything that is not a control: tables, fieldsets, images, mark. |
--radius-controlradius.control | 2px | Buttons, text fields, selects: the slight rounding browsers draw. |
--radius-roundradius.round | 50% | Radio buttons only. |
Shape
Borders, target size, button edges and the focus ring. Normal UI's values reproduce the browser's look; themes change them.
| Token | Value | Usage |
|---|---|---|
--border-widthshape.borderWidth | 1px | Structural borders: cards, panels, popups, menus, tabs, separators. |
--control-border-widthshape.controlBorderWidth | 1px | Border width of buttons, text fields, selects and textareas. A theme may thicken it; it never goes below 1px. |
--target-minshape.targetMin | 24px | Minimum height and width of any control (WCAG 2.5.8). Rows in menus and lists add to it. |
--button-padding-blockshape.buttonPaddingBlock | 4px | Space above and below a button label. |
--button-padding-inlineshape.buttonPaddingInline | 8px | Space either side of a button label. |
--button-edge-widthshape.buttonEdgeWidth | 0px | Height of the solid edge under a button (button-edge). 0 in Normal UI. |
--button-shadowshape.buttonShadow | 0 0 0 transparent | Any further shadow on a button at rest, e.g. a bevel. Nothing in Normal UI. Must be a shadow list, not none. |
--button-shadow-activeshape.buttonShadowActive | 0 0 0 transparent | The whole button shadow while pressed (:active); replaces the edge and button-shadow. |
--button-press-offsetshape.buttonPressOffset | 0px | How far a button moves down (and right, for a bevel) while pressed. |
--focus-widthshape.focusWidth | 2px | Width of the :focus-visible outline. Never below 2px (WCAG 2.4.11 / 2.4.13). |
--focus-offsetshape.focusOffset | 2px | Gap between an element and its focus outline. |
--choice-sizeshape.choiceSize | 18px | Checkbox and radio size: up from the browser's 13px. |
--sheet-shadowshape.sheetShadow | none | Shadow under a .pd-sheet. None in Normal UI. |
Text treatments
Heading sizes, control text, eyebrows and the link treatment in navigation, as values a theme can change.
| Token | Value | Usage |
|---|---|---|
--control-font-sizetext.controlFontSize | 1rem | Text in buttons, inputs, selects, textareas: grows with the reader's text size. |
--control-line-heighttext.controlLineHeight | 1.25 | Line height inside controls. |
--line-height-bodytext.lineHeightBody | 1.5 | Running text: the 24px rhythm line at 16px. |
--heading-weighttext.headingWeight | 700 | Every heading. |
--heading-1text.heading1 | 2rem | h1 in content. |
--heading-2text.heading2 | 1.5rem | h2 in content. |
--heading-3text.heading3 | 1.17rem | h3 in content. |
--heading-4text.heading4 | 1rem | h4 in content. |
--heading-5text.heading5 | 0.875rem | h5 in content. |
--heading-6text.heading6 | 0.875rem | h6 in content, set apart by uppercase. |
--app-heading-1text.appHeading1 | 1.5rem | h1 inside .pd-app. |
--app-heading-2text.appHeading2 | 1.125rem | h2 inside .pd-app. |
--app-heading-3text.appHeading3 | 1rem | h3 inside .pd-app. |
--app-heading-4text.appHeading4 | 0.9375rem | h4 inside .pd-app. |
--app-heading-5text.appHeading5 | 0.875rem | h5 and h6 inside .pd-app. |
--small-mintext.smallMin | 13px | Floor for <small> and other fine print: 0.83em, never under this, whatever the root size. |
--error-weighttext.errorWeight | 400 | Weight of a field's error message. Normal in Normal UI; the words and the doubled field border carry it. |
--eyebrow-sizetext.eyebrowSize | 0.8125rem | Small label above a group (nav sections, card kickers). |
--eyebrow-weighttext.eyebrowWeight | 700 | Eyebrow weight. |
--eyebrow-transformtext.eyebrowTransform | uppercase | Eyebrow case. |
--eyebrow-trackingtext.eyebrowTracking | 0.04em | Eyebrow letter-spacing. |
--nav-link-decorationtext.navLinkDecoration | underline | Links inside <nav>. Underlined in Normal UI. A theme may set none: a link in a navigation list is identified by where it sits (1.4.1); links in running text always stay underlined. |