/ component

Background Editor

Ridiculously typed editor for the CSS background shorthand. An index-aware fold permits a <color> token only on the final layer, so a color in any earlier layer is a compile error. A reorderable layer stack embeds the real gradient + color pickers over a live composite preview.

/ basic-usage

Edit a background shorthand

Controlled value + onChange. The component edits the CSS background shorthand — a comma-stacked list of layers painted back-to-front, each carrying an image, a position / size, and repeat / attachment / origin / clip keywords. The popover trigger shows the layer count and a truncated preview; the panel exposes the reorderable layer stack, a 2D position pad per layer, and a live composite tile. Only the final layer may carry a <color>.

background: linear-gradient(#3b82f6, #8b5cf6) center / cover no-repeat, #0f172a
/ layer-stack

The layer stack, composited live

The hero: a reorderable stack of layers painted back-to-front. Each card embeds a real GradientEditor (or a url() input) for the image, a 2D crosshair position pad with x/y UnitInputs, a size control, and repeat / attachment / origin / clip selects. Use the ↑ / ↓ buttons to reorder the paint order — and watch the <color> always re-home onto whichever layer is last (the index-aware invariant, kept true under reorder). Only the final card exposes the ColorPicker. The live tile composites every layer in real time, exactly as the browser paints it.

layer 1
image
size
layer 2
image
size
layer 3 (final)
image
size
color
linear-gradient(135deg, rgb(59 130 246 / 0.7), rgb(139 92 246 / 0.7)) center / cover no-repeat, radial-gradient(circle at 30% 30%, #fbbf24, transparent 60%) left top / 60% 60% no-repeat, #0f172a
preview
produced value
background: linear-gradient(135deg, rgb(59 130 246 / 0.7), rgb(139 92 246 / 0.7)) center / cover no-repeat, radial-gradient(circle at 30% 30%, #fbbf24, transparent 60%) left top / 60% 60% no-repeat, #0f172a

The value lands on the background property. Layers join with , and paint front-to-back (the first layer sits on top); the trailing <color> is emitted only on the final layer. The checkerboard underlay makes transparency visible.

/ types

Three usage tiers

From useState-and-go to compile-time last-layer-color-invariant validation.

01 casual
string

Pass any string

useState<string>. No compile-time validation; the runtime parser splits the value paren-aware into layers and slots every space-token by kind — image, position, the / size follower, repeat / attachment / origin / clip — exactly as the strict tier would, but lenient about token order and exotic forms.

url(photo.jpg) left top / 50% repeat-x, #fff
const [value, setValue] = useState<string>("url(photo.jpg) left top / 50% repeat-x, #fff")
02 intellisense
BackgroundString

Background-shaped hints

State typed as BackgroundString — a shorthand-shaped string (and the onChange return type). It stays assignable from any string literal, so you opt into the strict gate only at the call sites that want it (cssBackground()) while the editor still round-trips freely. LayersOf<S> and LayerCountOf<S> read the comma-split layer tuple off any value.

radial-gradient(#000, #fff) center, #0a0a0a
const [value, setValue] = useState<BackgroundString>("radial-gradient(#000, #fff) center, #0a0a0a")
03 strict
BackgroundLiteral<S>

The shorthand typed at compile time

cssBackground() splits the value into comma-stacked layers and folds them with an index-aware head/tail recursion that knows when it is at the last layer — permitting a <color> token ONLY there. A color in any non-final layer, or any unrecognized per-layer token, resolves to never before you run the code. Colors use color-picker forms (#f00 / oklch(…)), not named colors.

linear-gradient(#f00, #00f) center / cover no-repeat, #fff
cssBackground("…gradient… center / cover, #fff") // ✓ color on the final layer
// @ts-expect-error color in a non-final layer
cssBackground("#f00 center, url(x.png)")
// @ts-expect-error unknown size token
cssBackground("center / wibble")

Note: per-layer token validation is membership-based and order-free — the || ordering / cardinality (at most one image, position-before-/-before- size) is deferred to the runtime parser. Image internals are lenient (any parenthesized function or none; the embedded GradientEditor validates gradients), and calc()/var() defer to runtime.

/ api

API

Public surface — component props, runtime helpers, and the type exports.

§ BackgroundEditor / BackgroundEditorPanel

<BackgroundEditor
  value: BackgroundString | (string & {})
  onChange: (next: BackgroundString) => void
  className?: string
  aria-label?: string
/>

BackgroundEditor is popover-wrapped (a trigger showing the layer count + truncated value); BackgroundEditorPanel renders the same editor inline. Both are controlled. State is the parsed BgLayer[] with a lastEmittedRef resync, and a <color> that re-homes onto the final layer after any reorder.

PropTypeDescription
valueBackgroundString | (string & {})Current background shorthand string. Required. An empty / unparseable value seeds a default single-layer editor.
onChange(next: BackgroundString) => voidFires when the value changes. Emits the canonical re-serialized shorthand (color only on the final layer).
classNamestringForwarded to the popover trigger (BackgroundEditor) or the inline fieldset (BackgroundEditorPanel).

§ Sub-components

<LayerStack layers onChange />

The reorderable vertical stack of layer cards. Renders a LayerCard per layer plus up/down/remove controls (no drag-drop dependency) and an add-layer button. The parent re-derives isFinal from array position so the color always rides the last layer.

<LayerCard index isFinal layer onChange onMoveUp onMoveDown onRemove />

One layer's editors: an embedded GradientEditor (when the image is a gradient) or a url()/none input; a PositionPad + x/y UnitInputs; a size control (cover/contain/auto vs a length); repeat / attachment / origin / clip MiniSelects. The FINAL card additionally renders a ColorPicker for the layer color.

<PositionPad x y onChange />

The 2D crosshair position picker — a role='slider' control with pointer press/drag and Arrow-key nudge (Shift = 10). Locally re-implemented (registry self-containment) — not imported from gradient-editor.

<BackgroundPreview value />

The live composite tile. Renders the produced shorthand as the tile's background style so every layer composites in real time, over a checkerboard underlay that makes transparency visible.

<MiniSelect value options onChange />

The local compact <select> chrome. Each component owns its copy (registry self-containment) — it is not imported from query-builder.

<LiveString value />

The produced background value rendered in a <code> block.

§ Runtime helpers

cssBackground<S>(value: S & BackgroundLiteral<S>): S

Call-site validator for a background shorthand. Mirrors cssTransform() / cssPositionArea(). The index-aware last-layer-color rule is the namesake.

parseBackground(src): { layers: BgLayer[]; error: string | null }

String → editor state. The runtime superset of the strict tier: a paren-aware comma split into layers, then per-layer space-token slotting. A color in any non-final layer is an error (the index-aware invariant).

formatBackground(layers): string

Canonical re-serialization. Layers join with ', '; within a layer, position / size uses a space-separated slash; the <color> is emitted ONLY on the final layer.

classifyToken(token): BgTokenKind

The runtime mirror of the type IsBgToken: image | position | size | repeat | attachment | box | length | color | slash | unknown. A functional color (oklch(…)) is checked before the parenthesized-image rule.

repeatOptions()  ·  attachmentOptions()  ·  boxOptions()  ·  sizeKeywords()  ·  defaultBackground()

The <select> option sources (the strict whitelists) and a sensible single-layer seed value.

§ Types

BackgroundLiteral<S>
Strict validator — S if S is a valid background shorthand, else never. Splits into comma-stacked layers and folds them index-aware, permitting a <color> only on the final layer.
BackgroundString
The suggestion type (a shorthand-shaped string) and the onChange return type.
LayersOf<S> / LayerCountOf<S>
The comma-split layer tuple of a background value, and its length.
BgLayer / BackgroundValue
The internal editor state: one layer (image, position, size, repeat, attachment, origin, clip, and an optional color meaningful only on the final layer), and the layer array. Exported for advanced use.
BgTokenKind
The classifyToken result union (image | position | size | repeat | attachment | box | length | color | slash | unknown).

§ Strict-tier scope (validated vs deferred)

  • Validated: the comma-split layer structure; the last-layer-only-<color> invariant (a color token in any non-final layer → never); recognized per-layer token membership (image / position / size / repeat / attachment / box / <length-percentage>); and the final color via color-picker's ColorLiteral.
  • Deferred (lenient → runtime parser): the || ordering / cardinality (at most one image, position-before-/-before-size) — tokens are validated by membership, in any order; gradient / image internals (any parenthesized function or none; the embedded GradientEditor validates gradients); multi-value position forms; and calc()/var() values.
  • Colors use color-picker functional / hex forms (#f00 / oklch(…)), not named colors; and the / between position and size is space-separated in strict (the runtime parser handles both).
/ install

Drop it in

One command via the shadcn CLI.

$ pnpm dlx shadcn@latest add https://ridiculous.turtlesocks.dev/r/background-editor.json