Get started

Design tokens for focus indicators: define the contract and verify every state

A focus indicator is not one color token. It is a source-to-render contract connecting semantic intent, reusable geometry, selector policy, component behavior, environmental fallbacks, named consumers, and recorded observations.

Updated October 2, 2026

Start with a narrow acceptance boundary

The useful unit of work isn't "make focus accessible." That scope is too broad to assign or verify. Name one focus-indicator contract, its governing source and version, its supported environments and consumers, and the surfaces where it must remain inspectable. Mark everything outside that boundary as unresolved or not applicable, with a reason.

For example, a bounded review might cover the primary button, inline link, text input, checkbox, and tabs in one web application. It could include the application's light and dark themes, ordinary and contrasting surfaces, currently supported browsers, and forced colors. It would not cover every operating system, browser, component, or assistive-technology combination unless someone actually tested them.

Do not turn scope into proof

Writing "supports forced colors" in a contract records an expectation. It becomes an observation only after someone inspects the named consumer in the recorded environment. Neither state by itself establishes accessibility conformance.

Give every decision one enforceable home

Focus color is often semantic because several controls need to share the same intent across themes. Geometry can also be reusable when a system standardizes outline width, offset, style, or a combined treatment. HPE's global focusIndicator is one example of reusable geometry living above individual components. A component contract should own a decision only when a named component genuinely needs behavior narrower than the shared rule.

Decision ownedWhere it is enforcedAccountable ownerFailure signal
Semantic color and shared intentLight and dark focus role, plus any approved role relationshipToken source and emitted semantic artifactsDesign-system token ownerWrong role, wrong mode mapping, missing export, or stale artifact
Reusable geometryShared outline width, style, offset, or another reusable treatment when the system standardizes itGlobal or semantic token contract and generated outputDesign-system foundation ownerShared geometry differs without an approved component exception
Component-specific behaviorVariant geometry, clipping avoidance, internal focus treatment, or composite-control behaviorComponent contract and component implementationComponent ownerOne component or variant diverges while the shared inputs are correct
Selector and fallback behavior:focus, :focus-visible, fallback boundary, and rule orderingApplication or library CSSCSS implementation ownerThe intended selector does not match, the fallback is removed, or cascade order suppresses the indicator
Forced-color handlingMedia-query branch, system-color choice, and treatment of effects that may be suppressedCSS under @media (forced-colors: active)Accessibility and CSS ownersThe indicator disappears, becomes indistinguishable, or depends on a suppressed effect
Approved exceptionExact consumer, reason, replacement behavior, approver, and expiry or review conditionException register plus narrow component or product ruleNamed exception ownerAn override has no record, spreads to other consumers, or outlives its review condition
Unresolved authorityA missing source decision, disputed owner, or unsupported environmentDecision register, not production CSS disguised as policyPerson assigned to resolve the decisionImplementation proceeds through guesswork or an accidental local default
Portable responsibility matrix for a focus-indicator contract

Read HPE, Carbon, and NYS as examples, not a shared standard

Public design systems solve similar problems with different structures. Their names describe their own systems; they are not portable names that every project must copy.

Observed approachUseful lessonDo not universalize
HPEA global focusIndicator groups a standardized treatment used across its design system.Reusable focus geometry can be governed above individual components when one shared contract is intentional.The HPE token name, package shape, values, or ownership model.
CarbonFocus-related roles appear inside a larger catalog organized for Carbon's interface system.A mature system may need several roles rather than one generic focus color.Carbon's role names or the assumption that its catalog maps directly to another component library.
NYSPrimitive values feed purpose-driven semantic roles, including a semantic focus color, with theming as another layer.Consumers should depend on purpose rather than a raw palette value when the role must survive theme changes.The NYS variable name, layer boundaries, or theme mechanics outside an NYS implementation.
What each captured design-system reference contributes

A portable method begins above those system-specific choices: record the purpose, authority, mapping, consumers, evidence, and disposition. Your project can then adopt, translate, or reject a vendor's structure without treating the vendor's terminology as a web-wide convention.

Copy the focus-indicator contract before writing CSS

The contract below is long on purpose. A field may be required, conditional, unresolved, or not applicable, but it should not disappear. Missing fields are where assumptions turn into local overrides.

focusIndicatorContract:
  contractId: required
  governingSource:
    name: required
    version: required
    authorityOwner: required
  semanticPurpose: required
  supportedEnvironments:
    browsersAndVersions: required
    platforms: required
    forcedColors: required | not_applicable_with_reason
  modes:
    lightMapping: required
    darkMapping: required
    additionalThemes: conditional
  geometry:
    sharedTreatment: required | unresolved
    componentOverrides: conditional
  selectorPolicy:
    focusRule: required
    focusVisibleRule: required
    unsupportedSelectorFallback: required
    keyboardExpectation: required
    pointerExpectation: required
    programmaticFocusExpectation: required
  forcedColorPolicy:
    affectedPropertyRisk: required
    systemColorFallback: required | unresolved
  namedConsumers: required
  protectedSurfaces: required
  approvedExceptions: required | none
  owners:
    tokens: required
    components: required
    css: required
    verification: required
  evidence:
    sourceDecision: required
    artifactIdentity: required
    projectMapping: required | unresolved
    runtimeObservations: required | open
  retestTriggers: required
  disposition: accept | revise | block
Copyable focus-indicator contract. Status labels make missing evidence visible.

Protected surfaces should be concrete: a primary button on the default page background, an inline link inside a card, or tabs inside a contrasting navigation panel. "All surfaces" is not inspectable. Named consumers and representative surfaces keep verification finite without implying that untested consumers passed.

Use Ambient Sage v1 as bounded upstream intake

The public Ambient Sage page establishes a versioned upstream artifact: Ambient Sage v1, a semantic ring role, light and dark token sets, focus-ring direction, and implementation exports. It also describes vivid yellow as the saturated accent used for focus rings. Those facts belong in a project contract. They do not establish the project's alias name, selector policy, component adoption, forced-color branch, runtime result, or accessibility disposition.

Token specimen · real values

Ambient Sage

Live render

Ambient Sage's actual tokens — the same values its exports use.

Color tokensSemantic roles with HEX / HSL / CMYK

Color tokens

Ambient Sage
light · HEX · HSL · CMYK

Core

#F3F4EF

background

H 72 · C0, 0, 2, 4

#1A1C17

foreground

H 84 · C7, 0, 18, 89

#E5E6E0

card

H 70 · C0, 0, 3, 10

#ECEEE8

muted

H 80 · C1, 0, 3, 7

#D8D9D2

border

H 68.57 · C0, 0, 3, 15

Brand

#FEE951

primary

H 52.72 · C0, 8, 68, 0

#1A1C17

primary-fg

H 84 · C7, 0, 18, 89

#E5E6E0

secondary

H 70 · C0, 0, 3, 10

#F7E464

accent

H 52.24 · C0, 8, 60, 3

#FEE951

ring

H 52.72 · C0, 8, 68, 0

Semantic

#C0392B

destructive

H 5.64 · C0, 70, 78, 25

#FFFFFF

destructive-fg

H 0 · C0, 0, 0, 0

#2D7238

success

H 129.57 · C61, 0, 51, 55

#C97D12

warning

H 35.08 · C0, 38, 91, 21

#545651

muted-fg

H 84 · C2, 0, 6, 66

Charts

#FEE951

chart-1

H 52.72 · C0, 8, 68, 0

#4A8FD4

chart-2

H 210 · C65, 33, 0, 17

#6BBF8A

chart-3

H 142.14 · C44, 0, 28, 25

#E07498

chart-4

H 340 · C0, 48, 32, 12

#E8A24B

chart-5

H 33.25 · C0, 30, 68, 9

Ambient Sage v1 provides an inspectable upstream color system. The consuming project still has to map and verify its ring role.

Evidence state for this example

Upstream source and version: established. Ring role and available exports: established. Project alias, selector behavior, named component mapping, forced-color behavior, and rendered observations: unresolved until a consuming project records them.

Trace one ring role from source to a named consumer

The following chain is illustrative. Only the Ambient Sage v1 source role and export availability come from the frozen public kit evidence. Every downstream identifier is a proposed local mapping, so it remains unresolved until the project confirms it.

  1. 1

    Pin the governing source

    Record Ambient Sage v1 and the public kit page as the authority for the upstream semantic ring role. Do not replace that versioned source with a screenshot or copied literal.

    source: Ambient Sage v1
    role: ring
    status: established
  2. 2

    Identify the emitted artifact

    Choose the export the project actually consumes and record its file identity or generated version. Identity Forge offers DESIGN.md, CSS, Tailwind, DTCG, shadcn, CLI, and MCP routes, but availability does not prove which one a project used.

    artifact: unresolved
    expectedRole: ring
    status: open
  3. 3

    Declare the project alias

    Map the incoming role to one local semantic property. The name below is an example, not an observed Ambient Sage export.

    --app-focus-ring: var(--ring);
  4. 4

    Apply selector policy

    Keep a visible :focus fallback, then refine it with :focus-visible where supported. Record expected behavior by focus path instead of assuming that the device type determines the match.

    .control:focus {
      outline: var(--app-focus-width) solid var(--app-focus-ring);
      outline-offset: var(--app-focus-offset);
    }
    
    .control:focus:not(:focus-visible) {
      outline: none;
    }
    
    .control:focus-visible {
      outline: var(--app-focus-width) solid var(--app-focus-ring);
      outline-offset: var(--app-focus-offset);
    }
  5. 5

    Bind the component contract

    For the primary Button, record whether it inherits the shared geometry or owns an approved override. Put clipping, border-radius interaction, and any variant-specific rule in the component contract instead of hiding them in a local selector.

    consumerContract: Button.primary
    sharedGeometry: unresolved
    approvedOverride: none_recorded
    status: open
  6. 6

    Inspect the named consumer

    Render the primary Button on one named surface under every in-scope condition. Record its computed styles and visible result. Until then, the trace ends with an open observation.

    consumer: Button.primary
    surface: dashboard/default
    actualObservation: open
    disposition: block

This trace prevents a common false conclusion. Finding the ring role in an exported file proves that the artifact contains the role. It does not prove that the application loads the artifact, the local alias resolves, the selector matches, the component preserves the outline, or the rendered indicator remains distinguishable on its surface.

Use :focus-visible as a heuristic boundary

The :focus-visible pseudo-class matches a focused element when the user agent decides that a focus indicator should appear. Keyboard interaction commonly influences that decision, but "keyboard means match" and "pointer means no match" are not reliable universal rules. User preferences, element type, focus movement, browser behavior, and the way focus was assigned can all affect the result.

Expectation to recordWhat to inspectFailure condition
Keyboard pathThe focused control has a visible indicator when :focus-visible matches.Selector match, computed outline or replacement treatment, clipping, and surface distinction.No visible indicator on an in-scope keyboard path, or the indicator is obscured or clipped.
Pointer pathDo not promise suppression. Record whether :focus-visible matches in each supported environment.Focused element, matched selectors, and any remaining :focus fallback.The observed result contradicts the project's documented policy or removes an indicator required by that policy.
Programmatic focusState which scripted transitions are in scope, such as focus moved into an opened dialog or to a validation target.Calling flow, focused element, selector match, and resulting indicator.Focus moves but the expected indicator is absent, or focus lands on the wrong consumer.
Record expectations by focus path without predicting the browser's heuristic

A practical fallback begins with :focus. Remove or replace that treatment only inside a selector using :focus-visible. If a browser does not support :focus-visible, it ignores the unsupported refinement and keeps the :focus rule. Test the exact rule ordering and supported-browser set; copying the pattern does not prove that the cascade works in your application.

Keep the fallback visible

Do not begin with outline: none and assume that a later :focus-visible rule will repair every case. The fallback boundary should retain a visible :focus treatment when the refinement is unsupported or does not apply as expected.

Create an explicit forced-colors branch

Forced-colors mode changes the rendering environment. The browser may force colors on several properties, while altering or suppressing decorative effects. A focus treatment that depends on box-shadow alone is therefore risky: its visual separation may disappear even when the ordinary theme looks correct.

Use the forced-colors media feature to define a bounded fallback, then inspect it. System colors such as Highlight follow colors selected by the user agent or operating environment. They are a suitable candidate for a focus outline in this branch, but choosing one in CSS remains an expectation. It does not prove that every consumer renders correctly.

@media (forced-colors: active) {
  .control:focus-visible {
    box-shadow: none;
    outline: var(--app-focus-width) solid Highlight;
    outline-offset: var(--app-focus-offset);
  }
}
Illustrative forced-color branch using documented CSS syntax and a system color. Project aliases, dimensions, and results remain unverified.

Record four facts for this branch: which authored effects may be lost, which system color is selected, which consumers were inspected, and what happened. A global media query may be insufficient if a composite control draws focus on an internal item while its container clips outlines. That requires a component-specific contract or an approved exception; it is not evidence that the token failed.

Verify representative controls and surfaces

Prepare the matrix before testing so the expectations cannot drift to match a screenshot. Each row below is a test specification, not a completed benchmark. Replace "Open" with an environment-specific observation only after inspection.

Test content and conditionsInspectFailure conditionObservation and owner
Primary buttonLabel: Save changes. Keyboard, pointer, and programmatic focus on default and contrasting surfaces in light and dark modes; repeat in forced colors.Focused element, :focus and :focus-visible matches, resolved ring role, outline geometry, clipping, and disabled-state exclusion.Expected indicator is absent, clipped, obscured, mapped to the wrong mode value, or shown on a non-focusable disabled control.Open. Component owner for button behavior; CSS owner for selector or forced-color divergence.
Inline linkText: Review billing details within a paragraph. Test light and dark content surfaces, keyboard and pointer paths, then forced colors.Indicator remains distinguishable from link styling and surrounding text; inspect selector match and any outline clipping caused by line layout.Focus is indicated only by an indistinguishable color change, the indicator is clipped, or the fallback disappears.Open. Typography or link-component owner, with CSS owner for selector behavior.
Text inputLabel: Work email; value: alex@example.com; include normal and validation-error presentations. Test both modes and forced colors.Focus treatment can be distinguished from the existing border and error treatment; inspect resolved role, geometry, and cascade order.Focus state is hidden by the border or error style, replaces required error information, or disappears in forced colors.Open. Input owner for state composition; token owner if the mapped role is wrong.
CheckboxLabel: Send me release notes. Test checked and unchecked controls on default and contrasting surfaces in both modes.Indicator belongs to the actual focus target, remains visible around its small geometry, and is not confused with checked state.Checked styling is the only visible change, focus is drawn on the wrong element, or the indicator is clipped.Open. Checkbox owner for target and geometry; CSS owner for selector behavior.
Composite tabsTabs: Overview, Activity, Settings. Move focus among items through the component's supported keyboard interaction; repeat in both modes and forced colors.Distinguish focused item from selected item; inspect the active descendant or focused element, selector match, overflow clipping, and system-color result.Selection is mistaken for focus, focus moves without a visible indicator, multiple items appear focused, or the container clips the treatment.Open. Composite-control owner for focus management; CSS owner for rendering.
Expectation-first focus-indicator verification matrix

Why these controls

This is a representative set, not a claim of complete coverage. It includes text, compact controls, bordered controls, and a composite widget where selected and focused states can diverge. Add another consumer only when its focus contract differs materially.

Route failures to the first divergent layer

A rendered mismatch sits at the end of a chain. Correct it at the first layer that differs from the approved contract, then retest every downstream layer affected by the correction.

  1. 1

    Confirm source authority

    Check the governing source, version, approved semantic purpose, mode mappings, and reusable geometry. If authority is missing or disputed, stop and assign the decision. Do not let local CSS settle it by accident.

  2. 2

    Inspect the emitted artifact

    Identify the exact export the project uses. Confirm that the expected role and values appear in that artifact. A correct source paired with a stale or incomplete export is an artifact problem.

  3. 3

    Resolve the project mapping

    Follow the emitted role into the local alias and both mode branches. If the alias points elsewhere or an override applies at the wrong scope, the project mapping is the first divergence.

  4. 4

    Check selector and environmental CSS

    Inspect rule order, specificity, the :focus fallback, :focus-visible matching, and the forced-colors branch. Record the browser and environment instead of generalizing from one result.

  5. 5

    Check the component contract

    Confirm that the named component consumes the shared rule or has an approved narrow override. Inspect overflow, borders, shadows, internal focus targets, and state composition.

  6. 6

    Record the consumer observation

    Record expected and actual results separately. Assign the correction owner at the first divergent layer, name the change that triggers a retest, and set the disposition to accept, revise, or block.

If the upstream role is correct, the artifact contains it, and the project alias resolves, changing the source color is unlikely to restore a missing ring. Check the selector, cascade, component overflow, or focus target. Conversely, trace a ring rendered with the wrong light or dark value through the local mapping and emitted artifact before adding a component override.

Join expectations and observations in one acceptance record

The acceptance record closes the loop. Keep source facts, proposed mappings, observations, exceptions, and decisions in separate fields so reviewers can see exactly what remains open.

focusIndicatorAcceptance:
  contractId: focus-web-v1
  sourceAuthority:
    source: Ambient Sage
    version: v1
    upstreamRingRole: established
  artifactIdentity:
    format: unresolved
    fileOrBuildId: unresolved
    observedRole: open
  projectMapping:
    alias: --app-focus-ring
    status: illustrative_unverified
  consumer:
    component: Button.primary
    surface: dashboard/default
  environment:
    browserAndVersion: unresolved
    theme: light
    focusPath: keyboard
    forcedColors: inactive
  expectedResult:
    selectorPolicy: use_focus_fallback_then_focus_visible_refinement
    visibleTreatment: required
    geometry: unresolved
  actualResult:
    selectorMatches: open
    computedStyles: open
    visualInspection: open
  exception:
    id: none_recorded
    owner: not_applicable
  correctionOwner: unresolved_until_first_divergence
  retestTrigger: any_change_to_source_artifact_mapping_css_or_button_contract
  disposition: block
Illustrative acceptance record. Its open fields prevent an unexecuted example from being mistaken for a passing test.

Use accept only when every required field within the stated boundary has evidence and no blocking mismatch remains. Use revise when the contract or implementation needs a named correction that can be retested. Use block when required authority or evidence is missing, the first divergence has no owner, or an in-scope consumer fails its expectation. Optional evidence may be marked not applicable with a reason; required evidence may not.

Start with a versioned upstream system

If your project still lacks semantic light and dark roles or an identifiable implementation artifact, inspect the free Ambient Sage kit as an upstream example. Then complete the local mapping, selector policy, component contract, and observations in your own acceptance record.

Sources