Experience System
Foundation

Spacing

Spacing and sizing tokens, fixed and scalable tokens usage.

The Experience System is based on Tailwind’s spacing model. The theme sets a single base step:

@theme {
  --spacing: 0.25rem;
}
pnpm add @by/experience-system

That --spacing value is the unit Tailwind uses for numeric spacing utilities (p-4, gap-3, m-6, w-20, etc.): each step is a multiple of this base. In addition to Tailwind utility classes the --spacing() function can be used to retrieve spacing values in CSS. For example:

.my-element {
  margin: --spacing(4);
}

So default (non-scaled) spacing = ordinary Tailwind utilities whose resolved size is fixed for a given theme; it does not automatically change when the app switches “density.”

Density and scaling factor

Beside of standard Tailwind spacing tokens Fractal experience system provides a set of scalable spacing tokens to address density modes switching. Spacing density is controlled by data-ds-spacing (set by ThemeProvider’s spacing prop). It defines --scale-density variable that is used to calculate scalable spacings.

There are 3 density modes:

  • standard — full density (--scale-density: 1).
  • compact — layout spacing tightens to 87.5% of the same token (--scale-density: 0.875).
  • ultra-compact — layout spacing tightens to 75%, and typography scales down as well (--scale-density: 0.75).
[data-ds-spacing='standard'] {
  --scale-density: 1;
  --scale-typography: 1;
  --scale-line-height: 1;
}

[data-ds-spacing='compact'] {
  --scale-density: 0.875;
  --scale-typography: 1;
  --scale-line-height: 1;
}

[data-ds-spacing='ultra-compact'] {
  --scale-density: 0.75;
  --scale-typography: 0.875;
  --scale-line-height: 0.875;
}

Scaled spacing utilities (*-scaled-*)

The experience system adds utilities such as p-scaled-4, gap-scaled-6, px-scaled-3, size-scaled-8, etc. They apply the same numeric scale as normal Tailwind spacing, but multiply by --scale-density:

@utility p-scaled-* {
  padding: calc(var(--scale-density, 1) * --spacing(--value(number)));
  padding: calc(var(--scale-density, 1) * --spacing(--value([number])));
}

So for a given class (e.g. p-scaled-4), standard vs compact vs ultra-compact produces proportionally smaller padding/margin/gap without changing the class name.

Library components already use this pattern where internal rhythm should follow density (for example accordion trigger padding and avatar size use -scaled- utilities in the codebase).

When to use scaled vs default spacing

Prefer scaled for

  1. List, tree, and table items

    • Reduce vertical padding inside each row.
    • Decrease spacing between rows.
    • Slightly reduce supporting/icon sizes, but keep the overall row hit area at or near your accessibility minimum.
    • Cell padding: noticeably tighter in compact mode, especially for numeric or pivot tables where users value more rows over extra whitespace.

    Outcome: more rows on screen in compact mode without making individual rows fiddly to hit.

  2. Controls’ visual bodies (but not their hit areas)

    • Buttons, segmented controls, chips, toggles, etc. can visually compact:
    • Narrower padding inside buttons; slightly smaller icon size.
    • Tighter spacing between controls in toolbars and button groups.
    • For checkboxes/radios, keep the click/tap target large but reduce surrounding margins.

    Keyboard-heavy/pro environments often benefit from a more compact look while remaining usable with the mouse.

  3. Chrome and structural elements

    • These can scale quite a bit without directly affecting interaction safety:
    • Page header, toolbars, tab bars: reduce height and padding, as they are used frequently and benefit from fitting more actions on a single line.
    • Navigation rails, sidebars: slightly reduced padding, item spacing, and icon size.
    • Cards, panels, accordions: smaller internal padding and card gaps.
    • Tag/chip-like filters: smaller visual chips with tighter spacing, but preserved hover/click hit areas.

    Users perceive this as “more working area” with the same tools.

  4. Margins, gutters, and whitespace

    • Global layout spacing is one of the least risky knobs:
    • Reduce margins around content regions.
    • Tighten gaps in grid layouts and between sections.
    • Keep a minimum whitespace baseline to avoid a cluttered look, but bring everything a bit closer together.

What shouldn’t be scaled

Here are categories where aggressive scaling tends to hurt UX more than help.

  1. Minimum interactive hit areas

    • Accessibility minimums per platform shouldn’t be violated:
    • Touch: ~44–48 logical pixels square.
    • Desktop: could be more dense, but click targets should be comfortable. WCAG AA minimum for desktop is 24px.
  2. Primary typography

    • Key identifiers: IDs, names, statuses, and key numeric values should not drop below main body text size. If space is tight, prefer truncation with tooltip/hover detail over shrinking font sizes.
    • Error and validation text: these should remain clearly readable; density should never make error states easy to miss.

    You can slightly reduce line height and paragraph spacing in compact mode, but preserve legibility and scan-ability. Headings may shrink a bit, but keep your typographic hierarchy intact.

    Density is not the same thing as a “small font mode”; users who want smaller/larger fonts usually expect a separate setting.

  3. High-risk actions

    • Preserve interaction safety for high-risk actions:
    • Destructive buttons: these should not be shrunk so much that users mis-hit them. Often they keep more padding and a distinct placement (e.g., separated from dense inline action icons).
    • Inline actions (edit/delete/etc.) in dense rows: keep sufficient spacing between them to avoid mis-clicks, even in compact tables.
  4. Key flow elements

    • Wizard steps, important forms, and onboarding flows should remain relatively comfortable. Density controls are mainly for “workhorse” views: dashboards, grids, logs, queues, etc.
    • Admin or configuration screens with heavy explanation text shouldn’t be compressed to the point where labels wrap awkwardly and layout becomes noisy.

    If you change the representation (card → row), that’s a component swap, not just density scaling.

  5. Brand-critical elements and icons

    • Density modes shouldn’t shrink these below their minimum.
    • Icons can scale somewhat, but once they reach the smallest size (16px) where details are still clear, hold them constant and only adjust spacing around them.
  6. Information-heavy components where legibility is paramount

    • Charts, dense tables with lots of numeric data, complex diagrams:
    • If shrinking fonts or cell height makes scanning hard or increases misreads, don’t scale these aggressively.
    • Instead, consider alternative patterns: horizontal scrolling, column pinning, column visibility toggles, or zoom/fit controls.

Custom scaling

Tailwind custom variants let you spell different utilities per density (e.g. standard:p-6 compact:p-3) instead of one scaled utility.

Use this when:

  • Design needs non-uniform changes between modes (not a single multiply factor).
  • Specific regions need different discrete steps than *-scaled-* would produce.
  • Apps can also define semantic tokens that vary by [data-ds-spacing] (as in the “Defining New Tokens” doc), e.g. different --spacing-card-padding per mode — useful for named spacing that is not just “token × scale.”