Spec version v0.13.0

Force UI Design System — Design Philosophy & Decision Framework

Layer 2 of the Force UI spec. This document defines the design language, decision rules, and token mapping for the Perforce Force UI design system. It is optimized for both human and AI consumption. For token source of truth, see the token JSON files. For component API docs, see Storybook.


1. System Identity

Force UI is the enterprise design system for Perforce products. It targets data-intensive, professional applications where users spend hours per day. The visual language is flat, minimal, and information-dense.

Core values:

  • Clarity — Every visual element communicates something. Remove anything decorative that does not carry meaning.
  • Efficiency — Reduce cognitive distance between user intent and result. Tighter spacing, smaller default text, more information visible without scrolling.
  • Professional density — Default to compact layouts. This is not a marketing site. Users are power users.
  • Flat and structural — Borders define regions. Shadows communicate elevation (floating layers), not importance.

Brand identifiers:

Property Value
Primary brand color Indigo #5405ff (--force-color-bg-primary)
Secondary brand color Cyan #00cfff (--force-color-bg-secondary)
Primary font Noto Sans (fallback: -apple-system, Segoe UI, Roboto, sans-serif)
Mono font Noto Sans Mono (fallback: SF Mono, Monaco, Cascadia Code, Consolas)
Default body text size sm = 14px — NOT 16px
Default theme Light. Dark mode overrides color and shadow tokens only.

2. Design Principles

These are decision-making rules. When two implementations are both technically valid, apply these in order.

P1 — Explicit over computed. States (hover, active, focus, disabled) have dedicated tokens. Do not compute states with calc(), filter: brightness(), or OKLCH manipulation. Use --force-color-bg-primary-hover instead of darkening --force-color-bg-primary at runtime.

P2 — Density over whitespace. Default spacing is tight by design. Use --force-spacing-6 (24px) for card padding, --force-spacing-4 (16px) for grid gaps. Do not add whitespace to "breathe" — only add it when it resolves an actual cognitive problem.

P3 — Borders define structure; shadows signal elevation. Cards, panels, inputs, and table cells use border: 1px solid var(--force-color-border-default). Shadows are exclusively for floating/detached elements: modals, dropdowns, tooltips, popovers, toasts. Never apply a shadow where a border is the correct structural signal.

P4 — Semantic colors communicate status, not decoration. Success (green), warning (orange), error/cranberry, and info (blue) exist to communicate system state. Do not use them to make UI visually interesting. The primary brand indigo is used sparingly — for interactive elements and emphasis only, not for painting large surfaces.

P5 — Three-level background hierarchy is fixed. surface (white) > muted (near-white with subtle blue cast) > emphasis (light grey for chrome). Do not introduce ad hoc background values. Do not reverse the hierarchy.

P6 — Typography hierarchy through size and weight only. Size and weight are the primary signals. Color is secondary reinforcement. Never rely on color alone to establish visual hierarchy in type.

P7 — Token-only styling. Every spacing value, color, radius, shadow, z-index, and duration in the UI must reference a --force-* custom property. No raw hex values. No arbitrary pixel values for spacing. No magic numbers for z-index.

P8 — Explicitly style every form element. Native form elements (<input>, <select>, <textarea>, <button>) inherit browser and host-page default styles that vary by OS, browser, and color scheme. Never rely on these defaults. Every form element MUST have explicit background, color, and border declarations using Force UI tokens. Without these, form controls will fall back to browser dark-mode defaults when the host page uses prefers-color-scheme: dark or a dark stylesheet, producing dark inputs on a light Force UI surface. Set appearance: none (with vendor prefixes as needed) on <input> and <select> elements to remove browser-native chrome, then apply all visual styling through tokens.


2a. Interpretation Modes

The spec supports two interpretation modes for AI agents consuming it via the MCP server. The mode is chosen by the agent based on the user's prompt and passed to MCP tools via the mode parameter.

Strict mode (default)

Treat the spec as a specification. Patterns in patterns/ are prescriptive — follow anatomy, variants, states, and guidance as written. Tokens are required. Principles and accessibility requirements are binding. Deviate only when the spec is silent.

Use when the user is implementing a real component, page, or feature. Keywords: "build", "implement", "add", "fix", "ship", "wire up".

Creative mode

Treat the spec as a foundation for exploration. The goal is unique, well-considered ideas — not replication of existing components.

Required (non-negotiable) in both modes:

  • Tokens: all colors, spacing, radius, typography, motion, and shadow values must come from tokens.
  • Accessibility: focus rings, contrast, keyboard support, ARIA requirements remain binding.
  • Principles: P1–P8 above still apply.

Optional (advisory) in creative mode:

  • Component anatomy, variants, and states in patterns/ — deviate when a different structure serves the idea better.
  • Composition and layout guidance — try unconventional arrangements, novel visual hierarchy.

When deviating from a pattern in creative mode, briefly note what changed and why.

Use when the user is exploring. Keywords: "ideate", "explore", "brainstorm", "try", "what if", "unique", "novel", "prototype an idea", "mock up options".

Summary

Aspect Strict Creative
Tokens Required Required
Principles (P1–P8) Binding Binding
Accessibility Binding Binding
Pattern anatomy Prescriptive Advisory
Pattern variants/states Prescriptive Advisory
Composition/layout Prescriptive Advisory

3. Color System

Architecture

Tokens follow a strict two-tier model:

primitives.tokens.json  →  semantic-light.tokens.json / semantic-dark.tokens.json
(hidden, never shipped)     (shipped as --force-* CSS custom properties)

Primitives (color.neutral.200, color.indigo.500, etc.) are aliasing sources only. They never appear in CSS. Semantic tokens are the only public API.

Background Hierarchy

Role Token CSS Property Value
Primary content surface color.bg.surface --force-color-bg-surface #ffffff
Secondary / table headers / disabled inputs color.bg.muted --force-color-bg-muted #f8f8fd
Navigation chrome (topbar, sidebar) color.bg.emphasis --force-color-bg-emphasis #f3f3f8
Inverted surfaces (tooltips) color.bg.inverse --force-color-bg-inverse #111115
Modal scrim color.bg.overlay --force-color-bg-overlay rgba(0,0,0,0.5)

Note: bg.muted (#f8f8fd) has a subtle cool/blue cast. This is intentional — it is the Perforce neutral palette signature.

Interactive State Backgrounds

State Token Notes
Hover color.bg.interactive-hover --force-color-bg-interactive-hover
Selected / Active color.bg.interactive-active --force-color-bg-interactive-active — indigo-50
Primary CTA default color.bg.primary --force-color-bg-primary — indigo-500
Primary CTA hover color.bg.primary-hover --force-color-bg-primary-hover — indigo-600
Primary CTA active color.bg.primary-active --force-color-bg-primary-active — indigo-700
Subtle primary tint color.bg.primary-subtle --force-color-bg-primary-subtle — indigo-50
Destructive CTA default color.bg.destructive --force-color-bg-destructive — cranberry-500
Destructive CTA hover color.bg.destructive-hover --force-color-bg-destructive-hover
Destructive CTA active color.bg.destructive-active --force-color-bg-destructive-active

Semantic Status Backgrounds (alerts, badges, callouts)

Variant Background token Text token Border/accent token
Success color.bg.success-subtle color.text.success color.border.success
Warning color.bg.warning-subtle color.text.warning color.border.warning
Error color.bg.error-subtle color.text.error color.border.error
Info color.bg.info-subtle color.text.info color.border.info

For solid badges/CTAs in semantic colors: the system currently only ships color.bg.destructive as a solid semantic background. Solid success/warning/info solid backgrounds (bg.success, bg.warning, bg.info) are pending — see context/issues.md issue #2.

Text Colors

Role Token CSS Property
Default body text color.text.primary --force-color-text-primary
Labels, nav text, descriptions color.text.secondary --force-color-text-secondary
Captions, helper text, timestamps color.text.tertiary --force-color-text-tertiary
Placeholder, disabled color.text.disabled --force-color-text-disabled
Text on solid primary/destructive bg color.text.on-primary --force-color-text-on-primary (white)
Text on solid secondary (cyan) bg color.text.on-secondary --force-color-text-on-secondary (black — cyan is too light for white text)
Text on inverted bg color.text.on-inverse --force-color-text-on-inverse (white)
Link / ghost button text color.text.link --force-color-text-link
Active nav item text color.text.interactive-active --force-color-text-interactive-active
Brand emphasis text (on neutral/surface bg only) color.text.primary-brand --force-color-text-primary-brand

Border Colors

Role Token CSS Property Value
Default (cards, inputs, separators) color.border.default --force-color-border-default neutral-200
Subtle (cards on muted bg) color.border.muted --force-color-border-muted neutral-100
Emphasized (hover states) color.border.strong --force-color-border-strong neutral-300
Focus ring border color.border.focus --force-color-border-focus indigo-500
Error state border color.border.error --force-color-border-error cranberry-500
Success accent (left-border alerts) color.border.success --force-color-border-success green-500
Warning accent color.border.warning --force-color-border-warning orange-500
Info accent color.border.info --force-color-border-info blue-500
Primary brand border color.border.primary --force-color-border-primary indigo-500
Destructive border color.border.destructive --force-color-border-destructive cranberry-500

Icon Colors

Use color: inherit by default so icons match their parent text. Override explicitly only when needed.

Role Token CSS Property
Non-interactive icon color.icon.default --force-color-icon-default
Clickable standalone icon color.icon.interactive --force-color-icon-interactive
Success icon color.icon.success --force-color-icon-success
Warning icon color.icon.warning --force-color-icon-warning
Error icon color.icon.error --force-color-icon-error
Info icon color.icon.info --force-color-icon-info

Chart Colors (sequential by series)

Use in order: --force-color-chart-1 (indigo) → --force-color-chart-2 (cyan) → --force-color-chart-3 (green) → --force-color-chart-4 (orange) → --force-color-chart-5 (cranberry) → --force-color-chart-6 (blue).

Dark Mode

Dark theme overrides only color.* and shadow.* tokens. Typography, spacing, radius, motion, z-index, layout, and breakpoint tokens are theme-invariant. Do not add any non-color, non-shadow token to semantic-dark.tokens.json.

Background–Foreground Pairing Rules

Always pair backgrounds with their matching foreground. Never mix tiers:

  • color.bg.surface + color.text.primary or color.text.secondary
  • color.bg.primary (indigo-500) + color.text.on-primary (white)
  • color.bg.secondary (cyan-500) + color.text.on-secondary (black — cyan is too light for white text)
  • color.bg.primary-subtle (indigo-50) + color.text.primary-brand (indigo-500)
  • color.bg.interactive-active (indigo-50) + color.text.interactive-active (indigo-700)
  • color.bg.success-subtle + color.text.success (green-700)
  • color.bg.warning-subtle + color.text.warning (orange-700)
  • color.bg.error-subtle + color.text.error (cranberry-700)
  • color.bg.info-subtle + color.text.info (blue-700)
  • color.bg.inverse + color.text.on-inverse (white)

Zero-contrast traps — DO NOT use these pairings:

The following tokens share the same underlying value as color.bg.primary (indigo-500 in light, indigo-300 in dark) and produce zero contrast when used as foreground on a solid primary background:

  • color.text.primary-brand on color.bg.primaryzero contrast. Use color.text.on-primary instead.
  • color.text.link on color.bg.primaryzero contrast. Use color.text.on-primary instead.
  • color.border.primary on color.bg.primaryzero contrast. Border is invisible.
  • color.icon.interactive on color.bg.primaryzero contrast. Use icon color: inherit from color.text.on-primary.

The name color.text.primary-brand is a common source of errors — it sounds like "the primary brand text color" and is the intuitive first choice for text on brand-colored elements. It is intended for brand-colored text on neutral or surface backgrounds (e.g., outline button text, inline accents), not for text on solid primary backgrounds.


4. Typography System

Font Families

  • UI text: --force-font-family-default — Noto Sans (with system fallbacks)
  • Code / technical data: --force-font-family-code — Noto Sans Mono (with system fallbacks)

Do not override font-family per-component.

Type Scale

Token CSS Property Size Line Height Primary Usage
font.size.xs --force-font-size-xs 12px tight (1.25) Captions, labels, badges, helper text. Minimum readable size.
font.size.sm --force-font-size-sm 14px tight (1.25) Default body text. Most UI text. Do NOT use base as the default.
font.size.base --force-font-size-base 16px normal (1.5) Large body text, page descriptions, marketing copy.
font.size.lg --force-font-size-lg 18px snug (1.375) Subsection headers.
font.size.xl --force-font-size-xl 20px relaxed (1.75) Card titles, H2 headings.
font.size.2xl --force-font-size-2xl 24px relaxed (1.75) Section headers.
font.size.3xl --force-font-size-3xl 30px relaxed (1.75) Page titles (PageHeader component).
font.size.4xl --force-font-size-4xl 36px relaxed (1.75) Dashboard display headings. Use sparingly.

Font Weights

Token CSS Property Value Usage
font.weight.regular --force-font-weight-regular 400 Body text, descriptions
font.weight.medium --force-font-weight-medium 500 Buttons, nav items, labels, badges, table headers
font.weight.semibold --force-font-weight-semibold 600 Card titles, section headings
font.weight.bold --force-font-weight-bold 700 Page titles, strong emphasis

Line Height Tokens

Token CSS Property Value Usage
font.lineHeight.tight --force-font-line-height-tight 1.25 Headings, single-line labels
font.lineHeight.snug --force-font-line-height-snug 1.375 Subheadings, medium text
font.lineHeight.normal --force-font-line-height-normal 1.5 Default body text
font.lineHeight.relaxed --force-font-line-height-relaxed 1.75 Long-form content, descriptions

Letter Spacing Tokens

Token CSS Property Value Usage
font.letterSpacing.tight --force-font-letter-spacing-tight -0.01em Large headings (3xl+)
font.letterSpacing.normal --force-font-letter-spacing-normal 0em Default body text
font.letterSpacing.wide --force-font-letter-spacing-wide 0.025em Uppercase labels, badge text

Note: Callout card labels use 0.05em — this is a wider value not yet tokenized (see context/issues.md issue #9).

Heading Conventions

Heading Size Weight Letter Spacing
Page title (PageHeader) font.size.3xl (30px) bold tight
Section header font.size.2xl (24px) semibold normal
Card title / H2 font.size.xl (20px) semibold normal
Subsection header font.size.lg (18px) semibold normal
Default body font.size.sm (14px) regular normal
Table header font.size.xs (12px) medium wide (uppercase)
Caption / label font.size.xs (12px) medium wide (uppercase)

5. Spacing & Layout

Spacing Scale

Base unit: 4px. Every padding, margin, and gap value must use a spacing token.

Token CSS Property Value Common Usage
spacing.1 --force-spacing-1 4px Icon-to-label gap, tightest inline spacing
spacing.2 --force-spacing-2 8px Component internal gaps, badge inline padding
spacing.3 --force-spacing-3 12px Input horizontal padding, small component padding
spacing.4 --force-spacing-4 16px Card padding (compact), grid gaps, control bar gaps
spacing.5 --force-spacing-5 20px Medium internal padding
spacing.6 --force-spacing-6 24px Card padding (default), section gaps, layout content padding
spacing.8 --force-spacing-8 32px Section margins
spacing.10 --force-spacing-10 40px Large section separation
spacing.12 --force-spacing-12 48px Major layout breaks
spacing.16 --force-spacing-16 64px Page-level vertical spacing
spacing.20 --force-spacing-20 80px Hero / feature section spacing

Canonical Spacing Assignments

Pattern Token
Card internal padding --force-spacing-6 (24px)
Grid gap between cards --force-spacing-4 (16px)
Section gap within a tab --force-spacing-6 (24px)
Layout content padding --force-spacing-6 (24px)
Form field vertical gap --force-spacing-4 (16px)
Button padding (sm) --force-spacing-3 (12px)
Button padding (md) --force-spacing-4 (16px)
Button padding (lg) --force-spacing-5 (20px)
Inline icon-to-text gap --force-spacing-1 (4px) to --force-spacing-2 (8px)

Layout Selection

Every Perforce product uses a sidebar — it is the baseline navigation structure. The topbar is the variable. The decision is whether the product needs a persistent horizontal chrome strip above the sidebar, not whether it needs a sidebar at all.

Decision: Does this product need a topbar?

Add a topbar (use the Page Shell pattern) when any of these are true:

  • The product has multiple product areas exposed as top-level navigation (e.g., Streams, Repos, CI/CD as distinct sections in the topbar, with the sidebar navigating within the selected area).
  • The product participates in the app switcher — the multi-app grid icon that lets users move between Perforce products (analogous to the Google app grid). The app switcher lives in the topbar to the left of the product logo.
  • The product needs global search that spans across product areas, surfaced as a persistent trigger in the topbar.
  • The product requires a persistent brand identity strip — the product logo/wordmark in the top-right corner alongside global utility icons (user avatar, settings, notifications).

Use sidebar-only (use the Sidebar-Only Shell pattern) when:

  • The product has a single navigation tier — all routes fit comfortably in the sidebar without needing top-level product area switching.
  • The product is a focused tool, admin panel, documentation site, or internal utility that does not participate in the app switcher or cross-product navigation.
  • Global utilities (user avatar, settings) can be placed in the sidebar footer without crowding the navigation.

When in doubt, start with sidebar-only. Adding a topbar later is straightforward — the sidebar structure remains unchanged and the topbar layers on top. Removing a topbar is harder because global utilities and product area navigation must be relocated into the sidebar, which may require restructuring the information architecture.

There is no topbar-only variant. Every Perforce product has a sidebar. If the product has so few routes that a sidebar feels excessive, use the sidebar-only shell with a flat list of nav items — do not invent a horizontal-nav-only layout.

Signal Shell Why
Multiple product areas Page Shell Topbar provides the first tier of navigation; sidebar provides the second
App switcher needed Page Shell App switcher pattern lives in the topbar
Global search across areas Page Shell Search trigger belongs in the persistent topbar strip
Logo + utility icons need persistent strip Page Shell Topbar carries brand identity and global actions
Single nav tier, few routes Sidebar-Only Shell Keep it simple — one navigation surface
Admin panel or internal tool Sidebar-Only Shell No cross-product concerns
Documentation site Sidebar-Only Shell Content-focused, no global chrome needed

Application Shell Structure

+-------------------------------------------------------------+
|  TopBar  (height: --force-layout-header-height = 64px)      |
|  background: --force-color-bg-emphasis                      |
|  position: fixed; z-index: --force-z-index-fixed (300)      |
+----------+--------------------------------------------------+
|  Sidebar |  ╭──────────────────────────────────────────╮    |
|  256px   |  │ Content Panel                            │    |
|  bg-     |  │ background: --force-color-bg-surface     │    |
|  emphasis|  │ border-radius: --force-radius-xl (12px)  │    |
|          |  │ padding: --force-layout-content-padding   │    |
|          |  │ scrollable; fills remaining viewport      │    |
|          |  ╰──────────────────────────────────────────╯    |
|          |  ↑ grey chrome (bg-emphasis) continues behind    |
+----------+--------------------------------------------------+

The content area is an inset rounded panel, not a flat edge-to-edge region. The grey chrome background (--force-color-bg-emphasis) is continuous behind the content panel — it extends from the topbar and sidebar to fill the entire viewport. The white content panel sits on top with border-radius: var(--force-radius-xl) (12px), creating a visible rounded frame effect. This is a key visual signature of Perforce products. The panel has a small margin/gap between it and the sidebar/topbar edges so the grey background peeks through, reinforcing the inset appearance.

Layout Dimension Tokens

Token CSS Property Value
layout.header-height --force-layout-header-height 64px (4rem)
layout.sidebar-width --force-layout-sidebar-width 256px (16rem)
layout.sidebar-collapsed --force-layout-sidebar-collapsed 64px (4rem)
layout.content-max-width --force-layout-content-max-width 1400px (87.5rem) — opt-in reading-measure cap for forms/prose only; not applied to the content panel
layout.content-padding --force-layout-content-padding 24px (1.5rem)

Responsive Breakpoints

Token CSS Property Value
breakpoint.sm --force-breakpoint-sm 576px
breakpoint.md --force-breakpoint-md 768px
breakpoint.lg --force-breakpoint-lg 992px
breakpoint.xl --force-breakpoint-xl 1200px
breakpoint.2xl --force-breakpoint-2xl 1400px

Border Radius

Token CSS Property Value Usage
radius.sm --force-radius-sm 4px Subtle badges, small tags
radius.md / radius.button / radius.input --force-radius-md 6px Buttons, inputs (default)
radius.lg / radius.card --force-radius-lg 8px Cards, containers, dropdowns
radius.xl / radius.modal --force-radius-xl 12px Modals, large panels
radius.full / radius.badge --force-radius-full 9999px Pill badges, avatars, round buttons

Use the semantic aliases (radius.button, radius.card, radius.modal, radius.badge, radius.input) when available — they make intent explicit and insulate against future scale changes.

Page Layout Patterns

List page (data grid primary content):

  1. PageHeader — title (font.size.3xl, bold) + description (font.size.base, color.text.secondary) + action buttons
  2. ControlsBar — search + filters, gap --force-spacing-4
  3. DataGrid — bordered container, fills remaining height

Detail page (single entity):

  1. Breadcrumbs — links in --force-color-text-link, chevron separator in --force-color-text-tertiary
  2. DetailPageHeader — title + metadata badges + action dropdown
  3. TabList — underline variant; active tab has 3px bottom border in --force-color-border-primary
  4. Tab content — no extra padding; layout handles edge spacing

Overview tab (within detail pages):

.overview     { display: flex; flex-direction: column; gap: var(--force-spacing-6); }
.calloutCards { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--force-spacing-4); }
.contentGrid  { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--force-spacing-4); align-items: start; }

6. Elevation & Shadows

Shadows communicate floating layer elevation only. They are not used on cards, panels, tables, or any content container that is part of the page flow.

Shadow Scale

Token CSS Property Usage
shadow.xs --force-shadow-xs Dropdown menus
shadow.sm --force-shadow-sm Popovers, select dropdowns
shadow.md --force-shadow-md Floating panels, persistent popovers
shadow.lg --force-shadow-lg Modals, drawers
shadow.xl --force-shadow-xl Wizard panels, major overlays

Focus Rings

Token CSS Property Usage
shadow.focus-ring --force-shadow-focus-ring Default keyboard focus (3px indigo-100 spread)
shadow.focus-ring-error --force-shadow-focus-ring-error Focused input with validation error (3px cranberry-100 spread)

Focus ring always pairs with a border color change:

  • Default focus: border-color: var(--force-color-border-focus) + box-shadow: var(--force-shadow-focus-ring)
  • Error focus: border-color: var(--force-color-border-error) + box-shadow: var(--force-shadow-focus-ring-error)

What Gets Shadows vs. Borders

Element Elevation signal
Card border: 1px solid var(--force-color-border-default)
Table border: 1px solid var(--force-color-border-default)
Input border: 1px solid var(--force-color-border-default)
Dropdown menu --force-shadow-xs + border: 1px solid var(--force-color-border-default)
Popover --force-shadow-sm
Floating panel --force-shadow-md
Modal --force-shadow-lg
Wizard panel --force-shadow-xl
Toast --force-shadow-lg
Tooltip --force-shadow-sm

7. Motion

Use the shortest duration that still feels responsive. Avoid animation entirely for functional state changes (validation, loading indicators).

Duration Tokens

Token CSS Property Value Usage
duration.fast --force-duration-fast 150ms Hover state changes, color transitions, toggle switches
duration.normal --force-duration-normal 250ms Panel open/close, fade in/out, dropdown appear
duration.slow --force-duration-slow 350ms Page transitions, complex reveals
duration.entrance --force-duration-entrance 500ms First-paint entrance animations. Use sparingly.

Easing Tokens

Token CSS Property Curve Usage
easing.standard --force-easing-standard cubic-bezier(0.4, 0, 0.2, 1) Default for most transitions
easing.decelerate --force-easing-decelerate cubic-bezier(0, 0, 0.2, 1) Enter/appear animations — fast start, soft landing
easing.accelerate --force-easing-accelerate cubic-bezier(0.4, 0, 1, 1) Exit/leave animations — gentle start, fast exit

When to Use Which

Interaction Duration Easing
Button hover color change duration.fast easing.standard
Dropdown open duration.normal easing.decelerate
Dropdown close duration.normal easing.accelerate
Modal open duration.normal easing.decelerate
Modal close duration.fast easing.accelerate
Toast enter duration.normal easing.decelerate
Page transition duration.slow easing.standard
Toggle/switch duration.fast easing.standard

Note: Composed transition shorthand tokens (e.g., transition-colors, transition-shadow) are not yet in the token system — see context/issues.md issue #5. Define these at the component level using var(--force-duration-fast) and var(--force-easing-standard) directly.


8. Interaction Model

States are explicit tokens, never computed. Every interactive element implements this exact state set.

Standard State Progression

default → hover → active/pressed → [selected] → focus → disabled

State Token Assignments

State Background Text Border
Default transparent or color.bg.surface color.text.secondary color.border.default
Hover color.bg.interactive-hover color.text.primary color.border.default or color.border.strong
Selected / Active color.bg.interactive-active color.text.interactive-active color.border.primary
Focus inherit background inherit text color.border.focus + shadow.focus-ring
Disabled color.bg.muted color.text.disabled color.border.muted

Button Variants

Variant Default bg Default text Hover bg Notes
primary color.bg.primary (indigo-500) color.text.on-primary color.bg.primary-hover Main CTA
secondary color.bg.surface color.text.secondary color.bg.interactive-hover Bordered
tertiary transparent color.text.secondary color.bg.interactive-hover No border
ghost transparent color.text.link color.bg.primary-subtle Brand color text
outline transparent color.text.primary-brand color.bg.primary-subtle Brand border
danger color.bg.destructive color.text.on-primary color.bg.destructive-hover Destructive action
outline-danger transparent color.text.error color.bg.error-subtle Lighter destructive
text transparent color.text.link underline Inline action

Navigation State Conventions

  • Sidebar active item: color.bg.interactive-active (indigo-50) + color.text.interactive-active (indigo-700) + 4px left border in color.border.primary (indigo-500)
  • Tab active (underline variant): 3px bottom border in color.border.primary
  • Tab active (pills variant): color.bg.interactive-active background
  • Breadcrumb links: color.text.link color, underline on hover. Current page: color.text.primary, bold, not clickable.

Opacity Tokens

Token CSS Property Value Usage
opacity.disabled --force-opacity-disabled 0.5 Apply to the entire disabled element
opacity.subtle --force-opacity-subtle 0.7 De-emphasis without full disabled treatment
opacity.overlay --force-opacity-overlay 0.5 Scrim/overlay elements

Apply opacity.disabled to the element container, not just the text child.

Z-Index Stack

Token CSS Property Value Usage
zIndex.dropdown --force-z-index-dropdown 100 Dropdown menus, select lists
zIndex.sticky --force-z-index-sticky 200 Sticky table headers
zIndex.fixed --force-z-index-fixed 300 Topbar
zIndex.modal-backdrop --force-z-index-modal-backdrop 400 Modal scrim
zIndex.modal --force-z-index-modal 500 Modal content
zIndex.popover --force-z-index-popover 600 Popovers above modals
zIndex.tooltip --force-z-index-tooltip 700 Tooltips
zIndex.toast --force-z-index-toast 800 Toast notifications

9. Icons

Set: Material Symbols Rounded. Do not mix icon sets.

Icon Sizes

Name Size Usage
xs 14px Inline with small text (font.size.xs)
sm 16px Buttons (sm), table cells, inline with body text
md 20px Default — nav items, buttons (md), form field icons
lg 24px Headers, standalone icons, card titles
xl 32px Feature icons, empty states, onboarding

Color Rules

  • Icons inherit text color by default via color: inherit. Do not set icon color independently unless the icon is a standalone interactive element or a status indicator.
  • Standalone interactive icons (buttons without text labels): use --force-color-icon-interactive.
  • Status icons: use --force-color-icon-success, --force-color-icon-warning, --force-color-icon-error, --force-color-icon-info.
  • Non-interactive decorative icons: use --force-color-icon-default.

SVG Color Inheritance

Material Symbols Rounded ships SVGs with no fill attribute on <path> elements. Per the SVG spec, the default fill is black — meaning CSS color: inherit and --force-color-icon-* tokens have no effect unless the SVG is configured to use currentColor.

When using SVG-to-React tooling (e.g., vite-plugin-svgr), you must inject fill="currentColor" onto the root <svg> element via svgProps. The commonly-suggested replaceAttrValues option alone is insufficient — it only replaces existing color attributes, and Material Symbols SVGs have none to replace.

Required SVGR configuration (vite-plugin-svgr):

svgr({
  svgrOptions: {
    svgProps: { fill: "currentColor" },          // injects fill on <svg> root
    replaceAttrValues: {                          // catches any explicit fills
      "#000": "currentColor",
      black: "currentColor",
    },
    svgoConfig: {
      plugins: [
        { name: "convertColors", params: { currentColor: true } },
      ],
    },
  },
}),
  • svgProps: { fill: "currentColor" }required. Adds the fill attribute Material Symbols omits.
  • replaceAttrValues — safety net for third-party SVGs that do include explicit color values.
  • svgoConfig.convertColors — normalizes any remaining hardcoded colors during SVGO optimization.

For other SVG pipelines (webpack @svgr/webpack, standalone @svgr/cli, etc.), apply the same svgProps and replaceAttrValues options in the equivalent configuration format.

Guidance

  • DO pair icon size with text size: xs icon with xs text, sm icon with sm text, md icon with mdlg text.
  • DO provide aria-label on icon-only buttons. Icons without text labels are invisible to screen readers.
  • DON'T use icons as the sole indicator of meaning — always pair with text or an aria-label.
  • DON'T mix filled and outlined styles within the same interface — Force UI uses Rounded exclusively.

10. Accessibility Standards

Baseline: WCAG 2.1 Level AA.

Color Contrast

  • Normal text (< 18px or < 14px bold): minimum 4.5:1 ratio
  • Large text (>= 18px or >= 14px bold): minimum 3:1 ratio
  • UI components and focus indicators: minimum 3:1 ratio against adjacent colors

Verified pairs:

Pair Ratio
color.text.primary (#000) on color.bg.surface (#fff) 21:1
color.text.secondary (neutral-800 #26262e) on white ~14:1
color.text.link (indigo-500 #5405ff) on white ~5.6:1
color.text.error (cranberry-700 #a30e1c) on color.bg.error-subtle passes AA
color.text.success (green-700 #0f660f) on color.bg.success-subtle passes AA

Do not use color.text.disabled (neutral-400) for readable content — it does not meet AA contrast on white.

Focus Management

  • Every interactive element must have a visible focus indicator using --force-shadow-focus-ring and --force-color-border-focus.
  • Focus rings must never be suppressed with outline: none without providing the custom ring replacement.
  • Modal open: move focus to the first foceable element inside the modal.
  • Modal close: return focus to the trigger element.
  • Escape key always closes: modals, dropdowns, popovers, and tooltips.
  • When modal is open: lock body scroll.

Keyboard Navigation

  • All interactive elements reachable by Tab / Shift+Tab.
  • Menus and listboxes: Arrow Up/Down to navigate items, Enter/Space to select, Escape to close.
  • Tab navigation (underline/pills): Arrow Left/Right to switch tabs.
  • Data tables: Tab moves between interactive cells; row selection via Space.

Minimum Target Sizes

  • Buttons: minimum height 32px (sm), prefer 40px (md) for primary actions.
  • Interactive icons without text labels: minimum 32px × 32px hit area.
  • Form inputs: minimum height 32px (sm), prefer 40px (md).

Color Is Never the Only Signal

  • Status (error, success, warning, info) must use both color AND an icon or text label.
  • Disabled state must use both reduced opacity AND non-interactive cursor, not color alone.
  • Selected/active state must use both background color AND a border or typographic treatment.

11. Composition Rules

Card Pattern

background: var(--force-color-bg-surface);
border: 1px solid var(--force-color-border-default);
border-radius: var(--force-radius-card);   /* 8px */
padding: var(--force-spacing-6);           /* 24px */

Card on a bg.muted surface: use border-color: var(--force-color-border-muted) (one step lighter).

Card header (when present):

padding-bottom: var(--force-spacing-4);
border-bottom: 1px solid var(--force-color-border-default);
margin-bottom: var(--force-spacing-4);

Form Input Pattern

height: 40px;                              /* md size */
padding: 0 var(--force-spacing-3);        /* 0 12px */
border: 1px solid var(--force-color-border-default);
border-radius: var(--force-radius-input); /* 6px */
background: var(--force-color-bg-surface);
font-size: var(--force-font-size-sm);     /* 14px */

Alert / Callout Pattern

Left-border accent style:

padding: var(--force-spacing-4);
border-radius: var(--force-radius-lg);
border-left: 4px solid var(--force-color-border-{variant});
background: var(--force-color-bg-{variant}-subtle);
color: var(--force-color-text-{variant});

Modal Pattern

/* Backdrop */
background: var(--force-color-bg-overlay);
z-index: var(--force-z-index-modal-backdrop);

/* Panel */
background: var(--force-color-bg-surface);
border-radius: var(--force-radius-modal);  /* 12px */
box-shadow: var(--force-shadow-lg);
z-index: var(--force-z-index-modal);

Modal widths (not tokenized, use as documented constants): small 400px, medium 600px, large 900px, xl 1200px.

Dropdown / Popover Pattern

background: var(--force-color-bg-surface);
border: 1px solid var(--force-color-border-default);
border-radius: var(--force-radius-lg);     /* 8px */
box-shadow: var(--force-shadow-xs);        /* dropdown */ /* or shadow-sm for popover */
z-index: var(--force-z-index-dropdown);

Badge Pattern

Subtle (default): color.bg.{variant}-subtle background + color.text.{variant} text + radius.badge (pill) Outline: transparent bg + color.text.primary-brand or variant text + 1px color.border.{variant} border + radius.badge Strong: solid color.bg.{variant} + color.text.on-primary (white) + radius.badge. Exception: color.bg.secondary (cyan) pairs with color.text.on-secondary (black) — white fails contrast on cyan.

Badge text: font.size.xs (12px), font.weight.medium (500), uppercase, font.letterSpacing.wide.

Data Table Pattern

Container:

border: 1px solid var(--force-color-border-default);
border-radius: var(--force-radius-lg);
overflow: hidden;

Header row: color.bg.muted background, font.size.xs, font.weight.medium, uppercase, color.text.tertiary.

Body row: font.size.sm, color.text.primary, border-bottom: 1px solid var(--force-color-border-muted).

Hover row: color.bg.interactive-hover.

Selected row: color.bg.interactive-active.

Actions column: always rightmost, pinned, 100px, not sortable/filterable.


12. Do / Don't

Color

  • DO use --force-color-* tokens for every color value.
  • DO pair backgrounds with their designated foreground tokens (see Section 3, Background–Foreground Pairing Rules).
  • DO use semantic status colors (success, warning, error, info) only to communicate system state.
  • DON'T use raw hex values anywhere in component code.
  • DON'T reference primitive tokens (e.g., color.neutral.200, color.indigo.500) in component code — use semantic tokens only.
  • DON'T use semantic status colors (green, orange, cranberry, blue) for decorative or organizational purposes.
  • DON'T use color.text.disabled for readable content — it fails contrast requirements.
  • DON'T use color alone to communicate state (error, selected, disabled). Always add a secondary signal.
  • DON'T apply --force-color-bg-emphasis to content areas — it is for navigation chrome only.

Shadows & Borders

  • DO use border: 1px solid var(--force-color-border-default) to define card and container structure.
  • DO use shadow tokens only on floating/detached elements (modals, dropdowns, tooltips, toasts, popovers).
  • DON'T add a shadow to a card, panel, table, or any element that is part of the page flow.
  • DON'T use box-shadow for visual decoration or to suggest importance.
  • DON'T stack a border and a shadow on the same element unless it is a dropdown (which uses both).

Spacing

  • DO use --force-spacing-* tokens for all padding, margin, and gap values.
  • DO use --force-spacing-6 (24px) for card internal padding.
  • DO use --force-spacing-4 (16px) for grid gaps between cards and between form fields.
  • DON'T use arbitrary pixel values (e.g., padding: 13px or margin: 7px).
  • DON'T use values that fall between steps on the 4px grid.

Typography

  • DO use --force-font-size-sm (14px) as the default body text size.
  • DO use --force-font-weight-medium (500) for button labels, nav items, badges, and table headers.
  • DON'T use --force-font-size-base (16px) as the default body size — it is for descriptions and large text only.
  • DON'T apply font-family overrides per-component — use --force-font-family-default system-wide.
  • DON'T use font size alone to establish visual hierarchy — always pair size with a weight change.

Z-Index

  • DO use --force-z-index-* tokens for all stacking contexts.
  • DON'T use arbitrary z-index values (e.g., z-index: 9999, z-index: 50).
  • DON'T create new stacking layers outside the defined 8-layer scale without a documented reason.

Motion

  • DO use --force-duration-fast (150ms) for hover and color changes.
  • DO use --force-duration-normal (250ms) for panel and dropdown transitions.
  • DON'T animate functional state changes (form validation errors, loading indicators).
  • DON'T use duration.entrance (500ms) for routine interactions — it is for first-paint only.

Interaction & States

  • DO implement all five states for every interactive element: default, hover, active/selected, focus, disabled.
  • DO use explicit state tokens — never compute hover/active colors with filter, brightness(), or OKLCH calc().
  • DO apply opacity.disabled (0.5) to the entire disabled element container, not just the text.
  • DON'T suppress focus outlines without providing the --force-shadow-focus-ring replacement.
  • DON'T use pointer-events: none alone to disable an element — also set aria-disabled and apply disabled visual tokens.

Layout

  • DON'T add padding to tab content containers — the shell layout handles edge spacing.
  • DO let the main content panel fill the available width between the sidebar and the viewport edge. Do NOT cap the content panel with a fixed max-width.
  • DO apply an inner-column reading-measure cap (--force-layout-content-max-width, 1400px, or a tighter value where appropriate) to article-like content: long-form prose, documentation/markdown pages, release notes, settings forms. Reading comfort drops sharply as line length grows past ~75 characters, so narrow the column for these surfaces. When the capped column is the sole content of the page, center it horizontally within the panel so empty space balances on both sides.
  • DO let data-dense content fill the full panel width: dashboards, data tables, list views, detail views, wizards, multi-column forms, and any layout composed of components that can consume horizontal space. The cap is not appropriate here — it creates wasted chrome on wide monitors and cramps the components that were designed to use the room.
  • DON'T apply the reading-measure cap to the content panel itself. It is only ever applied to an inner column whose content type is article-like.
  • DON'T remove or flatten the content panel's rounded corners (--force-radius-xl). The inset rounded panel on the grey chrome is a key Perforce visual signature.
  • DON'T put a border on the sidebar's right edge — the inset content panel creates the visual separation.
  • DON'T set custom sidebar or topbar dimensions — use the layout tokens.

Accessibility

  • DON'T rely on color alone to convey meaning, status, or state.
  • DON'T set outline: none without replacing with the Force focus ring (--force-shadow-focus-ring).
  • DON'T use text smaller than --force-font-size-xs (12px).
  • DON'T lock keyboard focus inside a component unless it is a modal or dialog with proper ARIA role.

Token System

  • DON'T modify primitive tokens to fix a semantic issue — add or adjust semantic tokens instead.
  • DON'T add color or shadow tokens to semantic-dark.tokens.json without a corresponding entry in semantic-light.tokens.json.
  • DON'T add typography, spacing, radius, motion, z-index, or layout tokens to semantic-dark.tokens.json.
  • DON'T ship broken alias references — validate that every {path.to.token} in semantic files resolves in primitives before committing.

Force UI Design System — design.md Spec Layer 2 of 3. Layer 1: token JSON files (tokens/). Layer 3: UI pattern files (patterns/). Last updated: 2026-04-16