sigil/UI

Structural Grid Rhythm

How Sigil keeps sections, dividers, rails, and gutter ruler patterns phase-locked to full grid-cell intervals.

The Contract

Locked Sigil page layouts use one vertical structural period:

G = resolved pixel height of var(--s-grid-cell)

Every SigilSection boundary and every horizontal Divider boundary must land on a full multiple of G, measured from the top of the SigilPageGrid content column ([data-layout="sigil-content"]).

This is stricter than "looks close." The browser check is:

(sectionOrDividerEdge - contentOriginTop) % G === 0

Allow a tiny float epsilon when measuring through getBoundingClientRect(), but do not design around half-cell snap. Use rhythm="hairline" when a page should flow without full-cell structural boundaries.

Components In The System

ComponentResponsibility
SigilPageGridCreates the content origin with data-layout="sigil-content" and renders rail/gutter tracks.
SigilSectionMeasures its own border box and adds snap padding so its bottom edge lands on the next full cell.
SigilDivider / DividerOccupies exactly N * G layout height; its visual strokes do not add layout pixels.
SigilGutterPaints rail/ruler patterns from the shared page origin.

Full Cells, Not Half Cells

Some gutter patterns draw subdivisions inside a cell. That does not change the layout period. Sections and dividers still advance by full cells:

section bottom  = k * G
divider top     = k * G
divider bottom  = (k + N) * G

Do not snap section boundaries to G / 2. It can make arithmetic look cleaner for subdivided patterns while visually placing dividers between the major cells.

Divider Border Model

SigilDivider / Divider uses exact token heights:

SizeHeight
sm / xscalc(var(--s-grid-cell) / 2)
mdvar(--s-grid-cell)
lgcalc(2 * var(--s-grid-cell))
xlcalc(3 * var(--s-grid-cell))

Borders are visual strokes, not layout borders:

  • no real border-top / border-bottom
  • height remains exactly G
  • top stroke paints at -1px
  • bottom stroke paints inside the divider

This avoids cumulative +1px drift while keeping visible divider strokes aligned to rail lines.

Section Snap Model

SigilSection computes the base span before snap padding:

baseSpan = sectionBottom - contentOriginTop - previousSnapPadding
snap     = ceil(baseSpan / G) * G - baseSpan

The snap amount is split across top and bottom padding so inner content stays centered in the expanded section:

paddingTop    += floor(snap / 2)
paddingBottom += snap - floor(snap / 2)

The fractional remainder belongs on the bottom padding. If both halves are fractional, browser rasterization can introduce subpixel drift over long pages.

Authoring Rules

  • Use <Divider size="md" showBorders /> for one full structural band.
  • Avoid Divider size="sm" between sections unless intentionally breaking the major-cell rhythm.
  • Keep section block padding as whole-cell multiples, for example calc(2 * var(--s-grid-cell)).
  • Use fractional cell values (G / 2, G / 3, G / 4) only for interior component spacing, not for section or divider outer boundaries.
  • Do not add +1px to --s-divider-thickness-*.
  • Rebuild workspace package dist outputs when verifying apps/web; the local app imports built package entries.

Browser Verification

Run this against a local page before signing off any grid rhythm change:

node scripts/audit-grid-alignment.mjs --base=http://localhost:3000

The audit measures the resolved pixel value of --s-grid-cell in the browser, then checks every [data-slot="sigilsection"] and [data-slot="divider"] boundary against the full-cell rhythm.

For visual confirmation, also capture a screenshot at the target viewport and compare:

  • section bottom line
  • divider top line
  • divider bottom line
  • left/right gutter major cell lines

If a line is "one pixel low," check whether it is a real CSS border painting inside the box. The fix is usually visual stroke placement, not changing layout height.

Studiodefault
Presets
primary
secondary
background
surface
text
border
accent
success
warning
error
info
display
body
mono
heading wt
600
heading trk
-0.025em
base size
16px
page margin
24px
section pad
64px
card pad
24px
grid gap
24px
stack gap
12px
global
8px
button
8px
card
12px
input
6px
border w
1px
style
card border
card shadow
btn shadow
glow
spring
Type
Duration
0.20
Bounce
1.00
easing
cubic-bezier(0.16, 1, 0.3, 1)
fast
150ms
normal
200ms
slow
300ms
hover scale
1.02
press scale
0.98
hover lift
-1px
stagger
50ms
weight
transform
hover
active scale
0.98
min-width
0px
letter sp
0.000em
icon gap
8px
shadow
hover
border
shadow
padding
24px
title size
1px
title wt
desc size
0.875px
aspect
outline
height
36px
focus ring
2px
focus ring
h1 size
2.25px
h2 size
1.875px
h3 size
1.5px
h4 size
1.25px
weight
tracking
-0.020em
leading
1.20
pattern
pattern α
0.03
noise
gradient
grad angle
180°
height
50px
blur
12px
border
padding
24px
item gap
24px
min-height
600px
padding Y
80px
content-max
680px
layout
title size
56px
desc size
18px
padding Y
64px
max-width
600px
layout
title size
36px
padding Y
48px
columns
4
gap
36px
content-max
1200px
rail-gap
24px
grid-cell
50px
cross-stroke
1.5px
navbar-h
50px
bento-gap
16px
grid lines
dots
cell borders
cell bg
Gutter
Margin
content
hero
navbar
rail visible
enabled