Corners
Four corner marks: a frame that doesn't close.
Report an issue with CornersExample
Halden
A studio for type and silence. Frames a space without closing it.
Type
Measure
Leave
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.jsonUsage
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-insetand--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-2outside, 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>withasChild; its own quotation marks are dropped.
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
asChild | boolean | false | Put 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-corner | CSS length | 8px | The length of each mark. |
--ot-corner-inset | CSS length | 0 | How far the marks sit inside the edge. |
--ot-corner-colour | CSS colour | var(--ot-ink) | The marks' colour. |
Craft
Where its move comes from
| Component | Reference | Move |
|---|---|---|
| Corners | "the silence that heals" corners | A frame that doesn't close |
| Viewfinder corners | A camera's framing marks; "the silence that heals" | The frame glides onto what you point at and locks |
| Glide corners | The viewfinder's marks, following focus from control to control; the job of a target cursor | One frame carries across a group, and appears only while you are in it |
| Kagi corners | The Japanese corner brackets 「 」 | Two corners quote a phrase across its lines |
How it moves
| Component | Articulation | What moves |
|---|---|---|
| Viewfinder corners | Spiccato | The marks glide onto what you point at and lock with one small rebound, then open back out to the whole |
| Glide corners | Glide | Over 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 |