CEM D5 Stroke & Separation — Boundaries, Dividers, and Focus Indicators

Status: Canonical (D5 v1.0) Last updated: August 10, 2026

Taxonomy placement: D5. Stroke & Separation (part of the 7-dimensional CEM token framework)

Companion specs:


Contents

  1. What D5 controls
  2. Design goals
  3. Minimal stroke basis
  4. Semantic endpoints (product-facing contract)
  5. Focus, selected, and target indicators
  6. Dividers and separators
  7. Inter-dimension coupling
  8. Accessibility and forced-colors requirements
  9. Implementation guidance (CSS recipes)
  10. Framework mapping guidance (Angular Material, MUI)
  11. Canonical token summary
  12. Governance and versioning
  13. References

1. What D5 controls

D5 defines the geometry of separation:

D5 does not define:


2. Design goals

2.1 Consumer semantics first

Tokens must express what the stroke means to the consumer, not how it is implemented:

2.2 Bounded complexity

Stroke systems can explode into per-component and per-state width tokens. CEM avoids that by defining:

2.3 No layout shift on interaction

Changing border width on focus is a common source of layout shift (e.g., label jumps in outlined text fields). Focus and selection indicators should be drawn using outline or box-shadow, or on a pseudo-element, rather than changing border-box geometry.

2.4 Accessibility as a hard constraint

D5 must support modern focus-indicator requirements (including minimum visible area/thickness and sufficient contrast in high-contrast themes). See §8.


3. Minimal stroke basis

CEM D5 uses a 4-step stroke basis that covers almost all UI needs without a large token surface.

3.1 Stroke basis tokens

These tokens are foundational. Semantic endpoints map to them.

:root {
  /* Basis: keep small and stable */
  --cem-stroke-none:     0px;  /* no stroke */
  --cem-stroke-hair:     1px;  /* hairline separators, subtle boundaries */
  --cem-stroke-standard: 2px;  /* focus/selection indicators; high legibility */
  --cem-stroke-strong:   3px;  /* high-emphasis boundaries; contrast themes */
}

Notes

cem-stroke-basis
TokenValueDescriptiontier
--cem-stroke-none0pxNo strokerequired
--cem-stroke-hair1pxHairline separators / subtle boundariesrequired
--cem-stroke-standard2pxFocus / selection / target indicator thicknessrequired
--cem-stroke-strong3pxHigh-emphasis boundaries / contrast themesrequired

3.2 Density coupling (D2 interaction)

Stroke thickness is allowed to vary by density mode as a non-breaking adjustment, as long as semantics stay intact:


4. Semantic endpoints (product-facing contract)

These are the canonical semantic endpoints that components consume.

4.1 Endpoints

:root {
  /* Canonical boundaries */
  --cem-stroke-boundary:          var(--cem-stroke-hair);

  /* Canonical separators */
  --cem-stroke-divider:           var(--cem-stroke-hair);

  /* Canonical indicators */
  --cem-stroke-focus:             var(--cem-stroke-strong);
  --cem-stroke-selected:          var(--cem-stroke-standard);
  --cem-stroke-target:            var(--cem-stroke-standard);
  --cem-stroke-pending:           var(--cem-stroke-standard);

  /* Placement */
  --cem-stroke-indicator-offset:  2px; /* distance outside the edge, not thickness */

  /* Optional convenience endpoints (aliases, not required by contract) */
  --cem-stroke-grid:              var(--cem-stroke-divider);        /* tables / data grids */
  --cem-stroke-boundary-strong:   var(--cem-stroke-standard);       /* dialogs / sheets when needed */
}
cem-stroke-semantic
TokenValueDescriptiontier
--cem-stroke-boundaryvar(--cem-stroke-hair)Default control container edge (text fields, chips, cards)required
--cem-stroke-boundary-strongvar(--cem-stroke-standard)Strong boundary when elevation/shadow cannot carry separationrecommended
--cem-stroke-dividervar(--cem-stroke-hair)Sibling separators (list rows, table rows, sections)required
--cem-stroke-gridvar(--cem-stroke-divider)Tables / data-grid divider aliasrecommended
--cem-stroke-focusvar(--cem-stroke-strong)Keyboard focus indicator thicknessrequired
--cem-stroke-selectedvar(--cem-stroke-standard)Selection indicator thicknessrequired
--cem-stroke-targetvar(--cem-stroke-standard)Deep-link target indicator thicknessrequired
--cem-stroke-pendingvar(--cem-stroke-standard)Persistent pending-state boundary/indicator thicknessrecommended
--cem-stroke-indicator-offset2pxExternal ring/outline offset (distance, not thickness)required

4.2 Semantics (normative)

Optional convenience endpoints (aliases):


4.3 Component mapping matrix (informative)

The D5 contract intentionally avoids per-component thickness tokens (e.g., --cem-input-outline). Instead:

Rule of thumb: color is D0 (--cem-separator-color), thickness is D5, corner curvature is D3, spacing/insets is D1, and the interactive guard floor is D2.

Component familyPrimary D5 endpoint(s)Typical stroke usageNotes
Dividers (horizontal/vertical)--cem-stroke-divider + D0 --cem-separator-colorHairline inside a spaced trackPackage the line with D1 relationship margins, floor interactive separation at the D2 guard, and prefer inset patterns for lists.
List item separators--cem-stroke-divider (or --cem-stroke-grid)Row separatorsTreat dense “data-list” layouts as a grid for readability.
Table borders / grid lines--cem-stroke-gridCell/row separatorsUse grid to distinguish “structured separators” from generic dividers; same thickness by default.
Text field outline (outlined pattern)--cem-stroke-boundaryControl container edgeFocus indication should be a ring (--cem-stroke-focus) rather than mutating border width.
Text field underline (filled pattern)--cem-stroke-boundary or --cem-stroke-dividerBaseline boundaryUnderline is a boundary of the control, not a sibling divider; map to divider only when visually used as “row separation”.
Pending input boundary--cem-stroke-pendingPersistent thicker anchorPair with a pending color and programmatic busy state; do not infer pending from focus, validation, or value changes.
Outlined button border--cem-stroke-boundaryVariant boundaryUse boundary-strong only for “hard separation” themes (e.g., low-elevation UIs).
Card / tile border--cem-stroke-boundary (often --cem-stroke-none)Optional edgePrefer D4 elevation or D0 surface contrast; use boundary only when needed for scannability.
Checkbox / radio container border--cem-stroke-boundaryControl boundaryThe checkmark/dot is not a stroke token concern; it is icon geometry (D3) + color (D0).
Switch track outline (if any)--cem-stroke-boundaryTrack boundaryTrack height is D2/D1, not D5; D5 only provides line weight if outlined.
Chip / badge border (bordered variant)--cem-stroke-boundarySubtle outlineIn dense chips, consider mapping boundary → standard via D2 coupling if needed for separation.
Tabs / segmented control indicator--cem-stroke-selectedActive underline/bar thicknessAvoid using divider for active indicators; indicators communicate state, not separation.
Focus ring (all controls)--cem-stroke-focus + --cem-stroke-indicator-offsetExternal ring/outlineMust satisfy WCAG 2.2 focus appearance (§8.1).
Target highlight (deep link / “jump to”)--cem-stroke-targetTemporary outline/ringDistinct from focus: target is navigational, focus is interactive.
Menus / dropdowns / popovers (edge)--cem-stroke-boundary (often none)Optional borderPrefer D4 shadow separation; use boundary/boundary-strong in forced-colors or shadow-suppressed contexts.
Progress/slider tracks(usually none) / --cem-stroke-boundaryOutline only if requiredTrack thickness (e.g., 8px) is a dimension; treat that as D2/D1, not D5.

5. Focus, selected, and target indicators

CEM uses an outline-driven indicator model for states that must remain visible across backgrounds:

5.1 Zebra outline system (existing in theme-data.xhtml)

The current theme implementation uses a zebra ring composed of stacked outside strokes using box-shadow and zebra color variables:

In normal themes, a 3-stripe ring is used; in contrast themes, a 4-stripe ring is used (intent + focus + target + selected).

Ownership split (R-D5-1 resolved): --cem-zebra-strip-size and --cem-zebra-color-{0..3} are owned by D0 (cem-colors.md) and emitted by cem-colors.html because they ship with full theme-mode coverage (.cem-theme-{light,dark,contrast-light,contrast-dark,native}). D5 owns:

D5 ring recipes reference D0-owned zebra tokens via var(); D5 does NOT redeclare them.

5.2 Canonical zebra tokens (D5 contract)

D5 treats these as canonical indicator-pattern tokens:

:root {
  --cem-zebra-strip-size: 2px;      /* thickness per stripe */
  --cem-zebra-angle:      45deg;    /* if stripes are rendered as gradients */
  --cem-zebra-color-0:    Canvas;       /* intent / base stripe (contrast themes) */
  --cem-zebra-color-1:    CanvasText;   /* focus stripe */
  --cem-zebra-color-2:    Mark;         /* target stripe */
  --cem-zebra-color-3:    SelectedItem; /* selected stripe (or themed alternative) */
}

--cem-zebra-strip-size and --cem-zebra-color-{0..3} are owned by D0 (cem-colors.md) and emitted by cem-colors.html. D5 only declares --cem-zebra-angle and the ring composition recipes below.

cem-stroke-zebra-pattern
TokenValueDescriptiontier
--cem-zebra-angle45degStripe angle for gradient-mode zebrarecommended

Pending background geometry

Pending background stripes are separate from zebra focus/selection rings. The square tile and diagonal five-stop gradient follow the D0 pending recipe. The canonical angle is 45 degrees; arbitrary angles need a matching tile recipe to retain seamless edges.

cem-stroke-pending-pattern
TokenValueDescriptiontier
--cem-pending-angle45degPending background gradient tiltrecommended
--cem-pending-tile-size2remSquare background tile and distance traveled per cyclerecommended
/* 3-stripe ring: focus/target/selected (normal themes) */
--cem-ring-zebra-3:
  0 0 0 calc(1 * var(--cem-zebra-strip-size)) var(--cem-zebra-color-1),
  0 0 0 calc(2 * var(--cem-zebra-strip-size)) var(--cem-zebra-color-2),
  0 0 0 calc(3 * var(--cem-zebra-strip-size)) var(--cem-zebra-color-3);

/* 4-stripe ring: intent + focus + target + selected (contrast themes) */
--cem-ring-zebra-4:
  0 0 0 calc(1 * var(--cem-zebra-strip-size)) var(--cem-zebra-color-0),
  0 0 0 calc(2 * var(--cem-zebra-strip-size)) var(--cem-zebra-color-1),
  0 0 0 calc(3 * var(--cem-zebra-strip-size)) var(--cem-zebra-color-2),
  0 0 0 calc(4 * var(--cem-zebra-strip-size)) var(--cem-zebra-color-3);

Why box-shadow?

cem-stroke-rings
TokenValueDescriptiontier
--cem-ring-zebra-30 0 0 calc(1 * var(--cem-zebra-strip-size)) var(--cem-zebra-color-1), 0 0 0 calc(2 * var(--cem-zebra-strip-size)) var(--cem-zebra-color-2), 0 0 0 calc(3 * var(--cem-zebra-strip-size)) var(--cem-zebra-color-3)3-stripe focus/target/selected ringrecommended
--cem-ring-zebra-40 0 0 calc(1 * var(--cem-zebra-strip-size)) var(--cem-zebra-color-0), 0 0 0 calc(2 * var(--cem-zebra-strip-size)) var(--cem-zebra-color-1), 0 0 0 calc(3 * var(--cem-zebra-strip-size)) var(--cem-zebra-color-2), 0 0 0 calc(4 * var(--cem-zebra-strip-size)) var(--cem-zebra-color-3)4-stripe ring (contrast themes)recommended
cem-stroke-rings-forced
TokenValue
--cem-ring-zebra-30 0 0 var(--cem-stroke-focus) Highlight
--cem-ring-zebra-40 0 0 var(--cem-stroke-focus) Highlight

cem-stroke-rings-forced carries forced-colors fallback values for the rings (Principle P4). Generator-only — no new tokens. The same ring names are redeclared inside @media (forced-colors: active) :root { … }.

5.4 Outline/underline stripe-stack transform

Input indicators may reuse one locally composed stripe stack for both full-boundary and bottom-boundary appearances. D5 represents the appearance as a unitless geometry selector rather than an inherited box-shadow recipe:

cem-stroke-indicator-appearance
TokenValueDescriptiontier
--cem-indicator-appearance-outline0Put cumulative stripe width in box-shadow spread, producing a full outlinerecommended
--cem-indicator-appearance-underline1Put cumulative stripe width in positive block-axis offset and zero the spread, producing an underlinerecommended

For appearance selector a and cumulative stripe width w, every shadow uses:

0 calc(a * w) 0 calc((1 - a) * w) <stripe-color>

At 0, the Y offset is zero and spread produces a concentric outline. At 1, spread is zero and positive cumulative Y offsets leave only the bottom portions visible. Components MUST compose the shadow list locally so state-local stripe colors and widths resolve at the paint target. A root-declared recipe containing nested var(--stripe-color) references resolves those references at the declaration scope and can freeze descendant state overrides.

The transform does not fix a semantic-role count. Input components omit the shadow stack at rest. Invalid state activates an anchor using --cem-stroke-boundary; pending uses --cem-stroke-pending. Focus and selection activate independent stripes using --cem-zebra-strip-size. Choice indicators paint the native input rather than its enclosing label. Native radios use their built-in dot for selection and omit an additional selected stripe. Their focus, invalid and pending outlines use a decorative ::before on the input with --cem-bend-circle to follow the native circle; the native dot and input appearance remain intact. Checkbox and switch selection stripes remain. Inactive anchor width is zero; outside stripe changes do not affect layout. A component-friendly property such as --cem-input-indicator-appearance is an adapter hook, not a canonical D5 token; its supported values are references to the two tokens above.

box-shadow is not a forced-colors fallback. Components MUST remove the shadow stack in forced colors, preserve native state semantics, and provide a system-color full outline for focus-visible. An underline selection in normal rendering does not weaken that forced-colors focus requirement.

5.5 Focus heuristics

Prefer :focus-visible semantics. If the platform needs a polyfilled focus-ring behavior, align with heuristics similar to Material Web’s focus ring utilities (see references).


6. Dividers and separators

6.1 Divider role types

CEM recognizes three common divider roles (same thickness basis, different usage):

  1. Section divider: separates groups (higher salience)
  2. Row divider: list/table row hairlines
  3. Gridline: dense tabular separation

All three should normally map to --cem-stroke-divider (hairline). Promote to --cem-stroke-boundary-strong when:

6.2 Inset patterns (non-token guidance)

Avoid creating many inset tokens. Use a single inset rule:

Spacing and inset should use D1 tokens. The canonical divider is a track, not a bare line:

  1. Select the intended relationship space from D1 (--cem-gap-related, --cem-gap-group, --cem-gap-block, or --cem-gap-section).
  2. If the siblings are interactive, floor the track at D2 --cem-coupling-guard-min.
  3. Center the D5 line within that cross-axis track by assigning half of the remaining extent to each margin.
  4. Apply --cem-inset-container along the line's logical start edge for the standard inset pattern.
margin-block: calc(
  (max(var(--cem-gap-group), var(--cem-coupling-guard-min)) - var(--cem-stroke-divider)) / 2
);
border-block-start: var(--cem-stroke-divider) solid var(--cem-separator-color);

For a vertical divider, transpose block and inline axes. The line plus both cross-axis margins equals the divider track. A bare --cem-stroke-divider line cannot stand in for either D1 relationship spacing or the D2 guard.


7. Inter-dimension coupling

7.1 D3 shape coupling

Indicator rings should follow component rounding:

border-radius: var(--cem-bend-control);
box-shadow: var(--cem-ring-zebra-3);

If the ring is outside, it may require a slightly larger radius:

border-radius: calc(var(--cem-bend-control) + var(--cem-stroke-indicator-offset));

7.2 D1 spacing coupling

Where indicators sit outside the edge, ensure surrounding layout provides enough breathing room. Prefer:

Treat --cem-coupling-guard-min as the default clearance budget between adjacent operable zones. If you increase any of the following, you must also increase surrounding D1 spacing and/or D2 guard/halo (or accept overlap):

See the D2 compatibility rule in cem-coupling.md §4.1.1.

7.3 D4 elevation coupling

Elevation and stroke are substitutes:


8. Accessibility and forced-colors requirements

8.1 Focus appearance (WCAG 2.2)

The focus indicator must be large/visible enough to be reliably perceived. The default is a 3px perimeter (or equivalent area) around the focused component, which D5’s --cem-stroke-focus (aliased to --cem-stroke-strong) supports.

8.2 Forced colors / high contrast

In forced-colors: active contexts:

Suggested baseline:

@media (forced-colors: active) {
  .cem-focusable:focus-visible {
    outline: var(--cem-stroke-focus) solid CanvasText;
    outline-offset: var(--cem-stroke-indicator-offset);
    box-shadow: none;
  }
}

9. Implementation guidance (CSS recipes)

9.1 Prefer rings over border-width mutation

Avoid

/* Causes layout shift */
.control:focus { border-width: 2px; }

Prefer

.control:focus-visible {
  outline: var(--cem-stroke-focus) solid currentColor;
  outline-offset: var(--cem-stroke-indicator-offset);
}

or (for rounded geometry / zebra):

.control:focus-visible {
  box-shadow: var(--cem-ring-zebra-3);
}

9.2 Layering order for multiple indicators

If multiple indicators can apply, compose in a stable order:

  1. focus
  2. selected
  3. target

In zebra, this is naturally expressed as concentric stripes.

9.3 Action components (existing pattern)

Current implementation uses:

.action { box-shadow: var(--cem-action-box-shadow); }
.action:hover { --cem-action-box-shadow: var(--cem-action-box-shadow-hover); }
.action:active { --cem-action-box-shadow: var(--cem-action-box-shadow-active); }

In contrast themes, --cem-action-box-shadow becomes the zebra ring.


10. Framework mapping guidance (Angular Material, MUI)

This section is non-normative. It documents how to map framework tokens/variables into the CEM D5 semantic endpoints.

10.1 Angular Material (MDC-based) mapping

Common MDC variables (examples seen in Angular Material customization workflows):

Angular Material also exposes higher-level variables in some setups, e.g.:

Guidance: map these variables from CEM tokens; do not treat them as canonical tokens in CEM.

10.2 MUI mapping

MUI System

MUI Joy UI Joy exposes per-component CSS variables for focus ring geometry (examples):


11. Canonical token summary

TokenCategoryRequiredMeaning
--cem-stroke-noneBasis✓No stroke
--cem-stroke-hairBasis✓1px hairline
--cem-stroke-standardBasis✓2px standard indicator
--cem-stroke-strongBasis✓3px strong separation
--cem-stroke-boundarySemantic✓Default component boundary
--cem-stroke-boundary-strongSemanticOptionalStrong boundary for cases where elevation/shadow cannot carry separation
--cem-stroke-dividerSemantic✓Default separator/divider
--cem-stroke-gridSemanticOptionalDivider alias for structured content (tables/grids)
--cem-stroke-focusSemantic✓Focus-visible indicator width
--cem-stroke-selectedSemantic✓Selection indicator width
--cem-stroke-targetSemantic✓Target indicator width
--cem-stroke-pendingSemanticOptionalPersistent pending/loading indicator width
--cem-stroke-indicator-offsetPlacement✓Ring/outline offset distance
--cem-zebra-strip-sizePatternOptionalStripe thickness for zebra indicators
--cem-zebra-anglePatternOptionalStripe angle for gradient zebra
--cem-zebra-color-0..3PatternOptionalConcentric zebra stripe colors (intent/focus/target/selected)
--cem-action-box-shadowAdapter hookOptionalExisting action indicator/shadow hook (implementation detail)

12. Governance and versioning

D5 v1.0 became canonical after cross-dimension review on August 10, 2026. The review confirmed that D0 owns separator color, D1 owns relationship spacing and inset, D2 floors the complete divider track, and D5 owns the line and indicator geometry.

12.1 What counts as breaking

Treat as major (breaking) if you:

12.2 What is non-breaking

Treat as minor/patch if you:


13. Token manifest index

Source tableSectionDescription
cem-stroke-pending-pattern§5Pending gradient angle and tile geometry
cem-stroke-basis§3.1Stroke basis: --cem-stroke-{none,hair,standard,strong}
cem-stroke-semantic§4.1Semantic endpoints: --cem-stroke-{boundary,boundary-strong,divider,grid,focus,selected,target,pending,indicator-offset}
cem-stroke-zebra-pattern§5.2--cem-zebra-angle (gradient-mode geometry)
cem-stroke-action-outlineContrast action contoursAction contour display at ordinary/contrast theme boundaries
cem-stroke-rings§5.3Ring composition recipes: --cem-ring-zebra-3, --cem-ring-zebra-4
cem-stroke-rings-forced§5.3Forced-colors fallback values for the rings (generator-only; no new tokens)
cem-stroke-indicator-appearance§5.4Unitless outline/underline geometry selectors

Generator derivation rules:


14. References

Contrast action contours

cem-stroke-action-outline
TokenValueContrastDescriptiontier
--cem-action-outline-displaynoneblockEnable the action contour in contrast themes; reset in light/dark/native scopesrequired

This source table is part of the D5 token manifest. The generator emits Value at the root and every theme boundary, then Contrast at contrast-light/dark boundaries. Geometry uses existing stroke endpoints: boundary at rest, standard on enabled hover, strong during activation, and pending while an action is in progress. Disabled suppresses pointer state changes. The contour sits inside the painted surface, so changing thickness does not change the hit area or layout. Focus zebra stays outside the operable bounds. Pending animates D0's current-intent gradient, with a brighter stripe in light contrast mode and the ordinary dark endpoints in dark mode, using the existing D7 pending duration and D5 tile size; the center is masked out. Reduced motion freezes it, and forced colors replaces it with the native pending outline.

Persistent action selection

cem-action[selected] uses the D0 selected stripe (--cem-zebra-color-3) with D5 zebra geometry even without keyboard focus. Focus is stripe 1, target is stripe 2 and selected is stripe 3. Selection does not require a new fill or an additional hit area. Hover, held press, disabled and pending do not erase it. In forced colors, suppress shadows and retain an inset SelectedItem boundary separate from the outer focus/pending outline; neither changes layout.