Components
Component gallery
Everything in @croffcss/ui, live. Try them with a keyboard, too.
Button
Cut corners on anything you can press. Give it an href and it renders a link. While loading, it's disabled and announces that it's busy.
<Button>Save</Button>
<Button tone="secondary" size="sm">Cancel</Button>
<Button tone="danger">Delete</Button>
<Button href="/docs">Read the docs</Button>
<Button loading>Saving</Button>Without React
<button type="button" class="display(inline-flex) align(center) justify(center) gap(2) box-sizing(border) margin(0) border(none) cut(sm) font(sans) text-weight(semibold) line-height(tight) text-decoration(none) whitespace(nowrap) cursor(pointer) user-select(none) active:transform(translate-y(1px)) disabled:opacity(50) disabled:cursor(not-allowed) disabled:pointer-events(none) transition(colors) duration(fast) motion-reduce:transition(none) focus-visible:outline(2,accent) focus-visible:outline-offset(2) focus-visible:[clip-path:none] bg(brand) color(on-brand) hover:bg(fg) hover:color(ink) height[2.5rem] padding-x(4) text-size(sm)">Save</button>Badge
A short status label, led by the logo's slash.
<Badge tone="success">Passing</Badge>Without React
<span class="display(inline-flex) align(center) gap(2) padding(1,3) cut(xs) font(mono) text-size(xs) text-weight(medium) letter-spacing(wide) line-height(tight) whitespace(nowrap) before:display(block) before:shrink(0) before:width[0.7em] before:height[0.4em] before:bg[currentColor] before:transform(skew-x(-42deg)) bg(alpha(success,14)) color(success)">Passing</span>Card
A hairline panel with a lime tab on its corner.
Design tokens
Change a token and every use follows.
An interactive card
Its border lights up when you point at it.
<Card title="Design tokens">
<CardBody>Change a token and every use follows.</CardBody>
</Card>Without React
<div class="position(relative) box-sizing(border) display(flex) flex-direction(col) gap(3) padding(6) bg(surface) color(fg) border(1,line) before:position(absolute) before:top[-1px] before:left[-1px] before:width[2.5rem] before:height[3px] before:bg(brand)">
<h3 class="margin(0) text-size(lg) text-weight(semibold) line-height(snug)">Design tokens</h3>
<p class="margin(0) text-size(sm) color(soft) line-height(relaxed)">Change a token and every use follows.</p>
</div>Alert
A message on the page. Add role="alert" when it appears after something happens, so that screen readers announce it.
A note
Neutral, for anything worth saying.Build finished
Generated 42 rules.Built at runtime
CROFF can't see a class name that's put together at runtime.The build failed
Two expressions couldn't be compiled.Pre-release
The API may still change before 1.0.<Alert tone="danger" title="The build failed" role="alert">
Two expressions couldn't be compiled.
</Alert>Without React
<div class="box-sizing(border) display(flex) flex-direction(col) gap(1) padding(4) text-size(sm) line-height(relaxed) bg(alpha(danger,10)) border-left(4,danger) color(fg)" role="alert">
<p class="margin(0) text-weight(semibold)">The build failed</p>
Two expressions couldn't be compiled.
</div>Field, input, textarea and select
Field connects a label, one control and its hint or error for screen readers, and marks the control invalid when there's an error.
We'll only use it to reply.
Enter your name.
<Field label="Email" hint="We'll only use it to reply.">
<Input type="email" />
</Field>
<Field label="Name" error="Enter your name.">
<Input />
</Field>
<Field label="Plan">
<Select><option>Free</option></Select>
</Field>Without React
<div class="display(flex) flex-direction(col) gap(2)">
<label for="email" class="font(sans) text-size(sm) text-weight(medium) color(fg)">Email</label>
<input id="email" type="email" aria-describedby="email-hint" class="box-sizing(border) width(full) margin(0) font(sans) text-size(sm) color(fg) bg(ink) border(1,line-strong) radius(none) hover:border-color(faint) focus:border-color(accent) focus-visible:outline(none) focus-visible:[box-shadow:inset_0_-2px_0_var(--croff-color-accent)] invalid:border-color(danger) disabled:opacity(50) disabled:cursor(not-allowed) transition(colors) duration(fast) motion-reduce:transition(none) height[2.5rem] padding-x(3) placeholder:color(faint)" />
<p id="email-hint" class="margin(0) text-size(xs) color(faint) line-height(snug)">We'll only use it to reply.</p>
</div>Checkbox, radio and switch
Real form controls. A checked checkbox shows the slash, radio buttons are diamonds, and a switch's thumb is a slash that slides across.
<Checkbox label="Send me updates" defaultChecked />
<Radio label="Monthly" name="billing" defaultChecked />
<Switch label="Dark mode" defaultChecked />Without React
<label class="display(inline-flex) align(center) gap(3) font(sans) text-size(sm) color(fg) cursor(pointer)">
<input type="checkbox" class="appearance(none) box-sizing(border) margin(0) shrink(0) bg(ink) border(1,line-strong) cursor(pointer) hover:border-color(accent) disabled:opacity(50) disabled:cursor(not-allowed) transition(colors) duration(fast) motion-reduce:transition(none) focus-visible:outline(2,accent) focus-visible:outline-offset(2) focus-visible:[clip-path:none] width[1.125rem] height[1.125rem] cut(xs) checked:border-color(brand) checked:bg(brand) checked:bg-image[linear-gradient(130deg,transparent_36%,var(--croff-color-on-brand)_36%,var(--croff-color-on-brand)_64%,transparent_64%)]" /> Send me updates
</label>
<label class="display(inline-flex) align(center) gap(3) font(sans) text-size(sm) color(fg) cursor(pointer)">
<input type="checkbox" role="switch" class="appearance(none) box-sizing(border) margin(0) shrink(0) bg(ink) border(1,line-strong) cursor(pointer) hover:border-color(accent) disabled:opacity(50) disabled:cursor(not-allowed) focus-visible:outline(2,accent) focus-visible:outline-offset(2) focus-visible:[clip-path:none] width[2.75rem] height[1.375rem] cut(xs) bg-image[linear-gradient(115deg,transparent_10%,var(--croff-color-soft)_10%,var(--croff-color-soft)_90%,transparent_90%)] bg-size[45%_100%] bg-repeat(none) bg-position(left) [transition:background-position_120ms,background-color_120ms] motion-reduce:[transition:none] checked:border-color(brand) checked:bg(brand) checked:bg-position(right) checked:bg-image[linear-gradient(115deg,transparent_10%,var(--croff-color-on-brand)_10%,var(--croff-color-on-brand)_90%,transparent_90%)]" /> Dark mode
</label>Tabs
The arrow keys, Home and End move between tabs. In React this is handled for you. Without React, you write that behavior and switch the panels yourself.
<Tabs
label="Output"
items={[
{ id: "css", label: "CSS", content: "The generated stylesheet." },
{ id: "map", label: "Source map", content: "Where each rule came from." },
]}
/>Without React
<div role="tablist" aria-label="Output" class="display(flex) gap(1) overflow-x(auto) border-bottom(1,line)">
<button type="button" role="tab" aria-selected="true" class="margin-bottom[-1px] padding(3,4) bg(transparent) border(none) border-bottom(2,transparent) font(sans) text-size(sm) text-weight(medium) color(faint) whitespace(nowrap) cursor(pointer) hover:color(fg) selected:color(fg) selected:border-color(accent) transition(colors) duration(fast) motion-reduce:transition(none) focus-visible:outline(2,accent) focus-visible:outline-offset(2) focus-visible:[clip-path:none]">CSS</button>
</div>
<div role="tabpanel" tabindex="0" class="padding-y(4) text-size(sm) color(soft) line-height(relaxed) focus-visible:outline(2,accent) focus-visible:outline-offset(2) focus-visible:[clip-path:none]">The generated stylesheet.</div>Dialog
Built on the native dialog element, which keeps focus inside, restores it afterwards and closes on Escape. In React, clicking outside closes it too.
const [open, setOpen] = useState(false);
<Button onClick={() => setOpen(true)}>Delete project</Button>
<Dialog
open={open}
onClose={() => setOpen(false)}
title="Delete this project?"
actions={<Button onClick={() => setOpen(false)}>Keep it</Button>}
>
This can't be undone.
</Dialog>Without React
<dialog class="box-sizing(border) width[min(32rem,calc(100vw-2rem))] margin(auto) padding(6) bg(surface) color(fg) font(sans) border(1,line-strong) backdrop:bg(alpha(ink,80)) animation[croff-ui-rise_160ms_ease-out] motion-reduce:animation(none)">
<h2 class="margin(0) margin-bottom(3) font(display) [font-stretch:125%] text-size(xl) text-weight(bold) letter-spacing(tight) line-height(tight)">Delete this project?</h2>
<p class="margin(0) text-size(sm) color(soft) line-height(relaxed)">This can't be undone.</p>
</dialog>Accordion
Built on the native details element, so it works without JavaScript. Its slash lies flat when an item opens.
Does it need a runtime?
Do I need React?
Can I change the colors?
<Accordion
exclusive
items={[
{ id: "a", summary: "Does it need a runtime?", content: "No. It's plain CSS." },
]}
/>Without React
<details class="border-bottom(1,line)">
<summary class="display(flex) align(center) justify(between) gap(4) padding-y(4) font(sans) text-size(sm) text-weight(medium) color(fg) cursor(pointer) list-style(none) details-marker:display(none) hover:color(accent) transition(colors) duration(fast) motion-reduce:transition(none) focus-visible:outline(2,accent) focus-visible:outline-offset(2) focus-visible:[clip-path:none]">
Does it need a runtime?
<span aria-hidden="true" class="display(block) shrink(0) width[0.875rem] height[0.5rem] bg(accent) transform(skew-x(-42deg)) group-open:transform(skew-x(0deg)) group-open:height[0.125rem] [transition:transform_120ms,height_120ms] motion-reduce:[transition:none]"></span>
</summary>
<div class="padding-bottom(4) text-size(sm) color(soft) line-height(relaxed)">No. It's plain CSS.</div>
</details>Progress, spinner and skeleton
A slanted bar, a slash that sweeps across its track, and a placeholder for loading content. Their motion stops for visitors who ask for less of it.
<Progress value={64} label="Uploading" />
<Spinner label="Loading results" />
<Skeleton className="width(full)" />Without React
<div role="progressbar" aria-label="Uploading" aria-valuemin="0" aria-valuemax="100" aria-valuenow="64" class="display(block) width(full) height[0.5rem] bg(raised) overflow(hidden)">
<span class="display(block) height(full) bg(accent) clip-path[polygon(0_0,100%_0,calc(100%-0.5rem)_100%,0_100%)] [transition:width_200ms_ease-out] motion-reduce:[transition:none]" style="width: 64%"></span>
</div>
<span role="status" aria-label="Loading results" class="display(inline-block) width[2.5em] height[0.5em] bg[color-mix(in_srgb,currentColor_20%,transparent)] transform(skew-x(-30deg)) bg-image[linear-gradient(currentColor,currentColor)] bg-size[40%_100%] bg-position(center) bg-repeat(none) animation[croff-ui-sweep_1s_ease-in-out_infinite] motion-reduce:animation(none) color(accent)"></span>Avatar
A person's picture, or their initials when there isn't one.
<Avatar name="Ada Lovelace" />
<Avatar name="Ada Lovelace" src="/ada.jpg" size="lg" />Without React
<span role="img" aria-label="Ada Lovelace" class="display(inline-flex) align(center) justify(center) shrink(0) overflow(hidden) cut(sm) bg(raised) color(fg) font(mono) text-weight(semibold) text-transform(uppercase) width(10) height(10) text-size(sm)">
<span aria-hidden="true">AL</span>
</span>Table
Mono headers, and a scroll that the keyboard can reach when the table is wide.
| Name | Job |
|---|---|
| @croffcss/parser | Reads the grammar |
| @croffcss/compiler | Orders and prints the CSS |
| @croffcss/ui | These components |
<Table
caption="Packages"
columns={[
{ key: "name", label: "Name" },
{ key: "job", label: "Job" },
]}
rows={[{ id: "1", name: "@croffcss/parser", job: "Reads the grammar" }]}
/>Without React
<div tabindex="0" class="overflow-x(auto) border(1,line)">
<table class="width(full) border-collapse(collapse) font(sans) text-size(sm) color(soft) text-align(left)">
<thead>
<tr><th scope="col" class="padding(3,4) bg(raised) font(mono) text-size(xs) text-weight(medium) color(fg) text-transform(uppercase) letter-spacing(wider) whitespace(nowrap) border-bottom(1,line-strong)">Name</th></tr>
</thead>
<tbody>
<tr class="hover:bg(raised)"><td class="padding(3,4) border-bottom(1,line)">@croffcss/parser</td></tr>
</tbody>
</table>
</div>Heading, eyebrow and keyboard key
The system's type: wide display headings, mono labels led by the slash, and keys. Load Archivo and JetBrains Mono for the exact look, or override the font tokens.
Getting started
Style without the noise.
Press Ctrl K to search.
<Eyebrow>Getting started</Eyebrow>
<Heading level={2} size="lg">Style without the noise.</Heading>
<Kbd>Ctrl</Kbd> <Kbd>K</Kbd>Without React
<p class="display(flex) align(center) gap(3) margin(0) font(mono) text-size(xs) color(faint) text-transform(uppercase) letter-spacing(widest) before:color(accent) before:display(block) before:shrink(0) before:width[0.7em] before:height[0.4em] before:bg[currentColor] before:transform(skew-x(-42deg))">Getting started</p>
<h2 class="margin(0) font(display) [font-stretch:125%] text-weight(bold) letter-spacing(tighter) line-height[1.04] text-size[clamp(2rem,5vw,3.5rem)]">Style without the noise.</h2>
<kbd class="display(inline-block) padding(0,2) font(mono) text-size(xs) line-height(relaxed) bg(raised) color(soft) border(1,line) border-bottom(2,line-strong)">Ctrl</kbd>