/ component

Clip Path Editor

Edit the CSS clip-path / shape-outside property — a single <basic-shape> with an optional geometry-box keyword — with compile-time BASIC-SHAPE DISPATCH. ParseFunction routes the value to a per-shape validator: inset() validates 1-4 length-percentages, circle() a radius + at-position, ellipse() two radii, polygon() a variadic vertex list (each vertex two length-percentages, validated to a 32-vertex cap). inset(45deg) → never; circle(50% 60%) → never; polygon(0% 0%, 100% 0%, 50%) → never. Drag the polygon vertices to write percentage coordinates live.

/ basic-usage

Edit a basic shape

Controlled value + onChange. Pick a shape, edit each argument with the right units, drag polygon vertices, and the editor emits a valid clip-path string.

polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)
/ polygon-playground

Drag the vertices

The handles convert pointer position to percentage vertices and write the polygon() string live — the namesake demo. The same value drives both the masked image and the editor panel below.

fill-rule
1
2
3
4
5
polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%)
preview

drag a handle to move a vertex · arrow keys nudge (⇧ = 10%)

polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%)
/ types

Three usage tiers

From useState-and-go to per-shape dimension-typed dispatch.

01 casual
string

Pass any string

useState<string>. No compile-time validation; the runtime parser handles whatever you type — including calc() and var() coordinates.

circle(40% at 50% 50%)
const [value, setValue] = useState<string>("circle(40% at 50% 50%)")
02 intellisense
ClipPathString

Literal-shaped hints

State typed as ClipPathString — the union of every inset()/circle()/ellipse()/polygon() head (with an optional geometry box) plus a bare box and none. It is also the onChange return type.

ellipse(40% 30%)
const [value, setValue] = useState<ClipPathString>("ellipse(40% 30%)")
03 strict
ClipPathLiteral<S>

Per-shape grammar at compile time

cssClipPath() dispatches on the shape function and resolves a wrong dimension, arg count, vertex shape, or a double geometry box to never — a type error before you run the code.

polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)
cssClipPath("polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)") // ✓
// @ts-expect-error circle radius cannot be two values
cssClipPath("circle(50% 60%)")
// @ts-expect-error a vertex needs two coordinates
cssClipPath("polygon(0% 0%, 100% 0%, 50%)")

Note: calc() / var() coordinates, the inset() round radius tail, and 3/4-token edge-offset positions are undecidable / out-of-scope at the strict tier — use the casual or IntelliSense tier; the runtime parser accepts them. Polygon vertices past 32 are weak-validated.

/ api

API

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

§ ClipPathEditor / ClipPathEditorPanel

<ClipPathEditor
  value: ClipPathString | (string & {})
  onChange: (next: ClipPathString) => void
  mode?: "clip-path" | "shape-outside"
  className?: string
  aria-label?: string
/>

ClipPathEditor is popover-wrapped; ClipPathEditorPanel renders the same editor inline. Both are controlled.

PropTypeDescription
valueClipPathString | (string & {})Current CSS clip-path value. Required. `none` is the empty state.
onChange(next: ClipPathString) => voidFires when the edited shape changes. Emits the normalized basic-shape string (or `none`).
mode"clip-path" | "shape-outside"Which property the live preview targets. Both share the grammar, so this does NOT narrow the output type. Default `clip-path`.

§ Sub-components

<ShapeSelect value onChange />

Choose the basic shape (inset / circle / ellipse / polygon) or `none / box only`.

<GeometryBoxSelect value onChange />

The optional geometry-box keyword (margin/border/padding/content/fill/stroke/view-box, or none).

<InsetControls state onChange />

Four length-percentage editors (top/right/bottom/left) plus an optional round radius.

<CircleControls state onChange /> · <EllipseControls state onChange />

Radius editor(s) — a length-% or a closest-side/farthest-side keyword — plus an optional `at <position>` clause.

<PolygonControls state onChange />

A fill-rule select and a list of vertex rows (add / remove / edit each x,y; min 3).

<LengthPctEditor label value onChange />

A numeric field + unit select (%, px, rem, em, vw, vh) for a <length-percentage> slot; raw text for opaque calc()/var().

<ClipPathPreview value mode? onChange? />

The showcase: a masked surface with the live clip-path / shape-outside, draggable SVG vertex handles for polygons (pointer drag → percentage vertices; arrow keys nudge), a clip-path ↔ shape-outside toggle, and a UnitInput radius scrub for circle/ellipse.

§ Runtime helpers

cssClipPath<S extends string>(value: S & ClipPathLiteral<S>): S

Call-site validator. Mirrors cssFilter() / cssTransform() / cssCalc() / color() / easing().

parseClipPath(src: string): ClipPathState | null

String → typed state, or null on unknown-shape / arity / double-box / syntax error. `none` and empty → { shape: null }. Tolerates calc()/var() coordinates and a leading or trailing geometry box.

formatClipPath(state: ClipPathState): string

Canonical re-serialization (fill-rule first, single spaces, box per stored position). Null shape with no box → `none`.

shapeToCss(shape: ClipPathShapeState): string

One shape → its CSS function string.

shapeName(src: string): string

Runtime mirror of ShapeOf — the shape name, `box`, or `none`.

polygonVertices(src: string): Array<{ x; y }>

The polygon's vertices in order, or [] if the value is not a polygon.

defaultShape(shape): ClipPathShapeState

Seed a fresh shape with sensible defaults (centered circle/ellipse, a 10% inset, a triangle polygon).

§ Types

ClipPathLiteral<S>
Strict validator — S if the shape (arity + each argument's dimension) and optional geometry box validate, else never.
ClipPathString
Suggestion union: per-shape heads (± a leading/trailing box), a bare box, and `none`.
ClipPathStringMap / ClipPathShape
Shape → output-string map and its key union.
BasicShapeName / GeometryBox
The four shape names; the seven geometry-box keywords.
ShapeOf<S>
The basic shape of a value: the shape name, `box`, or `none`.
VertexCountOf<S>
Number of vertices in a polygon (0 otherwise).
GeometryBoxOf<S>
The geometry-box keyword present, or `none`.
ClipPathState / ClipPathShapeState
The editor's discriminated-union state (exported for advanced use).

§ Strict-tier scope

  • Full basic-shape dispatch: ParseFunction → a per-shape validator after peeling an optional leading/trailing geometry box. inset() validates 1–4 length-percentages; circle() a single radius (length-% or keyword) + an at <position> clause; ellipse() exactly two radii + a position; polygon() a variadic comma-separated vertex list where each vertex is two length-percentages.
  • polygon() validates each vertex up to a 32-vertex depth cap; beyond that the tail is weak-validated (accepted without per-coordinate checks) to bound tsc. The runtime parser validates every vertex regardless.
  • At most one geometry box, leading or trailing; a bare box is valid. A double box (border-box circle() padding-box) resolves to never.
  • Weak-validated / deferred: the inset() round radius tail (presence + non-empty), 3/4-token edge-offset <position> forms, and polygon vertices past the cap. The runtime parser accepts these.
  • calc() / var() inside an argument resolve to never at the strict tier (undecidable at compile time) — use the casual / IntelliSense tier.
/ install

Drop it in

One command via the shadcn CLI.

$ pnpm dlx shadcn@latest add https://turtiesocks.github.io/ridiculous/r/clip-path-editor.json