Component showcase
A live gallery of every component in @civitai/components — the framework-agnostic, dual-consumption design system that powers Civitai App Blocks. Each demo renders live and fully themed (painted by the published@civitai/theme + @civitai/components CSS a real consumer installs), above a source panel you can toggle between the framework-agnostic HTML and the @civitai/components-react binding.
- HTML authors follow the
data-civitai-uimarkup contract (see the Components reference); any HTML that follows it renders identically to the React bindings. - React authors use
@civitai/components-react— thinforwardRefwrappers that emit exactly that markup and auto-inject the stylesheet.
The previews re-theme with the site: toggle the header's light/dark switch and every --civitai-* token re-resolves in place. The React snippets below are type-checked against the pinned @civitai/[email protected] declarations on every build, so they can't drift from the shipped API.
Setup
Load the tokens and the component CSS (order-independent):
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@civitai/theme/styles.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@civitai/components/styles.css" />Then set data-theme="light" or data-theme="dark" on any ancestor. From JS, import { injectStyles } from '@civitai/components'; injectStyles() injects both idempotently.
Button
Native <button> (ref-forwarded in React). data-variant: filled · light · outline · subtle. data-size: sm · md · lg. Loading sets aria-busy + disabled and prepends a sm loader.
data-civitai-ui="button"light<div data-civitai-ui="stack" data-gap="md">
<div data-civitai-ui="group" data-gap="sm">
<button data-civitai-ui="button" data-variant="filled">Filled</button>
<button data-civitai-ui="button" data-variant="light">Light</button>
<button data-civitai-ui="button" data-variant="outline">Outline</button>
<button data-civitai-ui="button" data-variant="subtle">Subtle</button>
</div>
<div data-civitai-ui="group" data-gap="sm">
<button data-civitai-ui="button" data-size="sm">Small</button>
<button data-civitai-ui="button" data-size="md">Medium</button>
<button data-civitai-ui="button" data-size="lg">Large</button>
</div>
<div data-civitai-ui="group" data-gap="sm">
<button data-civitai-ui="button" data-variant="filled" aria-busy="true" disabled>
<span data-civitai-ui="loader" data-size="sm" aria-hidden="true"></span>
Saving…
</button>
<button data-civitai-ui="button" data-variant="outline" disabled>Disabled</button>
</div>
</div>Badge
Presentational <span>. data-variant: filled · light · outline. data-size: sm · md · lg. data-color (new in 0.1.2): info · success · warning · error — omit it for the default primary accent.
data-civitai-ui="badge"light<div data-civitai-ui="stack" data-gap="md">
<div data-civitai-ui="group" data-gap="sm">
<span data-civitai-ui="badge" data-variant="filled" data-size="md">Filled</span>
<span data-civitai-ui="badge" data-variant="light" data-size="md">Light</span>
<span data-civitai-ui="badge" data-variant="outline" data-size="md">Outline</span>
</div>
<div data-civitai-ui="group" data-gap="sm">
<span data-civitai-ui="badge" data-variant="filled" data-color="info" data-size="md">Info</span>
<span data-civitai-ui="badge" data-variant="filled" data-color="success" data-size="md">Success</span>
<span data-civitai-ui="badge" data-variant="filled" data-color="warning" data-size="md">Warning</span>
<span data-civitai-ui="badge" data-variant="filled" data-color="error" data-size="md">Error</span>
</div>
<div data-civitai-ui="group" data-gap="sm">
<span data-civitai-ui="badge" data-variant="light" data-size="sm">Small</span>
<span data-civitai-ui="badge" data-variant="light" data-size="md">Medium</span>
<span data-civitai-ui="badge" data-variant="light" data-size="lg">Large</span>
</div>
</div>Alert
data-civitai-ui="alert" with role="alert" (use role="status" for non-urgent). data-color: info (default) · success · warning · error. The body lives in data-civitai-ui-alert-body, with an optional data-civitai-ui-alert-title and an optional dismiss button.
data-civitai-ui="alert"light<div data-civitai-ui="stack" data-gap="sm">
<div data-civitai-ui="alert" data-color="info" role="alert">
<div data-civitai-ui-alert-body>
<div data-civitai-ui-alert-title>Heads up</div>
A new model version is available.
</div>
</div>
<div data-civitai-ui="alert" data-color="success" role="alert">
<div data-civitai-ui-alert-body>
<div data-civitai-ui-alert-title>Saved</div>
Your changes are live.
</div>
<button data-civitai-ui-alert-close aria-label="Dismiss">×</button>
</div>
<div data-civitai-ui="alert" data-color="warning" role="alert">
<div data-civitai-ui-alert-body>Approaching your Buzz limit.</div>
</div>
<div data-civitai-ui="alert" data-color="error" role="alert">
<div data-civitai-ui-alert-body>
<div data-civitai-ui-alert-title>Generation failed</div>
The provider rejected the request.
</div>
</div>
</div>TextInput
Labeled <input>. The wrapper carries data-civitai-ui="text-input"; the label is wired via for/id, help text via aria-describedby, and the invalid state via aria-invalid="true" + data-invalid="true" on the wrapper.
data-civitai-ui="text-input"light<div data-civitai-ui="stack" data-gap="md">
<div data-civitai-ui="text-input">
<label data-civitai-ui-label for="display-name">
Display name
<span data-civitai-ui-required aria-hidden="true">*</span>
</label>
<span id="display-name-desc" data-civitai-ui-description>Shown on your profile.</span>
<input data-civitai-ui-control id="display-name" aria-describedby="display-name-desc" required />
</div>
<div data-civitai-ui="text-input" data-invalid="true">
<label data-civitai-ui-label for="email">Email</label>
<input data-civitai-ui-control id="email" value="not-an-email" aria-invalid="true" aria-describedby="email-err" />
<span id="email-err" data-civitai-ui-error role="alert">Enter a valid email address.</span>
</div>
</div>Textarea
Identical to TextInput, but the control is a resizable <textarea>.
data-civitai-ui="textarea"light<div data-civitai-ui="textarea">
<label data-civitai-ui-label for="prompt">Prompt</label>
<span id="prompt-desc" data-civitai-ui-description>Describe what you want to generate.</span>
<textarea data-civitai-ui-control id="prompt" rows="3" aria-describedby="prompt-desc">a serene alpine lake at dawn</textarea>
</div>NumberInput
Identical to TextInput; the control is <input type="number">.
data-civitai-ui="number-input"light<div data-civitai-ui="number-input">
<label data-civitai-ui-label for="steps">Steps</label>
<span id="steps-desc" data-civitai-ui-description>Sampling steps (1–50).</span>
<input type="number" data-civitai-ui-control id="steps" value="30" min="1" max="50" aria-describedby="steps-desc" />
</div>Select
Labeled native <select> — the same field chrome as TextInput, with the control being a <select data-civitai-ui-control> (the native disclosure caret is retained). This is the framework-agnostic native select, not the interactive JS Select from @civitai/blocks-react.
data-civitai-ui="select"light<div data-civitai-ui="select">
<label data-civitai-ui-label for="base-model">Base model</label>
<span id="base-model-desc" data-civitai-ui-description>Determines available samplers.</span>
<select data-civitai-ui-control id="base-model" aria-describedby="base-model-desc">
<option value="sdxl">SDXL 1.0</option>
<option value="flux" selected>Flux.1 dev</option>
<option value="pony">Pony Diffusion</option>
</select>
</div>Checkbox
A themed native checkbox: the box and its label sit inline in a -choice row, with optional description/error below. accent-color carries the theme tint, so keyboard, focus and indeterminate states are all native.
data-civitai-ui="checkbox"light<div data-civitai-ui="stack" data-gap="md">
<div data-civitai-ui="checkbox">
<div data-civitai-ui-choice>
<input type="checkbox" id="mature" checked aria-describedby="mature-desc" />
<label data-civitai-ui-label for="mature">Show mature content</label>
</div>
<span id="mature-desc" data-civitai-ui-description>You can change this later in settings.</span>
</div>
<div data-civitai-ui="checkbox">
<div data-civitai-ui-choice>
<input type="checkbox" id="newsletter" />
<label data-civitai-ui-label for="newsletter">Email me product updates</label>
</div>
</div>
</div>Radio & RadioGroup
Themed native radios. A RadioGroup wraps a set of Radios in a role="radiogroup" with a group label; give the children a shared name to make them one native radio set. data-orientation="horizontal" lays the options in a row (default is a vertical stack).
data-civitai-ui="radio-group"light<div data-civitai-ui="radio-group" role="radiogroup" aria-labelledby="sampler-lbl">
<span data-civitai-ui-label id="sampler-lbl">Sampler</span>
<div data-civitai-ui-radio-options>
<div data-civitai-ui="radio">
<div data-civitai-ui-choice>
<input type="radio" name="sampler" id="s-euler" checked />
<label data-civitai-ui-label for="s-euler">Euler a</label>
</div>
</div>
<div data-civitai-ui="radio">
<div data-civitai-ui-choice>
<input type="radio" name="sampler" id="s-ddim" />
<label data-civitai-ui-label for="s-ddim">DDIM</label>
</div>
</div>
<div data-civitai-ui="radio">
<div data-civitai-ui-choice>
<input type="radio" name="sampler" id="s-dpm" />
<label data-civitai-ui-label for="s-dpm">DPM++ 2M Karras</label>
</div>
</div>
</div>
</div>Card
Presentational surface container. In light mode a card gets a subtle default hairline (surface == body there, so an unbordered card would be invisible); data-with-border="true" upgrades that to the stronger, fully-opaque border. data-padding: sm · md · lg.
data-civitai-ui="card"light<div data-civitai-ui="group" data-gap="md">
<div data-civitai-ui="card" data-with-border="true" data-padding="md">
<div data-civitai-ui="stack" data-gap="sm">
<strong>With border</strong>
<span>An explicit, fully-opaque border at medium padding.</span>
</div>
</div>
<div data-civitai-ui="card" data-padding="lg">
<div data-civitai-ui="stack" data-gap="sm">
<strong>Default</strong>
<span>The subtle default hairline at large padding.</span>
</div>
</div>
</div>Stack & Group
Layout primitives. Stack lays its children out vertically (top-to-bottom); Group lays them out horizontally (left-to-right, center-aligned). Both take data-gap: sm · md · lg — the numbered chips below make the direction and the gap obvious (the chips are plain filler content, not components).
data-civitai-ui="stack"light<div data-civitai-ui="stack" data-gap="lg">
<div data-civitai-ui="stack" data-gap="sm">
<strong>Stack — vertical, data-gap="md"</strong>
<div data-civitai-ui="stack" data-gap="md">
<div data-demo-chip style="display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;border-radius:8px;background:var(--civitai-color-primary);color:var(--civitai-color-primary-fg);font-weight:700">1</div>
<div data-demo-chip style="display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;border-radius:8px;background:var(--civitai-color-primary);color:var(--civitai-color-primary-fg);font-weight:700">2</div>
<div data-demo-chip style="display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;border-radius:8px;background:var(--civitai-color-primary);color:var(--civitai-color-primary-fg);font-weight:700">3</div>
</div>
</div>
<div data-civitai-ui="stack" data-gap="sm">
<strong>Group — horizontal, data-gap="md"</strong>
<div data-civitai-ui="group" data-gap="md">
<div data-demo-chip style="display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;border-radius:8px;background:var(--civitai-color-primary);color:var(--civitai-color-primary-fg);font-weight:700">1</div>
<div data-demo-chip style="display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;border-radius:8px;background:var(--civitai-color-primary);color:var(--civitai-color-primary-fg);font-weight:700">2</div>
<div data-demo-chip style="display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;border-radius:8px;background:var(--civitai-color-primary);color:var(--civitai-color-primary-fg);font-weight:700">3</div>
</div>
</div>
</div>Loader
Spinner. data-size: sm · md · lg. Decorative inside a button → aria-hidden="true"; standalone → wrap with role="status" + an accessible label.
data-civitai-ui="loader"light<div data-civitai-ui="group" data-gap="lg">
<span data-civitai-ui="loader" data-size="sm"></span>
<span data-civitai-ui="loader" data-size="md"></span>
<span data-civitai-ui="loader" data-size="lg"></span>
<span role="status">
<span data-civitai-ui="loader" data-size="md" aria-hidden="true"></span>
<span style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)">Loading…</span>
</span>
</div>