Get started

Z-index design tokens: define layer roles and debug stacking failures

Use shared z-index tokens for stable, named relationships between independently owned interface layers. Keep ordering inside a component local to that component. If an element still appears underneath another one, trace the stacking contexts and mappings before raising the number. A larger value cannot outrank an element in a higher stacking context.

Updated October 3, 2026

What public systems are optimizing for

The systems captured on October 3, 2026 use different z-index scales. That difference matters. Each vocabulary reflects a different ownership boundary, so copying the names without copying that boundary creates false clarity.

ConventionExamplesUseful whenMain risk when borrowed
Numeric scaleNumeric scaleUSWDS; Duet token categoryThe system wants supported values and implementation helpers without naming every product layer.A number shows where something sits but may not explain which relationship it protects.
Application-role rampApplication-role rampSalt roles such as application header, modal, notification, and context menuThe design system governs recurring layers across an application.A borrowed role may imply authority that the consuming project does not actually share.
Component-oriented rampComponent-oriented rampShoelace drawers, dialogs, dropdowns, toasts, and tooltipsA component library owns a known overlay family.Product integrations can introduce relationships outside the library's vocabulary.
Broad framework vocabularyBroad framework vocabularyChakra roles including sticky, banner, overlay, modal, popover, toast, and tooltipA framework supplies common application roles out of the box.The convenient list can encourage unused global layers or hide component-local relationships.
Observed conventions as captured on October 3, 2026. These are system-specific choices, not a proposed universal ramp.

Nord publishes named depth roles inside a versioned cross-foundation catalog. Panda treats z-index as a supported token category and makes the authoring, semantic-reference, generated-CSS, and consumption stages visible. Martin Fowler's token architecture adds another useful distinction: available options, application decisions, and component scope answer different questions.

Shoelace is historical evidence

The captured Shoelace page says the library is sunset with no active development. Its component-oriented naming still shows one possible ownership model, but use the current library's documentation for implementation decisions.

Don't average these ramps into a new one. Decide who owns each relationship first. The numeric values come later.

Route every relationship to one of four outcomes

Start with a sentence, not a number: "The notification must remain above an open dialog," or "The tooltip must remain above the control that owns it." Then classify the relationship by authority and change scope.

OutcomeUse it whenRequired recordOwner
Shared semantic roleShared semantic roleThe relationship recurs across independently changing components or surfaces and should change as one policy.Semantic purpose, ordering constraints, named consumers, context assumptions, and protected relationships.Design-system or application-layer policy owner.
Component-local orderComponent-local orderParts move together with one component and have no independent application-level meaning.Component contract, internal parts, allowed local range, and supported states.Component owner.
Approved exceptionApproved exceptionA named integration or product surface cannot follow the shared relationship for a documented reason.Scope, conflicting rule, replacement behavior, approver, expiry or retest trigger.Product or integration owner plus the policy approver.
Unresolved ruleUnresolved ruleAuthority, required order, affected consumers, or runtime conditions remain disputed or unknown.Open question, evidence needed, decision owner, and blocking consequence.Named decision owner; no token is created yet.
Responsibility matrix for proposed layer relationships.

A candidate earns shared status only if one authority can decide it and multiple named consumers need the relationship. Those consumers may change independently, but the relationship should remain stable. A controlled change must also show which consumers should move and which should not.

A name such as tooltip isn't enough. If a tooltip exists only inside one menu and its order changes only with that menu, the menu component probably owns the relationship. If tooltips from several component families must consistently outrank ordinary overlays, the relationship may belong to shared application policy.

Do not encode a disagreement as a token

If one team expects notifications above dialogs while another expects critical dialogs above every notification, the missing artifact is a policy decision. Adding --z-super-top only hides the disagreement.

Copy this versioned layer contract

The contract below is illustrative and unexecuted. Its identifiers and values don't come from Identity Forge or any observed design system. Replace every field with project evidence before treating it as accepted.

layerContract:
  id: application-overlay-policy
  authority:
    source: ui-system/layers.yaml
    version: 0.1.0-proposed
    owner: design-system-team
  role:
    name: layer.notification
    purpose: transient application notification
    value: 500
  orderingConstraints:
    above:
      - layer.dialog
    below:
      - approved-emergency-surface
  allowedConsumers:
    - global-notification-region
  contextAssumptions:
    - consumer host participates in the intended application stacking context
    - no ancestor traps the consumer below the dialog host
  componentLocalRelationships:
    - notification close button remains local to Notification
    - notification progress bar remains local to Notification
  exceptions:
    - id: embedded-support-widget
      status: proposed
      owner: integrations-team
      rationale: third-party host relationship requires separate verification
  evidence:
    state: UNOBSERVED
    references: []
  correctionOwners:
    sourcePolicy: design-system-team
    generatedArtifact: build-platform-team
    projectMapping: application-platform-team
    componentContract: component-team
    renderedConsumer: owning-product-team
  retestTriggers:
    - source version changes
    - overlay host or portal target changes
    - positioning, transform, opacity, isolation, or containment changes on an ancestor
    - named consumer changes its mounting location
  disposition: revise
Illustrative layer contract. No runtime check has been executed.

The contract keeps intent, implementation assumptions, evidence, and disposition separate. A proposed value can be internally coherent while the evidence remains UNOBSERVED. A correct rendered screenshot also doesn't prove that the source policy or mapping is correct if an accidental local override produced the result.

What belongs outside the shared contract

  • The order of a dialog panel above its own backdrop belongs in the dialog contract unless another component depends on that internal relationship.
  • The arrow, bubble, and dismiss control inside a tooltip remain component details.
  • A third-party widget with a fixed host and separate lifecycle should remain an integration exception until its relationship is verified.
  • A temporary debugging value belongs in a branch or diagnostic fixture, not in the accepted semantic scale.
  • A relationship with no named consumer remains a proposal, not inventory.

Start from a complete upstream system

A design kit can supply the broader visual direction, semantic colors, typography, spacing, and implementation guidance. The consuming project still needs to define and verify its own layer policy.

Trace one role from source to rendered consumer

Consider an invented rule: the global notification region should appear above the standard dialog layer. This trace doesn't prove that the ordering is right for every product. It shows where a correct decision can diverge on its way to the screen.

  1. 1

    Record the source decision

    The proposed source declares layer.dialog: 400 and layer.notification: 500. Authority is the versioned application layer policy. Evidence state: UNOBSERVED.

  2. 2

    Inspect the emitted artifact

    Expected artifact: both identifiers appear with their intended values in the generated project asset. Observation: UNOBSERVED.

    --app-layer-dialog: 400;
    --app-layer-notification: 500;
  3. 3

    Inspect the project mapping

    Expected mapping: the project aliases its dialog and notification hosts to the emitted properties without swapping or duplicating the values. Observation: UNOBSERVED.

    --layer-dialog: var(--app-layer-dialog);
    --layer-notification: var(--app-layer-notification);
  4. 4

    Check the component contract

    Expected behavior: the notification region consumes the project notification alias. Internal notification parts use local ordering and don't create new global roles. Observation: UNOBSERVED.

    .notification-region {
      z-index: var(--layer-notification);
    }
  5. 5

    Check the named consumer

    Expected result: under the recorded mounting and ancestor conditions, the notification region appears above the standard dialog. Observation: UNOBSERVED. No acceptance decision is justified until this is executed and recorded.

Illustrative CSS is not a compatibility result

The snippets show a possible mapping shape. They weren't generated by a named tool, run in a browser, or tested against a framework. Keep the observation fields open until your project executes the cases.

Verify named consumers under real conditions

A single overlay demo is weak evidence. The same shared value can behave differently when consumers mount in different hosts, sit inside scrolling containers, or inherit new stacking contexts from ancestors. Record one row for each named consumer, including any conditions that could change the result.

verificationCases:
  - consumer: primary-sticky-navigation
    conditions: viewport scroll; navigation inside application shell
    expectedOrder: above page content; below dialog and notification hosts
    observedResult: UNOBSERVED
    evidenceReference: null
    firstDivergentLayer: UNOBSERVED
    correctionOwner: UNASSIGNED_UNTIL_DIVERGENCE
    retestTrigger: shell positioning or ancestor context changes
    disposition: revise

  - consumer: account-menu-dropdown
    conditions: opened from sticky navigation; menu may mount locally or in an overlay host
    expectedOrder: above navigation content; below dialog
    observedResult: UNOBSERVED
    evidenceReference: null
    firstDivergentLayer: UNOBSERVED
    correctionOwner: UNASSIGNED_UNTIL_DIVERGENCE
    retestTrigger: portal target or navigation component changes
    disposition: revise

  - consumer: field-help-tooltip
    conditions: control inside dropdown; tooltip open while dropdown remains open
    expectedOrder: above its owning dropdown without redefining application policy
    observedResult: UNOBSERVED
    evidenceReference: null
    firstDivergentLayer: UNOBSERVED
    correctionOwner: UNASSIGNED_UNTIL_DIVERGENCE
    retestTrigger: tooltip host or dropdown contract changes
    disposition: revise

  - consumer: confirmation-dialog
    conditions: dialog open over scrolled page; standard overlay host
    expectedOrder: above page, sticky navigation, and dropdown
    observedResult: UNOBSERVED
    evidenceReference: null
    firstDivergentLayer: UNOBSERVED
    correctionOwner: UNASSIGNED_UNTIL_DIVERGENCE
    retestTrigger: dialog backdrop, panel, or host changes
    disposition: revise

  - consumer: global-notification-region
    conditions: notification emitted while confirmation dialog is open
    expectedOrder: follows the approved notification-versus-dialog policy
    observedResult: UNOBSERVED
    evidenceReference: null
    firstDivergentLayer: UNOBSERVED
    correctionOwner: UNASSIGNED_UNTIL_DIVERGENCE
    retestTrigger: notification policy, host, or ancestor context changes
    disposition: revise
Expectation-first verification cases. Every runtime observation is intentionally UNOBSERVED.

The tooltip case is especially revealing. A global tooltip token may be unnecessary if the tooltip only needs to outrank content inside its owning dropdown. If the tooltip mounts elsewhere, however, a local value may no longer describe the relevant relationship. The contract must name the mounting assumption instead of relying on the component name.

Debug the first divergence, not the largest number

When the rendered order is wrong, compare the expected chain with the actual chain from authority to screen. Stop at the first mismatch. That layer owns the first correction attempt.

  1. 1

    Check the source contract

    Is the intended relationship explicit, current, approved, and assigned to the right authority? If the contract itself is ambiguous, changing CSS is premature.

  2. 2

    Check the generated artifact

    Did the expected source version emit both identifiers and values? A stale or incomplete artifact is a generation or distribution failure.

  3. 3

    Check the project mapping

    Does the consuming project map each semantic role to the intended emitted value? Look for swapped aliases, copied literals, stale imports, and mode-specific overrides.

  4. 4

    Check the component contract

    Does the component consume the project role at the correct host? Separate its application-level host from the internal ordering of the backdrop, panel, arrow, or control.

  5. 5

    Check approved exceptions

    Is a documented integration or surface exception intentionally replacing the shared policy? Confirm its scope, owner, and retest trigger.

  6. 6

    Inspect the rendered consumer

    Record the mounting target, relevant ancestors, scroll container, positioned elements, and stacking contexts. If two elements are in different contexts, compare the contexts rather than their descendant values.

  7. 7

    Change one hypothesis and retest

    Raise or lower a value only when the evidence shows that the numeric relationship inside the relevant context is wrong. Otherwise, correct the mapping, host, component contract, exception, or context boundary.

A z-index token can govern an intended relationship, but it cannot guarantee that two rendered elements participate in a relationship where their values can compete.

Value escalation should remain a hypothesis. If a tooltip is trapped inside a lower stacking context, changing it from 100 to 100000 only changes its position among peers in that context. The higher context still wins. The useful finding is the first divergent boundary, not the biggest value in the stylesheet.

Migrate arbitrary values without a global renumbering

Don't begin a migration by sorting every integer and assigning a token name. That approach preserves accidental relationships and promotes component internals into global policy. Inventory the relationships first, then migrate one accepted relationship at a time.

  1. 1

    Inventory consumers and relationships

    For each literal z-index, record the named consumer, what it must appear above and below, its mounting location, and the owner who can confirm the rule.

  2. 2

    Mark context boundaries

    Group values that actually compare within the same stacking context. Don't infer a global order from integers found in unrelated contexts.

  3. 3

    Preserve intentional local ordering

    Keep backdrop-to-panel, trigger-to-popup, and similar internal relationships in their component contracts when no external consumer depends on them.

  4. 4

    Classify each candidate

    Choose shared semantic role, component-local order, approved exception, or unresolved rule. Reject unused placeholder layers.

  5. 5

    Map one named relationship

    Introduce the source role, emitted value, project alias, and component consumption path for one relationship. Don't renumber unrelated values to make the scale look tidy.

  6. 6

    Run the affected verification cases

    Record the expected order before executing. Capture observations, the first divergent layer, correction owner, retest trigger, and accept, revise, or block disposition.

  7. 7

    Remove a literal only after its consumer moves

    The existence of a new token doesn't prove that the old value is unused. Trace references and retain a visible migration state until the named consumer is verified.

Leave room without giving it meaning

Numeric gaps can make later value changes convenient, but gaps aren't semantic roles. Add a role only when a current relationship and named consumer justify it.

Where Identity Forge stops

This topic is adjacent to Identity Forge's shipped design-token and implementation-ready design-system coverage. Identity Forge can supply upstream design-kit artifacts such as semantic color tokens, typography, spacing, layout guidance, DESIGN.md, and supported exports. The frozen evidence for this article doesn't establish a shipped Identity Forge z-index token or z-index export.

The consuming project owns its layer policy, numeric mappings, component behavior, exceptions, stacking assumptions, runtime tests, and acceptance decision. Don't attribute the illustrative identifiers, values, or unexecuted results in this guide to Identity Forge.

Make one relationship verifiable today

Choose the overlay relationship your product reuses most often. Name its authority, two real consumers, expected order, mounting assumptions, and retest trigger. Then execute one consumer case and trace the first divergence before adding another shared layer token.

Sources