IV Space and line

Star on GitHubFeedback
IV Space and lineNative HTMLot-corners

Corners

Four corner marks: a frame that doesn't close.

Report an issue with Corners

Example

Halden

A studio for type and silence. Frames a space without closing it.

The note on the studio door says come in, the kettle is on, and leave your shoes by the stove and nothing else.

Install

Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base along.

npx shadcn@latest add https://thedirectors.agency/ui/r/corners.json

Usage

corners-example.tsx
import { Corners } from "@/components/ui/corners"const tools = [["Type", ["Roman", "Italic", "Small caps"]],["Measure", ["Narrow", "Book", "Wide"]],["Leave", ["Save", "Discard"]],] as constconst plates = [["01", "Morning, the lake"],["02", "The ferry at noon"],["03", "Birches, late"],]export default function Example() {return (<div className="grid gap-16"><Corners className="max-w-[28rem] p-(--ot-space-6)"><p className="ot-mf">Halden</p><p className="ot-mp mt-3 text-pretty">A studio for type and silence. Frames a space without closing it.</p></Corners><Corners variant="viewfinder" className="grid max-w-[40rem] grid-cols-3 gap-6 p-(--ot-space-4)">{plates.map(([n, name]) => (<a key={n} href="#" className="grid gap-2 p-(--ot-space-2) no-underline" style={{ color: "inherit" }}><span className="ot-mf ot-figures">{n}</span><span className="ot-pp">{name}</span></a>))}</Corners><Corners variant="glide" role="toolbar" aria-label="Setting" className="flex max-w-[44rem] flex-wrap gap-x-10 gap-y-6 py-(--ot-space-2)">{tools.map(([group, names]) => (<div key={group} role="group" aria-label={group} className="grid gap-2"><span className="ot-pp text-(--ot-pencil)">{group}</span><div className="flex flex-wrap gap-5">{names.map((name) => (<button key={name} type="button" className="ot-mp disabled:text-(--ot-pencil)" disabled={name === "Discard"}>{name}</button>))}</div></div>))}</Corners><p className="ot-mp max-w-[34rem] text-pretty">The note on the studio door says{" "}<Corners asChild variant="kagi"><q>come in, the kettle is on, and leave your shoes by the stove</q></Corners>{" "}and nothing else.</p></div>)}

Contract

  • Underneath: native. Its CSS lives in base.css, so every item may use it.
  • Four corner marks as one ::after. It frames without closing. Tune it with --ot-corner (length), --ot-corner-inset and --ot-corner-colour. Forced colours draw them in CanvasText.
  • data-variant="viewfinder": after the camera's framing marks ("the silence that heals" names the phone it was shot on). At rest the marks frame the whole. Point at or focus a part (a direct child) and they glide onto it, a step outside it, and lock with one small rebound; leaving, they open back out to the whole. For the keyboard the marks are the focus ring: in the accent, and the part draws no outline of its own. A tap leaves the frame where the finger put it. The component sets --ot-aim-x/y/w/h.
  • data-variant="glide": the viewfinder's marks lent to a group of controls, at any depth (a toolbar, a row of links, a form's actions); the job of a target cursor, done with the one frame the page already owns instead of a cursor of its own. At rest there is no frame. Point at or focus a control and one frame fades in round it, --ot-space-2 outside, then glides from control to control over moderato with breath, and moves only when the pointer or focus moves. Leaving, it goes back to the focused control if there is one, or fades where it was. Disabled controls are passed over; a touch shows nothing. For the keyboard the frame is the focus ring, in the accent, and the controls draw no outline of their own; forced colours draw it in CanvasText, and Highlight on focus. Arriving from rest it only fades, so it never flies in from a corner; under reduced motion it is simply there, on the control you are at. Positions are measured from the box, so right to left needs nothing of its own.
  • data-variant="kagi": after the Japanese corner brackets 「 」, where the corner mark is the quotation mark. Only two corners at --ot-stroke, the opening one before the first word and the closing one after the last (box-decoration-break: slice), however many lines the words cross; right to left they mirror. A word joiner keeps the opening mark with its first word. Put it on a <q> with asChild; its own quotation marks are dropped.

Props

Props of Corners
PropTypeDefaultWhat it does
asChildbooleanfalsePut the corners on the child element instead of a new div.
variant"viewfinder" | "kagi" | "glide"viewfinder frames the whole at rest, and glides onto the part (a direct child) you point at or focus, locking with one small rebound. kagi sets only the opening and closing corners round a phrase in running text, as the Japanese corner brackets 「 」 quote it, across line breaks; put it on a <q> with asChild. glide keeps no frame at rest: point at or focus any control inside, at any depth, and one frame appears round it, glides from control to control as the pointer or focus moves, and fades where it was when both leave. Disabled controls are passed over.
--ot-cornerCSS length8pxThe length of each mark.
--ot-corner-insetCSS length0How far the marks sit inside the edge.
--ot-corner-colourCSS colourvar(--ot-ink)The marks' colour.

Craft

Where its move comes from

Where its move comes from
ComponentReferenceMove
Corners"the silence that heals" cornersA frame that doesn't close
Viewfinder cornersA camera's framing marks; "the silence that heals"The frame glides onto what you point at and locks
Glide cornersThe viewfinder's marks, following focus from control to control; the job of a target cursorOne frame carries across a group, and appears only while you are in it
Kagi cornersThe Japanese corner brackets 「 」Two corners quote a phrase across its lines

How it moves

How it moves
ComponentArticulationWhat moves
Viewfinder cornersSpiccatoThe marks glide onto what you point at and lock with one small rebound, then open back out to the whole
Glide cornersGlideOver moderato with breath, the frame glides from control to control and lands without a rebound; from rest it only fades in, and it fades out where it was