Density is a mode, not a scale factor
A density mode changes how much information and interaction fit into a working area, so compact and comfortable modes may resolve the same semantic role to different values. Atlassian describes compact and comfortable views as possible non-color themes. The SAS implementation shows why the practical work reaches several property families rather than a single spacing number.
The tempting shortcut is a global multiplier: take every numeric value and apply 0.8 for compact mode. It's easy to describe and hard to defend. Reducing a row gap may improve scanning in a data table, but reducing a focus outline may weaken a critical state. A smaller font can change readability and wrapping. A smaller hit area can affect operation. The values are all numeric, but they do different jobs.
Do not make density a universal transform
A global multiplier hides ownership and couples decisions that need different constraints, evidence, and release criteria. Route each property separately.
Before naming density tokens, write down which layer controls each kind of decision. Otherwise, a source token, component override, media query, and product preference can compete without anyone noticing.
- Source values define reusable primitives and approved semantic roles.
- Density aliases select values for compact or comfortable presentation.
- Component contracts map those roles to named parts, variants, and states.
- Application policy selects a mode, stores any preference, and decides where the mode applies.
- Controlled exceptions document deliberate departures for a named component or surface.
- Consumer evidence records what a particular application resolved, emitted, mapped, and rendered.
These layers are related, but none proves the next. A valid density alias doesn't prove that a build emitted it. An emitted variable doesn't prove that a component uses it. Even a correct component mapping doesn't prove that the product selected the intended mode or rendered it correctly.
Use a density-ownership matrix
For each candidate property, ask four questions: who owns it, what variation is allowed, what must remain fixed, and what evidence is required? The matrix below is a starting contract, not a universal set of values.
| Governing layer | Permitted density variation | Fixed constraint or separate owner | Required evidence | |
|---|---|---|---|---|
| Internal spacing | Semantic density role, then component mapping | Padding and gaps inside repeated controls or cells | Content order and required affordances | Resolved values plus representative component renders |
| Control dimensions | Component contract backed by density roles | Height and internal padding where the control remains operable | Supported variants, states, and any minimum requirement set by the team | Default, focus, disabled, error, and content-extreme checks |
| Table and list rows | Semantic row role with component mapping | Row height, cell padding, and repeated vertical gaps | Data hierarchy, selection behavior, and status meaning | Scanning task in one named table or list consumer |
| Layout gaps | Layout contract or semantic density role | Local gaps where density is the controlling condition | Viewport transitions and container behavior | Checks on both sides of every relevant layout boundary |
| Typography | Typography role plus explicit density decision | Line height or role mapping only when approved for that role | Script coverage, locale needs, font availability, and product readability constraints | Wrapping, clipping, fallback, zoom, and locale observations |
| Icons | Component or icon contract | Size only when the icon's role supports it | Meaning, stroke legibility, alignment, and accessible name policy | Representative icon controls at each density |
| Hit areas | Component and application policy | Visible geometry may change independently from the operable area | The team's accessibility and device-input requirements | Pointer and keyboard operation in named consumers |
| Focus treatment | Semantic focus roles plus component and selector policy | Offset only if the complete focus contract remains valid | Visibility, contrast policy, forced-colors behavior, and state meaning | Keyboard checks across controls, themes, and density modes |
| Radius and borders | Stable semantic or component roles by default | Change only when density has an explicit visual requirement | Brand shape language and state communication | Controlled comparison showing the intended affected surfaces |
| Behavior | Application and component policy | Usually none | Sorting, validation, disclosure, navigation, permissions, and side effects | Functional tests separate from visual density checks |
Start narrow. Let density affect repeated spatial relationships and selected component dimensions. Require a specific reason before changing typography, icon geometry, focus treatment, or operable areas. Keep radius and behavior fixed unless the product has a named requirement that says otherwise.
Compose density with other axes
Theme, density, locale, and viewport behavior are independent axes. Light versus dark changes color relationships, while compact versus comfortable changes selected spatial decisions. Locale may change text, script, direction, font support, or wrapping. Viewport and container conditions may change layout. Combining all of them into one mode produces an expanding list of combinations and makes conflicts harder to locate.
| Primary question | Typical owner | Fallback | |
|---|---|---|---|
| Theme | Which visual values fit the active color scheme? | Theme policy and semantic color mappings | Declared default theme |
| Density | How much space should selected repeated UI consume? | Application preference plus density aliases | Comfortable unless the product declares another default |
| Locale | What language, script, direction, and content conditions apply? | Localization and typography policy | Declared product locale and supported font path |
| Viewport or container | How should structure adapt to available space? | Layout and component CSS | Base layout outside defined conditions |
| Component exception | Why may one named consumer depart from the shared mapping? | Versioned component or product exception | Shared contract when the exception is absent or expired |
Set precedence before implementation
Precedence should describe responsibility, not merely file order. One practical rule is to apply fixed constraints first, followed by active axis mappings, component contracts, and approved exceptions. Application policy selects the active axes. Viewport rules may change layout, but they shouldn't silently select density unless that coupling is an explicit product requirement.
- Apply non-negotiable product and accessibility constraints.
- Resolve theme, density, and locale independently from their declared defaults.
- Apply the component's supported mapping for the resolved roles.
- Apply a versioned exception only to its named target and conditions.
- Record unsupported or unresolved combinations instead of inventing a fallback in the consumer.
Responsive does not mean compact
A narrow viewport may need larger, easier-to-operate controls, while a wide data workstation may benefit from compact rows. Couple viewport and density only when a named requirement supports the relationship.
Write an explicit multi-axis contract
The following contract is illustrative and unexecuted. It isn't an Identity Forge export, a DTCG conformance example, or evidence that these values work for a product. It shows where decisions and unresolved fields belong.
contract: density-v0-proposal
status: illustrative-unexecuted
axes:
theme:
supported: [light, dark]
default: light
density:
supported: [comfortable, compact]
default: comfortable
locale:
supported: [en, de]
default: en
viewport:
owner: application-layout
selectsDensity: false
roles:
space.control.inline:
comfortable: 12px
compact: 8px
space.control.block:
comfortable: 10px
compact: 6px
size.row.standard:
comfortable: 44px
compact: 36px
fixed:
typography.body.family: source-typography-role
focus.indicator.contract: shared-focus-contract
behavior.table.sort: application-policy
exceptions:
- id: orders-table-touch-target
target: OperationsConsole.OrdersTable.SelectionControl
reason: unresolved
owner: unassigned
expires: unresolved
unsupported:
- compact typography remapping by locale
- viewport-selected density
fallback:
unknownDensity: comfortable
missingDensityRole: fail-review-no-local-guessThe sample pixel values aren't the point. What matters is the separation among roles, fixed decisions, exceptions, unsupported combinations, and fallback. A consumer that encounters an unknown density can use the declared default. A missing required role should remain visible as an integration problem instead of being replaced by a local guess.
Use bounded fallback rules
- Unknown density selection: use the declared default and record the invalid input.
- Missing optional density mapping: inherit the documented base role only if the contract permits it.
- Missing required component role: block that consumer's migration or keep it on the legacy mapping.
- Unsupported axis combination: keep the last supported combination or disable the new mode for that scope, according to application policy.
- Expired exception: don't silently renew it; review whether the shared contract now covers the need.
Inspect a real upstream token system
Ambient Sage provides a published typography, spacing, color, and export baseline. Use it as source intake, then define and verify density behavior in the consuming application.
Control component exceptions
A shared density role won't fit every component, but that doesn't justify hidden local values. Record an exception when a named component needs independent change authority, such as a touch-oriented selection control inside an otherwise compact table.
Every exception should name its target, reason, owner, permitted modes, protected surfaces, review date or removal condition, and verification cases. It should also state whether it replaces a semantic role, adds a component alias, or changes only application behavior. If those fields are unknown, the exception remains a proposal.
Protect unrelated consumers
For every exception, name at least one surface that must not change. This turns a vague override into a bounded contract you can test.
Migrate without pretending delivery is adoption
Move one high-use component at a time. A compatibility alias can keep unmigrated consumers working, but it needs a removal gate. The goal isn't to make the old name disappear quickly. It's to prove that named consumers no longer depend on it.
- 1
Freeze the current behavior
Before changing token names, record current values, component mappings, supported states, named consumers, local overrides, and known exceptions.
- 2
Add semantic density roles
Create comfortable and compact mappings for the approved property set. Keep primitives stable unless a separate source decision changes them.
- 3
Preserve a compatibility route
Keep the legacy token or value mapped to the comfortable role when that matches existing behavior. Mark the route deprecated, not removed.
- 4
Migrate one named consumer
Update one representative table, form, or navigation surface. Record the exact component and project mapping instead of saying the application is migrated.
- 5
Verify both modes and adjacent axes
Check representative states in comfortable and compact modes, then repeat the relevant cases in light and dark themes, supported locales, and viewport conditions.
- 6
Set rollback triggers
Roll back the consumer mapping if required states fail, content becomes unusable, the wrong artifact resolves, or an unapproved exception is needed.
- 7
Remove compatibility only after evidence closes
Remove the old route when dependency search, generated artifacts, project mappings, and named-consumer observations show that no supported consumer needs it.
Use Ambient Sage as bounded upstream intake
Ambient Sage is a published Identity Forge kit with a compact-product typography scale. Its public typography uses Plus Jakarta Sans for body and heading roles at weights 400, 500, 600, and 700. JetBrains Mono covers the mono role at weights 400, 500, and 700. Identity Forge makes design-kit artifacts available through formats that include DESIGN.md, DTCG, Tailwind, CSS, and shadcn routes.
This evidence establishes the available upstream decisions and artifacts. It doesn't establish comfortable and compact variants, application mode selection, component mappings, locale behavior, accessibility results, or rendered observations. Those fields stay unresolved until a consuming team defines and checks them.
Token specimen · real values
Ambient Sage
Live renderAmbient Sage's actual tokens — the same values its exports use.
Verify one named consumer
Use a record that keeps expectations and observations together without confusing them. The example below names the Operations Console Orders Table, but it remains unexecuted. Replace every placeholder with evidence from the actual consumer.
record: density-acceptance-001
status: unexecuted
consumer:
product: Operations Console
surface: Orders Table
component: DataTable
source:
contractVersion: unresolved
densitySelection: [comfortable, compact]
sourceValues: unresolved
resolution:
resolvedAliases: unresolved
emittedArtifact: unresolved
artifactVersionOrHash: unresolved
projectMapping:
packageOrFile: unresolved
semanticToComponentMap: unresolved
exceptions:
approved: []
proposed: []
representativeCases:
- table/default/light/en/wide
- table/selected/dark/en/wide
- table/loading/light/en/narrow
- table/error/dark/de/wide
- table/long-content/light/de/narrow
- selection-control/focus/light/en/wide
expectations:
comfortable: unresolved
compact: unresolved
fixedConstraints: unresolved
observations:
comfortable: not-run
compact: not-run
themeInteraction: not-run
localeInteraction: not-run
viewportInteraction: not-run
owner: unassigned
rollbackTrigger: unresolved
disposition: pending-evidenceA useful record captures the source version, active density, resolved values, emitted artifact, project mapping, component contract, exceptions, representative cases, expectations, observations, owner, rollback trigger, and disposition. It should cover the tables, forms, navigation, content extremes, component states, themes, locales, and viewport conditions that matter to the named consumer.
Don't promote a source value into an observed result. If the compact row token resolves to 36px in the source, the record can state that source fact. It can't claim that the Orders Table rendered at 36px until someone has inspected the emitted artifact, project mapping, computed result, and rendered consumer.
Find the first divergent layer
When the result is wrong, compare expected and observed evidence in ownership order. Fix the first layer that diverges. A later override may explain the screenshot, but changing it can conceal an earlier source or transformation defect.
- 1
Check the governing source
Does the approved contract contain the expected role, mode value, fixed constraint, and version?
- 2
Check alias resolution
Does the active density resolve the semantic role to the intended value without a missing target, cycle, or unintended fallback?
- 3
Check transformation and emission
Did the pipeline preserve the role and value in the artifact consumed by this project?
- 4
Check the project mapping
Is the project loading the intended artifact version and mapping the semantic role to the expected component property?
- 5
Check the component contract
Does the component support the relevant mode, variant, state, and content condition?
- 6
Check approved exceptions
Is a bounded, current exception intentionally changing this consumer?
- 7
Check the rendered consumer
Under the recorded theme, locale, viewport, content, and input conditions, does the computed and visible result match the expectation?
Accessibility needs its own evidence
This workflow can expose density-sensitive risks and preserve constraints, but it doesn't certify accessibility. Test the shared component and representative product consumers under the conditions required by your accessibility scope.
Start with one high-use component
Choose one component where density has a clear user benefit, such as a frequently used table. Route each property through the ownership matrix and write the comfortable and compact expectations. Declare the other axes and fallback, then trace both modes from source to the rendered consumer. Expand the system only after that record shows which decisions transferred cleanly and which need a different owner.
Sources
- Refactoring Design System Tokens for a Multidimensional Future: The SAS case study documents density work across spacing, dimensions, typography, icons, responsiveness, accessibility, theme, and language, including a concrete file order and a locale-related typography conflict.
- Design tokens explained: Atlassian defines tokens as named design decisions, recommends names based on intended use, and identifies compact, comfortable, and cozy views as possible non-color themes.
- Introduction | Backbase Design System: Backbase documents primitive, semantic, and component token tiers and presents modes as a multi-dimensional theming mechanism.
- Design tokens | Cloudscape Design System: Cloudscape presents design tokens as abstractions of visual properties and documents structured naming and usage guidance for downstream consumers.
- Design Token Architecture | Joelle Gregory: The HPE case study records density and viewport as interacting dimensions within a broader problem of token-library structure, access, publication, and adoption.
- Ambient Sage Design Kit: The published Ambient Sage kit uses Plus Jakarta Sans for body and heading roles, JetBrains Mono for mono roles, and a compact-product typography scale; it is an upstream kit example rather than proof of density-mode implementation.
- Design token handoff checklist: prove the path from source to consumer: Identity Forge's handoff guide separates governing source, delivered artifact, project mapping, named consumers, and observed results instead of treating delivery as proof of adoption.