Skip to content
Domphy

Spacing System

themeSpacing(n) — the base scale

themeSpacing(n) returns n/4 em (scales with font size — 4px steps at 16px base):

import { themeSpacing } from "@domphy/theme"

themeSpacing(1)   // "calc(0.25em)" = 4px at 16px base
themeSpacing(2)   // "calc(0.5em)"  = 8px
themeSpacing(3)   // "calc(0.75em)" = 12px
themeSpacing(4)   // "calc(1em)"    = 16px
themeSpacing(5)   // "calc(1.25em)" = 20px
themeSpacing(6)   // "calc(1.5em)"  = 24px
themeSpacing(8)   // "calc(2em)"    = 32px
themeSpacing(12)  // "calc(3em)"    = 48px
themeSpacing(16)  // "calc(4em)"    = 64px

Never hardcode spacing literals ("16px", "1.5rem") — @domphy/doctor flags those as raw-spacing-value.

Density-aware spacing

For bounded controls (buttons, inputs, badges), multiply by themeDensity(l) so the component scales with the user's density preference:

import { themeSpacing, themeDensity } from "@domphy/theme"

const Button = {
  button: "Click me",
  style: {
    paddingBlock:  (l) => themeSpacing(themeDensity(l) * 1),  // 4px × density
    paddingInline: (l) => themeSpacing(themeDensity(l) * 3),  // 12px × density
    borderRadius:  (l) => themeSpacing(themeDensity(l) * 1.5),
  },
}

Density scale (dataDensity, clamped into 5 steps [0.75, 1, 1.5, 2, 2.5]):

DensityMultiplierTypical use
decrease-4 / decrease-3 / decrease-20.75Ultra-compact (data grids) — clamps to the same floor
decrease-11Compact
inherit (also decrease-0 / increase-0)1.5Default
increase-12Comfortable / touch targets
increase-2 / increase-3 / increase-42.5Accessible large — clamps to the same ceiling
const CompactTable = {
  div: TableContent,
  dataDensity: "decrease-2",   // all descendants inherit compact density
}

Layout vs component spacing

UseRule
Component internal padding (button, input, card)themeSpacing(density * n)
Gap between components in a layoutthemeSpacing(n) (no density)
Section/page paddingthemeSpacing(n) (no density)
Between tightly-related elementsthemeSpacing(1–2)
Between loosely-related sectionsthemeSpacing(4–8)

The reason: density should affect UI chrome (button size, input height) but NOT the page's structural rhythm. If density affected both, a "compact" page would have a broken layout grid.

Common spacing values

SpacingReturn valuepx at 16px baseUse
themeSpacing(0.5)"calc(0.125em)"2pxHairline gap between tightly stacked items
themeSpacing(1)"calc(0.25em)"4pxIcon-to-label gap, tight list gap
themeSpacing(2)"calc(0.5em)"8pxInternal button padding, small component gap
themeSpacing(3)"calc(0.75em)"12pxMedium gap, form label-to-input
themeSpacing(4)"calc(1em)"16pxDefault content padding, card padding
themeSpacing(6)"calc(1.5em)"24pxSection padding, between cards
themeSpacing(8)"calc(2em)"32pxBetween sections
themeSpacing(12)"calc(3em)"48pxLarge section breaks

Fluid spacing with themeFluidSpacing

For structural spacing — page padding, section gaps — that should grow with the viewport, use themeFluidSpacing(min, max). It returns a CSS clamp() that scales linearly between themeSpacing(min) and themeSpacing(max) across the standard 320 px → 1280 px viewport range:

import { themeFluidSpacing } from "@domphy/theme"

const Section = {
  section: Content,
  style: {
    padding: themeFluidSpacing(4, 16),    // 1em at 320px → 4em at 1280px
    gap: themeFluidSpacing(4, 8),         // 1em at 320px → 2em at 1280px
  },
}

Custom viewport range:

// Scale between themeSpacing(2) and themeSpacing(12) from 480px to 1440px
padding: themeFluidSpacing(2, 12, 480, 1440)

When to use fluid vs fixed spacing:

SituationUse
Page/section paddingthemeFluidSpacing(min, max) — grows with viewport
Gap between layout sectionsthemeFluidSpacing(min, max)
Component internal padding (button, input)themeSpacing(density * n) — density-aware, not fluid
Icon-to-label gap, tight list gapthemeSpacing(n) — fixed, no density or fluid

Do not use themeFluidSpacing for bounded-control padding — density already handles responsive sizing for controls.

Using spacing in grid/flex layouts

const CardGrid = {
  div: (l) => cards.get(l).map((card) => ({ div: card.title, _key: card.id })),
  style: {
    display: "grid",
    gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))",
    gap: themeSpacing(4),   // 16px gap between cards
    padding: themeSpacing(6),   // 24px page padding
  },
}

Padding shorthand

// All sides
padding: themeSpacing(4)

// Block and inline separately
paddingBlock: themeSpacing(2),
paddingInline: themeSpacing(4),

// Individual sides
paddingTop: themeSpacing(2),
paddingRight: themeSpacing(4),
paddingBottom: themeSpacing(2),
paddingLeft: themeSpacing(4),

Domphy's doctor watches these properties: margin, padding, gap, rowGap, columnGap and all their longhand variants.

Border radius

Follow the bounded control pattern:

// Tight radius (small components)
borderRadius: (l) => themeSpacing(themeDensity(l) * 0.5),

// Medium radius (cards, modals)
borderRadius: themeSpacing(2),   // 8px (no density — structural, not control)

// Large radius (pills, chips)
borderRadius: "9999px",   // exception: fully-round is categorical, not a spacing value

Spacing in scroll containers

const ScrollArea = {
  div: Content,
  style: {
    overflowY: "auto",
    padding: themeSpacing(4),
    // Prevent content from touching scrollbar
    paddingRight: themeSpacing(6),
  },
}

inset shorthand

For overlays and sticky elements:

const Overlay = {
  div: null,
  style: {
    position: "fixed",
    inset: 0,   // shorthand for top/right/bottom/left: 0 (no spacing needed)
    background: "rgba(0,0,0,0.5)",
  },
}