Skip to content

Components

Component gallery

Everything in @croffcss/ui, live. Try them with a keyboard, too.

Theme

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.

A link
React
<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
HTML
<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.

NeutralBrandSuccessWarningDangerInfo
React
<Badge tone="success">Passing</Badge>
Without React
HTML
<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.

React
<Card title="Design tokens">
  <CardBody>Change a token and every use follows.</CardBody>
</Card>
Without React
HTML
<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.
React
<Alert tone="danger" title="The build failed" role="alert">
  Two expressions couldn't be compiled.
</Alert>
Without React
HTML
<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.

React
<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
HTML
<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.

React
<Checkbox label="Send me updates" defaultChecked />
<Radio label="Monthly" name="billing" defaultChecked />
<Switch label="Dark mode" defaultChecked />
Without React
HTML
<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.

The generated stylesheet.
React
<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
HTML
<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.

Delete this project?

This can't be undone. Press Escape or click outside to close this.
React
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
HTML
<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?
The styles are plain CSS. The class functions are small, and optional.
Do I need React?
No. Every component is also a function that returns its class names.
Can I change the colors?
Yes. Override any token, with no rebuild.
React
<Accordion
  exclusive
  items={[
    { id: "a", summary: "Does it need a runtime?", content: "No. It's plain CSS." },
  ]}
/>
Without React
HTML
<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.

Loading results
React
<Progress value={64} label="Uploading" />
<Spinner label="Loading results" />
<Skeleton className="width(full)" />
Without React
HTML
<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.

React
<Avatar name="Ada Lovelace" />
<Avatar name="Ada Lovelace" src="/ada.jpg" size="lg" />
Without React
HTML
<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.

Some of the packages
NameJob
@croffcss/parserReads the grammar
@croffcss/compilerOrders and prints the CSS
@croffcss/uiThese components
React
<Table
  caption="Packages"
  columns={[
    { key: "name", label: "Name" },
    { key: "job", label: "Job" },
  ]}
  rows={[{ id: "1", name: "@croffcss/parser", job: "Reads the grammar" }]}
/>
Without React
HTML
<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.

React
<Eyebrow>Getting started</Eyebrow>
<Heading level={2} size="lg">Style without the noise.</Heading>
<Kbd>Ctrl</Kbd> <Kbd>K</Kbd>
Without React
HTML
<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>