Centinadocs

Grid visual editor — reference

The CSS grid editing surface. Two synchronized surfaces edit the same grid: the inspector renders template strings as structured track rows, and the canvas makes the selected grid box directly manipulable — draggable track boundaries, gap pills, per-track chips. Underneath, a grid's templates are still plain CSS strings — everything here is a view over them, so anything CSS grid can express survives round-trips through the editor.

Try it: draw a box (B), switch Display to Grid. The inspector shows Columns/Rows as rows; the canvas shows chips above/left of the box and resize strips on every interior boundary.

User-facing tour with screenshots: the grid & subgrid tour — what each part of the chrome and inspector does.

The two surfaces

Inspector

  • Track lists — one list per axis. Header: axis label, a scrubbable count field (1–100; growing appends copies of the last track, shrinking drops from the end), a code toggle (raw CSS text mode), and +. Each track row is a scrubbable number + unit menu (fr / px / % / auto / ) plus a hover-revealed remove button (disabled, not hidden, at one track).
    • Unit switches are extent-preserving: with a single node selected the control converts through the track's resolved px, so the rendered layout doesn't jump — only the notation changes.
    • auto disables the field and shows the resolved px grayed.
    • (custom) flips the row to free CSS text — this is how you type minmax(40px, 1fr) into one track. Values that could be expressed as plain numbers stay text until you switch them back; nothing is lost.
  • Alignment — the 3×3 marker writes justify-items + align-items in one entry; two standalone per-axis toggles carry stretch (the CSS default). Content distribution is one "Distribute" row of paired selects; a section note appears when a non-start/stretch value gates canvas dragging (see the per-axis canvas gate).
  • Placement — shown when every selected node's parent is a grid box. The area picker renders the shared parent's explicit tracks as a mini-grid (≤12 tracks per axis): click writes a single cell, drag paints a rect ("2 / 5" — CSS line numbers, end-exclusive), one history entry per gesture; the header minus clears to auto flow. Spans, negative and named lines don't highlight in the picker but work through the raw grid-column/grid-row inputs kept below.
  • Item alignment with a scope toggle (Placement section) — by default the Align X / Align Y rows edit the parent grid's justify-items/align-items (the whole grid — how plain CSS aligns things: on the container). The row-end toggle switches them to Self X / Self Y — per-item justify-self/align-self overrides with a dashed auto segment. Toggling back off clears the overrides ("Follow grid alignment", one entry). CSS grid has no per-row/column alignment property, so "align the whole second column to the bottom" is: multi-select that column's children, toggle to Self, set Y once (writes fan out; one history entry). Items that already carry overrides open in override mode, so state is never hidden.
  • Subgrid — when a grid box itself sits in a grid, each axis header grows a subgrid toggle. On: the axis template becomes subgrid — the box adopts the parent's tracks for perfectly aligned nesting. Off: the axis gets one 1fr per track it was rendering, so the layout doesn't jump. While subgridded, the track rows are replaced by a card ("Columns come from the parent grid") with an Edit parent jump; count and + disappear (the tracks aren't this box's to edit); the code toggle stays as the raw-text escape hatch for line-name lists (subgrid [a] [b]).

Canvas

Chrome appears for the single selected grid box, or the grid parent of a single selected child — you never have to deselect your item to adjust the grid it sits in. Everything keeps a constant size on screen at any zoom.

  • Boundary strips — one per interior boundary, centered in the gap, a resize cursor and an accent line revealed on hover. Dragging resizes the adjacent track pair (math below) with a badge riding the boundary (1.39fr · 0.61fr). Boundaries whose neighbors aren't plainly numeric (minmax, fit-content, calc…) show not-allowed — edit those in the inspector.
  • Gap pills — at the top/left of each gap band; dragging scrubs the axis gap (one gap per axis — all bands move together). Gap creation from zero stays in the inspector: at gap 0 the boundary strip owns the space.
  • Track chips — authored sizes above each column / left of each row (1fr, 240px, mm for minmax, fit, for other CSS; full value in the tooltip). Hover reveals a remove ×; tracks too narrow skip their chip. A + chip past the last track adds a track inside the box's extent — adding never resizes the box: a flexible track absorbs the new one, rigid px/%/auto templates re-fit proportionally (autos pinning to px), and only a hug axis that can really grow grows — by content, as any added content would. The inspector's header + is the same operation on a different surface.
  • Cross-surface highlight — hovering a chip tints the track's full extent on canvas and lights the matching inspector row; hovering an inspector row tints the canvas track.
  • Subgrid adopted-track chrome — a subgrid axis renders the parent tracks it spans as its own chrome: dashed lines at the adopted interior boundaries and line numbers renumbered from 1 (what placement inside the subgrid counts). Dragging an adopted boundary forwards to the parent's template — same badge, undo, and Escape behavior as dragging on the parent itself. No chips, +, or gap pills on a subgridded axis — track structure belongs to the parent. A subgrid shrink-wrapped off the parent's track edges (self-alignment other than stretch) gets no adopted chrome rather than drifting chrome.

Interaction reference

Global rules first: every continuous gesture is exactly one undo step; Escape during a live drag cancels just the drag — the document reverts to the exact pre-drag state and the selection is kept; typed fields commit on Enter/blur and revert on Escape.

Canvas (single grid box selected, or a single child of one)

AffordanceTriggerEffectHistory / cancel
Boundary strip (invisible until hover; accent line; resize cursor)dragResizes the adjacent track pair with unit-aware math (fr redistributes, px transfers, auto pins to px); badge shows live values (1.4fr · 0.6fr)One step; Escape restores the exact template string
Boundary strip, not-⁠allowed cursorhoverTooltip: numeric tracks resize on canvas — minmax/fit-content/calc neighbors edit in the panel
Gap pill (at the top/left of each gap)dragScrubs the axis gap (one gap per axis — all bands move)One step; Escape restores
Track chip (size pill above a column / left of a row)hoverTints the track on canvas and lights its inspector row
Track chip × (revealed on chip hover)clickRemoves that track (hidden at one track; chips also hide on very narrow tracks)One step
+ chip (past the last column / below the last row)clickAdds a track without resizing the box: a flexible track absorbs the squeeze, rigid px/%/auto templates re-fit proportionally (autos pin to px), and only a hug axis that really grows grows — by contentOne step (template only)
select a child of a gridThe parent's chrome appears, but boundary strips shrink into the gaps (none at gap 0) so children stay clickable; the grid box itself gets full-height strips
Subgrid boundary strip (on a subgrid axis of the selected box)dragResizes the parent grid's adjacent track pair — same math, badge, and coalescing as dragging on the parent itselfOne step on the parent's template; Escape restores its exact string
Subgrid line numbers (accent badges, renumbered from 1)hoverTitle explains: placement inside this box counts from here

Canvas chrome appears per axis only when the template parses, no implicit tracks exist, and that axis's Distribute is start/stretch — a gated axis keeps passive guides and the inspector notes why (see the per-axis canvas gate).

Inspector — track lists (Columns / Rows)

AffordanceTriggerEffectHistory
Count fieldtype + Enter · ↑/↓ (Shift ×10) · drag to scrubSets track count: grows by duplicating the last track, shrinks from the end (1–100)Typed/stepped: one step each; scrub: one step total
Track row numberdrag to scrub (Shift ×10, Alt ×0.1) · ↑/↓ in the fieldScrubs the track's value in its own unit (fr steps by 0.1)Scrub: one step total
Unit menu (fr / px / % / auto / …)selectConverts through the track's resolved size so the layout doesn't jump (single selection; defaults otherwise). auto shows resolved px grayed; flips the row to free CSS text (minmax, calc…)One step (no-op if identical)
Row (revealed on row hover)clickRemoves the track (disabled at one track — visible, not hidden)One step
+ (header)clickAdds a track without resizing the box — same behavior as the canvas + chipOne step
Code toggle (header, <> icon)clickSwaps the rows for the raw grid-⁠template-⁠* text input (full syntax escape hatch); forced automatically for auto-fill/subgrid/mixed selectionsUI-only
Subgrid toggle (header; only when this grid box's parent is a grid)clickOn: axis adopts the parent's tracks (subgrid) — the rows become a card with an Edit parent jump; off: restores one 1fr per currently-resolved trackOne step each way
Edit parent (on the subgrid card)clickSelects the parent grid — the inspector flips to the tracks this axis adoptsSelection only
Track rowhoverTints that track on canvas (and vice versa)

Inspector — alignment & spacing (grid box selected)

AffordanceTriggerEffectHistory
3×3 markerclick a dotWrites justify-⁠items + align-⁠items together (exits stretch on both axes)One step
Stretch toggles (per axis, next to the marker)clickThat axis stretches (marker centers, bars fill); click again → startOne step
Distribute selects (X / Y)selectjustify-⁠content / align-⁠content; center/end/space-* values gate canvas dragging for that axis and show a note saying soOne step
Gap fieldstype · scrubAxis gaps (same values the canvas pills scrub)Scrub: one step total

Inspector — Placement (child of a grid selected)

AffordanceTriggerEffectHistory
Area picker (parent's tracks in miniature)click a cellPlaces the item there (grid-⁠column/row line numbers)One step
Area pickerdrag across cellsSpans the painted rect ("2 / 5" style)One step per gesture
Header clickClears placement — back to auto flow (disabled when nothing is placed)One step
Align X / Y rows (default scope)click a segmentEdits the parent grid's items alignment — the whole grid, like plain CSS; same property as the grid's own 3×3One step (writes the parent)
Scope toggle (item-in-cell icon, row end)clickSwitches the rows to Self X / Y — per-item justify-⁠self/align-⁠self with a dashed auto segment. Items that already carry overrides open in this modeUI-only
Scope toggle (lit)clickClears the overrides — the item follows the grid againOne step ("Follow grid alignment")
Self X / Y segmentsclickPer-item override for that axis; auto = inherit. Multi-select fans out (per-column workflow: select the column's items, toggle, set once)One step
Col / Row text fieldstype + EnterRaw grid-line syntax — spans, negative and named lines (span 2, 1 / -⁠1)One step

Everything the inspector and canvas both express (gaps, track sizes, items alignment) is the same underlying value — editing in one place moves the other live, including mid-drag.

How templates are treated

A grid's grid-template-columns/rows are stored as real CSS strings, and the editor is a view over them:

  • Documents round-trip byte-identical until you actually edit.
  • **Integer repeat(n, …) expands to n tracks when displayed.* The abstraction is dropped by the first structural edit* (the saved form is the expanded list); until then the stored string keeps its repeat().
  • Numbers save with ≤3 decimals, trailing zeros trimmed; unitless 0 reads as 0px.
  • Exotic values (calc(), clamp(), em…) are held verbatim, so they survive edits to other tracks untouched.

Degradation ladder (each level keeps every lower level reachable):

TemplateInspectorCanvas
fr/px/%/auto tracksnumeric rows, scrub + unit menufull: drag, chips, add/remove
minmax/fit-content/other trackthat row is a text fieldchip says mm/fit/; its boundaries don't drag
subgridlabeled card ("tracks come from the parent grid") with an Edit parent jump; count/+ hidden; raw text behind the code toggleadopted-track chrome: dashed lines + subgrid-relative numbers; boundary drags edit the parent's template (absent when the parent is gated or the box is shrink-wrapped off the track grid)
auto-fill/auto-fit, none, unbalancedwhole axis is a text input + "no fixed track count" note; count field disabledaxis chrome absent
mixed multi-selecttext input with Mixed placeholder (writes fan out)absent (single-target only)
repeat(n>100, …)text fallback (expansion would be useless as UI)absent

Nothing ever becomes uneditable — it just degrades to CSS text.

Boundary drag math

Dragging a boundary resizes only the adjacent track pair; other tracks stay visually pinned. The math is unit-aware:

  • fr / fr — the pair's total fr is conserved and redistributed by resolved proportion.
  • px / px, % / % (and mixes of absolute units) — the drag transfers space between them, clamping at 0 with the residual going to the other side so the sum holds. Percent writes back relative to the box's content size.
  • px / fr (either order) — only the absolute side changes; the fr side reflows through free space, which keeps other fr tracks' ratios.
  • auto — converts to px at its current size the moment you drag it; the track stays px afterwards.

The whole drag measures against its starting state, so the values you see in the badge never feed back into themselves mid-gesture.

The per-axis canvas gate

Browsers expose resolved track sizes, never positions — geometry can only assume tracks start at the content edge, which is true for justify/align-content: start | stretch and wrong under center, end, and space-*. Rather than draw drifting chrome, each axis independently enables canvas editing only when:

  1. its template parses,
  2. no implicit tracks exist (nothing has overflowed the explicit grid), and
  3. its content distribution is start or stretch.

A gated axis keeps its passive guides, and the inspector shows "Distributed tracks resize here, not on canvas" right under the Distribute row — the setting that causes the gate explains it.

Escape semantics

Escape during a live drag cancels just the drag — the document restores to the exact pre-drag state (including an unexpanded repeat() string) and the selection is kept. Escape clears the selection only when nothing is in flight.

Known limits

  • No track drag-to-reorder or between-track canvas insertion (the inspector covers insertion; + appends).
  • The area picker doesn't visualize spans, negative or named lines (the raw inputs do the work); implicit tracks aren't shown.
  • No canvas editing under center/end/space-* distribution (needs real track positions, which browsers don't expose).
  • Subgrid adopted geometry assumes the parent's gap and no subgrid padding: a subgrid's own gap override shifts item edges within the adopted tracks, which the dashed lines don't reflect. Shrink-wrapped subgrids get no adopted chrome by design.
  • Named lines are preserved through every edit but have no dedicated UI (subgrid [a] [b] line-name lists are raw-text-only).
  • Per-gap widths can't exist (one gap per axis) — gap pills all move together by design.
  • Self-alignment is grid-only for now; align-self for flex children is a separate (smaller) feature since flex ignores justify-self.