Centinadocs

Grid visual editor — reference

The CSS grid editing surface. Two synchronized surfaces edit the same grid: the inspector renders each template axis as a proportional track strip plus a detail row for the selected track, 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 strips; 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 + (which also selects the track it appends).
  • Track strip — the axis as a proportional miniature: one segment per track, sized by that track's resolved extent (segments go equal when the extent can't be measured — multi-select, unmeasured templates), each labeled in the canvas chips' vocabulary (1fr, 61px, mm, fit, min/max, …; the label hides when its segment is too small for it, full value in the tooltip). Columns run horizontally, rows stack vertically — the two strips together sketch the grid. A uniform template reads as one repeated shape, and a collapsed track (a 1fr starved by overflowing px rows, an empty auto) shows as a stub: layout truth a list of inputs can't tell you.
    • Clicking a segment selects that track for the detail row below.
    • The seams between segments drag like the canvas boundary strips — the same conservation math, scaled so dragging across the strip sweeps the whole axis, with the canvas badge riding along. A seam between neighbors that aren't plainly numeric (minmax, fit-content, calc…) shows not-allowed and points at the detail row; a collapsed track keeps a small clickable stub so it's still reachable.
  • Detail row — the selected track's scrubbable number + unit menu (fr / px / % / auto / …) plus an always-visible remove button (disabled, not hidden, at one track). Its index chip wears the selected segment's accent fill: one selection, two views.
    • 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 strip is 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 strip segment; hovering a segment or the detail 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 segment in the inspector strip—
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
Strip segmentclickSelects that track — the detail row below edits it (the selection clamps when tracks are removed, and resets when you select a different node)UI-only
Strip seam (the joint between two segments; resize cursor)dragResizes the adjacent track pair — the same conservation math as the canvas boundary strips, scaled so the strip spans the whole axis; the canvas badge rides alongOne step; Escape restores the exact template string
Strip seam, not-⁠allowed cursorhoverTooltip: numeric tracks drag here — minmax/fit-content/calc neighbors edit in the detail row—
Detail row number (the index chip)drag to scrub (Shift ×10, Alt ×0.1) · ↑/↓ in the fieldScrubs the selected 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 detail row to free CSS text (minmax, calc…)One step (no-op if identical)
Detail row −clickRemoves the selected track (disabled at one track — visible, not hidden)One step
+ (header)clickAdds a track without resizing the box — same behavior as the canvas + chip; the new track becomes the selectionOne step
Code toggle (header, <> icon)clickSwaps the strip 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 strip becomes 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
Strip segment / detail rowhoverTints that track on canvas (and vice versa: hovering a canvas chip lights its segment)—

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 tracksproportional strip + detail row: scrub + unit menu, draggable seamsfull: drag, chips, add/remove
minmax/fit-content/other trackits segment says mm/fit/… and its seams don't drag; selecting it makes the detail row 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.