Foundations

Spacing

Comitor uses a base-4 spacing scale (1 unit = 4 px). All space values in UI must come from this scale — no arbitrary pixel values. The base itself is Tailwind's --spacing: 0.25rem, which the package leaves alone on purpose.

Spacing scale

1
4pxInline icon gaps, tight badges
1.5
6pxMenu and sidebar item padding — the default --menu-item-py
2
8pxInternal component padding (sm)
3
12pxTable row and list item padding — the default --row-py
4
16pxPageContainer gutter on mobile, card padding (compact)
5
20pxComposite padding — StatCard and FormSection
6
24pxCard padding (default), PageContainer gutter from md up
8
32pxSection gap, page gutter
10
40pxLarge card padding; --table-head-h at the default density
12
48pxSection padding (mobile)
16
64pxSection padding (desktop)
24
96pxHero top padding
32
128pxHero bottom padding

Bars are drawn at 2× for legibility. Because the scale is calc(var(--spacing) * n) in rem, every value here scales with the text-size axis — including icon sizes, which are drawn from this scale rather than the type scale.

Corner radius

One variable, --radius: 6px, and three steps derived from it. Six pixels is the visual signature of the system. It is written in px, not rem, and that is the point: every rem value follows the text-size axis, so a rem radius would become 6.75px at the large step and the signature would drift with a user preference. rounded-xl is the one step still declared in rem.

rounded-sm4px
rounded-md6px
rounded-lg10px
rounded-xl12px
UtilityDeclared asAt defaultUsage
rounded-smcalc(var(--radius) - 2px)4pxNested chips, small inner surfaces
rounded-md--radius6pxThe default. Buttons, inputs, badges, menu items
rounded-lgcalc(var(--radius) + 4px)10pxCards, popovers, dialogs
rounded-xl0.75rem12pxLarge panels and hero surfaces

Elevation

Four steps, low to high. The raw values live as --elevation-* CSS variables rather than inside @theme, because a shadow has to change between light and dark at run time. In dark mode the geometry is unchanged but the ink shadow is replaced by pure black at a much higher alpha — on #0E1116 an ink shadow is simply invisible, and a dialog with no shadow reads as pasted flat onto the page.

shadow-surface

A plane at rest, where the border is still the main signal — Card by default

shadow-control

An interactive control lifted slightly off the page — solid-fill Button

shadow-card

A floating card or panel — StatCard, Toast, Card variant elevated, chart tooltip

shadow-overlay

A layer over the content — Dialog, Sheet, Popover, menus, Select, Tooltip

UtilityVariableLightDark
shadow-surface--elevation-surface0 1px 2px rgba(14,17,22,.04)0 1px 2px rgba(0,0,0,.32)
shadow-control--elevation-control0 1px 2px rgba(14,17,22,.08)0 1px 2px rgba(0,0,0,.4)
shadow-card--elevation-card0 1px 2px rgba(14,17,22,.04), 0 8px 24px rgba(14,17,22,.06)0 1px 2px rgba(0,0,0,.32), 0 8px 24px rgba(0,0,0,.4)
shadow-overlay--elevation-overlay0 8px 32px rgba(14,17,22,.16)0 8px 32px rgba(0,0,0,.64)

To restyle elevation across an app, override the four --elevation-* variables — never --shadow-*. One consequence of holding the value as a var(): Tailwind cannot isolate the colour component, so shadow-card/50 and colour modifiers such as shadow-red have no effect.

App frame measurements

Three measurements are published into the spacing namespace so the shell layer never has to write a magic number.

VariableUtilityValueWhat it measures
--spacing-headerh-header3.5rem · 56pxApp header height. Mirrored as layout.headerHeight in @comitor/ui/tokens.
--spacing-sidebarw-sidebar16rem · 256pxSidebar width when expanded. layout.sidebarWidth.
--spacing-sidebar-iconw-sidebar-icon3.5rem · 56pxSidebar width when collapsed to icons. layout.sidebarCollapsedWidth.

All three are declared in rem, so they follow the text-size axis. The px figures mirrored in @comitor/ui/tokens are the values at the default step — use them for canvas and PDF work, not to lay out the DOM.

Density is a user setting

Compact layout is one of the four display axes, switched by <html data-density="compact">. It overrides exactly five tokens and touches nothing else. Note what it does not do: it does not override the global --spacing. That would be one line, but it would also shrink horizontal padding and drag every size-4 icon down to about 12.8px. Compact means more rows on screen — not text pressed against an edge — so the axis is vertical only.

VariableComfortableCompactWhat it pads
--row-py0.75rem · 12px0.375rem · 6pxVertical padding of a table cell
--menu-item-py0.375rem · 6px0.25rem · 4pxVertical padding of a menu item
--list-item-py0.75rem · 12px0.5rem · 8pxVertical padding of a list item
--nav-item-py0.375rem · 6px0.25rem · 4pxVertical padding of a sidebar item
--table-head-h2.5rem · 40px2rem · 32pxHeight of a table header row
tsx
/* Consume a density token the way the package does — as a Tailwind
   arbitrary-property value, not as a hardcoded py-3. */
<td className="py-(--row-py)">…</td>
<div className="py-(--list-item-py)">…</div>
settings page
'use client'

// The density axis is wired once with <DensityProvider> in the root layout.
// Default labels are Vietnamese, so pass English ones explicitly.
import { DensityToggle } from '@comitor/ui/shell'

export function DisplaySettings() {
  return (
    <DensityToggle
      label="Compact layout"
      description="Tightens table rows, menus and list items."
    />
  )
}

Page layout

Page gutters and content width come from PageContainer rather than from a per-page max-w-… px-…. If every page writes its own, the left margin of two apps in the ecosystem ends up a few pixels apart — hard to see, and enough to make the whole system look loosely assembled.

tsx
import { PageContainer } from '@comitor/ui'

// Every variant renders mx-auto w-full px-4 md:px-6 py-4 md:py-6,
// plus one of four max widths:
//   sm   max-w-2xl    42rem — settings pages, one-column forms
//   md   max-w-5xl    64rem — the default, detail pages
//   lg   max-w-[90rem]      — wide multi-column tables
//   full max-w-none          — full-bleed tables, kanban boards
<PageContainer width="lg" as="main">…</PageContainer>

// flush drops the vertical padding when the page paces itself.
<PageContainer width="full" flush>…</PageContainer>

Breakpoints

Mobile:768px
Tablet:1024px
Desktop:1280px

Mirrored as layout.breakpoints in @comitor/ui/tokens for code that has to branch in JS.

Touch targets

Minimum:44px
Token:layout.minTapTarget

WCAG 2.5.5. Compact density shortens rows but must never take an interactive target below this on a touch surface.