@doriansmith/kiln
v0.13.1
Published
Accessible React component library for indie developers. WCAG AA, < 25 KB gzipped, zero dependencies, zero config. Install and ship in under 2 minutes.
Maintainers
Readme
Why Kiln
1. Accessible by default
Every component meets WCAG AA out of the box. Keyboard navigation, focus management, focus rings, and correct ARIA are built in — not bolted on. Accessibility debt costs more to fix later than to build correctly now.
2. Performance-first
Kiln components don't tank your Lighthouse score. The production site scores 99 Performance / 100 Accessibility / 100 SEO on Lighthouse (FCP 0.6s, LCP 0.6s, TBT 0ms, CLS 0.005). Zero layout shift on every interaction. All animations use transform and opacity — GPU-accelerated, no layout thrashing. Bundle size is measured and budgeted. These scores are a floor, not a one-time achievement — every new component must maintain them.
3. Genuinely mobile-ready
Every component works on real devices at 375px. All interactive elements meet the 44×44px WCAG touch target requirement. No text below 14px on small screens. Positioned overlays are viewport-constrained. Mobile is not optional.
4. Built for solo devs
From npm install to rendering a Kiln page in under 2 minutes. No config files, no setup wizards, no theme provider components, no required context wrappers. Every code example is copy-paste ready. TypeScript is fully inferred — no required generic annotations.
Install
npm install @doriansmith/kilnPeer deps (already in your project):
npm install react react-domUsage
Import the CSS once at your app root:
import '@doriansmith/kiln/kiln.css';Use components:
import { Button, Input, Card } from '@doriansmith/kiln';
export default function App() {
return (
<Card variant="raised">
<Input label="Email" placeholder="[email protected]" />
<Button variant="primary">Ship it</Button>
</Card>
);
}Dark mode: set data-theme="dark" on <html>. Use the built-in ThemeToggle component for automatic localStorage persistence.
document.documentElement.setAttribute('data-theme', 'dark');Using your own brand colors
Kiln supports named themes — register as many as you need and switch between them with a single call. All themes are accessibility-enforced automatically.
Register and apply themes
import { registerTheme, applyTheme } from 'kiln';
// Register light and dark variants of your brand
registerTheme('brand-light', {
primary: '#2563eb',
accent: '#f59e0b',
mode: 'light',
});
registerTheme('brand-dark', {
primary: '#2563eb', // same hue — Kiln recalculates for dark surface
accent: '#f59e0b',
mode: 'dark',
});
// Apply one
applyTheme('brand-light');
// Switch later (e.g. when user toggles dark mode)
applyTheme('brand-dark');Kiln compiles each theme independently against its surface, so your brand-light and brand-dark primary values will have different lightness — whatever is needed to meet WCAG AA on each background.
Register any number of themes
registerTheme('high-contrast', {
primary: '#000080',
accent: '#cc0000',
mode: 'light',
});
registerTheme('campaign-summer', {
primary: '#e85d04',
accent: '#ffba08',
mode: 'light',
});
// Inspect registered themes
import { getRegisteredThemes } from 'kiln';
getRegisteredThemes(); // ['brand-light', 'brand-dark', 'high-contrast', 'campaign-summer']Preview a theme before applying
import { getThemeTokens } from 'kiln';
const tokens = getThemeTokens('brand-dark');
// { '--kiln-primary': '#4d8fff', '--kiln-primary-fg': '#ffffff', ... }
// Inspect resolved values before committing to the switchCompile without registering (SSR / build-time use)
compileTheme is a pure function — no DOM, no side effects. Use it at build time or in server environments:
import { compileTheme } from 'kiln';
const tokens = compileTheme({ primary: '#2563eb', mode: 'light' });
// Inject as inline styles, generate a CSS file, or pass to a rendererSimple single-theme setup
If you only need one theme and don't want to think about registration:
import { applyKilnTheme } from 'kiln';
applyKilnTheme({
primary: '#2563eb',
accent: '#f59e0b',
mode: 'light',
});HTML attribute API (SSR / no-JS)
<html data-kiln-primary="#2563eb" data-kiln-accent="#f59e0b" data-kiln-mode="light">Kiln picks these up automatically on load. For multi-theme SSR setups, use registerTheme / applyTheme in your JS instead.
Resetting to Kiln defaults
import { resetTheme } from 'kiln';
resetTheme(); // Removes all custom tokens, restores design-tokens.css defaultsWhat gets themed
| Affected | Not affected | |---|---| | Buttons, badges, chips | Status colors (error, warning, success) | | Focus rings, links | Severity indicators | | Primary-tinted surfaces | Neutrals / grays | | Gradients using primary | Typography, spacing, radius |
Note on color adjustment: Kiln may silently adjust your color's lightness to meet contrast requirements. Use
getThemeTokens(name)to inspect the final resolved values before applying.
Note on dark mode: Register a separate
darkvariant of your theme. Kiln does not automatically derive a dark variant — it recalculates from scratch against the dark surface, which produces better results than any automatic inversion.
Note on OS preference detection: Kiln does not watch
prefers-color-schemeautomatically. Wire that up yourself and callapplyTheme()in the handler:const mq = window.matchMedia('(prefers-color-scheme: dark)'); mq.addEventListener('change', e => applyTheme(e.matches ? 'brand-dark' : 'brand-light')); applyTheme(mq.matches ? 'brand-dark' : 'brand-light');
What's included
| Component | Description |
|---|---|
| AppLayout | Full-page app shell. Composes sidebar, tools panel, breadcrumbs, notifications, page header, and split panel. |
| Avatar | Circular user avatar showing an image, auto-derived initials (first + last initial from name), or a fallback icon. 5 sizes, 8 deterministic hue variants, optional dropdown menu — ideal for navbar user menus. |
| Blockquote | Styled pull quote with a left accent border, italic serif text, and optional attribution. Three variants: default, accent, subtle. |
| Breadcrumbs | Hierarchical nav trail with chevron separators and mobile truncation. |
| Button | Primary / secondary / ghost / danger. Loading state, icons, link mode. |
| Checkbox | Custom-styled checkbox with label, helper text, error state, and indeterminate support. Controlled and uncontrolled. Three sizes, animated checkmark draw-on, GPU-accelerated micro-interactions. |
| Input | Label, helper text, error state, left/right icons. ARIA-linked. |
| List | Consecutive items with secondary content, icons, actions, link mode, and drag-to-reorder. Keyboard reorder with live announcements. |
| Textarea | Like Input, plus character counter. |
| Card | Default / raised / glass / gradient-border / coming-soon. Hover lift. |
| Header | Page and section heading block with optional tagline, description, and actions slot. Provides consistent spacing after the Nav. |
| Hero | Full-width page hero section with eyebrow, title, description, actions, and optional media slots. Semantic <section> landmark. Three variants (default / gradient / glass), three sizes, left or centre alignment. |
| Badge | Severity (critical → low) and status (success / warning / error / info / pending / running). |
| Chip | Selectable filter chip. Controlled and uncontrolled. |
| Toggle | Binary switch for boolean settings. Three sizes, controlled and uncontrolled. |
| RadioButton | Single-selection control for mutually exclusive options. Controlled and uncontrolled, with optional description text, disabled, and read-only states. |
| Tabs | Arrow-key navigation, ARIA tablist / tab / tabpanel. |
| Modal | Portal, focus trap, Escape to close, returns focus on dismiss. |
| Nav | Complete drop-in nav bar — logo slot, desktop links, actions slot, and built-in mobile hamburger + slide-out drawer. |
| NavMenu | Desktop-only link strip. Used inside Nav, also available as a primitive for custom nav bars. |
| MobileNav | Standalone mobile hamburger + slide-out drawer. Use with NavMenu when building a custom nav bar without Nav. |
| NotificationBar | Stacked dismissible banners with info / success / warning / error variants and aria-live. |
| SideNav | Grouped vertical nav with active state and keyboard navigation. |
| SplitPanel | Expandable bottom panel with drag-to-resize handle and keyboard resize (arrow keys). |
| Table | Full-featured data table. Column sorting, multi/single row selection, sticky header, column resize, column visibility, loading/empty/error states, and slots for header, filter, pagination, and footer. ARIA grid pattern with optional arrow-key cell navigation. |
| TableOfContents | Sticky ToC with IntersectionObserver active-section tracking. |
| ThemeToggle | Light/dark toggle, persists to localStorage. |
| Toast / ToastContainer | Non-blocking toasts with four severity variants and configurable position. |
| Tooltip | Hover/focus popup label attached to any element. |
| ToolsPanel | Collapsible right panel for help or tools. Desktop slide animation, mobile overlay drawer. |
| Footer | Logo, link list, copyright. |
| GradientText | Inline or block gradient text using background-clip: text. Renders as any HTML element (span by default). Four built-in presets (brand / warm / cool / sunset) plus a custom variant driven by CSS tokens. Forced-colors safe. |
| Grid + GridItem | Responsive CSS grid. Fixed-column mode (4→2→1) or container-aware auto-fit. Span cells with GridItem. |
| LoadingIndicator | Spinner, inline or block. aria-live announcement. |
| ErrorMessage | Error display with optional retry button. |
| ScrollIndicator | Pulsing scroll cue — typically placed at the bottom of a full-page hero. Mouse icon with animated wheel dot and label. Renders as <a>, <button>, or decorative <div> depending on whether href or onClick is provided. |
| ScrollToTop | Scrolls to top on route change. Renders nothing. |
| CodeBlock | Styled <pre><code> with copy button and language label. |
| CopyToClipboard | Zero-intrusion wrapper that copies a value to the clipboard on click and shows a contextual confirmation tooltip adjacent to the trigger. |
| Prose | Reading-optimized typography container. Applies heading hierarchy, paragraph spacing, link styles, blockquote, inline code, and table styles to any HTML content. |
| Section | Full-width layout section with background alternation (default / subtle / emphasis / transparent), constrained inner content, vertical padding scale, optional ambient animation, and optional ARIA landmark. |
Icons
Kiln ships a built-in fill-based icon library. Import icons directly:
import { ChevronDownIcon, CheckCircleIcon, TrashIcon } from '@doriansmith/kiln';
<ChevronDownIcon size={20} />
<CheckCircleIcon size={16} aria-label="Success" />
<TrashIcon className="my-icon" style={{ color: 'red' }} />All icons accept:
| Prop | Type | Default | Description |
|---|---|---|---|
| size | number \| string | 20 | Width and height in px |
| className | string | — | CSS class |
| style | CSSProperties | — | Inline styles |
| aria-label | string | — | Makes icon visible to screen readers; omit for decorative icons |
| aria-hidden | boolean | true | Auto-set to true when no aria-label is provided |
Icon catalogue
Navigation: ChevronDownIcon, ChevronUpIcon, ChevronRightIcon, ChevronLeftIcon, ArrowRightIcon, ArrowLeftIcon, ArrowUpIcon, ArrowDownIcon, MenuIcon, AngleUpIcon, AngleDownIcon, AngleLeftIcon, AngleRightIcon
Status: CheckIcon, CheckCircleIcon, XIcon, XCircleIcon, InfoIcon, WarningIcon, StatusPositiveIcon, StatusNegativeIcon, StatusInfoIcon, StatusWarningIcon, StatusInProgressIcon, StatusPendingIcon, StatusStoppedIcon, StatusNotStartedIcon
Actions: PlusIcon, MinusIcon, TrashIcon, PencilIcon, SearchIcon, ExternalLinkIcon, CopyIcon, SettingsIcon, EyeIcon, EyeOffIcon, EyeOpenIcon, EyeClosedIcon, UploadIcon, DownloadIcon, FilterIcon, SortIcon, AnchorLinkIcon, CalendarIcon, CommandPromptIcon, DeleteMarkerIcon, DotIcon, EditGenAiIcon, EllipsisIcon, FlagIcon, GenAiIcon, HistoryIcon, RefreshIcon, RemoveIcon, RedoIcon, ScriptIcon, SearchGenAiIcon, SendIcon, ShareIcon, SlashIcon, SubtractMinusIcon, SuggestionsIcon, TicketIcon, UndoIcon, UnlockedIcon, UploadDownloadIcon
Content: FileIcon, FolderIcon, ImageIcon, CodeIcon, TerminalIcon, LinkIcon, BookmarkIcon, TagIcon, FileOpenIcon, FolderOpenIcon
Social: UserIcon, UsersIcon, BellIcon, StarIcon, HeartIcon, FaceSmileIcon, FaceNeutralIcon, FaceFrownIcon, GroupIcon, HeartFilledIcon, StarHalfIcon, StarFilledIcon, UserProfileIcon
Miscellaneous: DragHandleIcon, ShieldIcon, ZapIcon, SmartphoneIcon, GridIcon, ListIcon, KeyIcon, KeyboardIcon, LocationPinIcon, MapIcon
Theme: SunIcon, MoonIcon, LightDarkIcon
Communication: CallIcon, ContactIcon, EnvelopeIcon, GlobeIcon, BugIcon, LockPrivateIcon, SecurityIcon, AtSymbolIcon, MicrophoneIcon, MicrophoneOffIcon
Media: AudioFullIcon, AudioHalfIcon, AudioOffIcon, Backward10SecondsIcon, Forward10SecondsIcon, ClosedCaptionIcon, ClosedCaptionUnavailableIcon, MiniPlayerIcon, MultiscreenIcon, PauseIcon, PlayIcon, StopCircleIcon, TranscriptIcon, VideoOffIcon, VideoOnIcon, VideoUnavailableIcon, VideoCameraOffIcon, VideoCameraOnIcon, VideoCameraUnavailableIcon
UI: CaretDownFilledIcon, CaretDownIcon, CaretLeftFilledIcon, CaretRightFilledIcon, CaretUpFilledIcon, CaretUpIcon, ExpandIcon, ShrinkIcon, FullScreenIcon, ExitFullScreenIcon, TreeviewCollapseIcon, TreeviewExpandIcon, ZoomInIcon, ZoomOutIcon, ZoomToFitIcon, ResizeAreaIcon, ViewFullIcon, ViewHorizontalIcon, ViewVerticalIcon, InsertRowIcon, GridViewIcon, ListViewIcon
Nav
A complete, drop-in navigation bar. Renders a sticky <header> with a logo slot on the left, a desktop link strip in the centre, a right-side actions slot, and a fully accessible mobile hamburger with a focus-trapped slide-out drawer — all from a single component.
import { Nav, ThemeToggle } from '@doriansmith/kiln';
const NAV_ITEMS = [
{ href: '/', label: 'Home' },
{ href: '/docs', label: 'Docs' },
{ href: '/about', label: 'About' },
];
<Nav
logo={<img src="/logo.png" alt="MyApp" style={{ height: 32 }} />}
items={NAV_ITEMS}
isActive={(href) => window.location.pathname === href}
onNavigate={(href) => navigate(href)}
actions={<ThemeToggle />}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| logo | React.ReactNode | — | Left-side brand slot |
| items | NavItem[] | [] | { href, label, icon? } links — rendered on desktop and in the mobile drawer |
| actions | React.ReactNode | — | Right-side slot (ThemeToggle, avatar, CTAs, etc.) |
| sticky | boolean | true | Sticks the bar to the top of the viewport |
| isActive | (href) => boolean | pathname match | Returns true to mark a link active (aria-current="page") |
| onNavigate | (href, e) => void | — | Called on any link click — call e.preventDefault() for client-side routing |
| ariaLabel | string | 'Main navigation' | Accessible label for the nav landmark and mobile dialog |
For full control over the nav bar layout, use the lower-level NavMenu and MobileNav primitives instead.
AppLayout
A full-page application shell — drop it in once and get sidebar, tools panel, breadcrumbs, notifications, and a sticky top bar wired up and accessible.
import { AppLayout, Nav, SideNav, ThemeToggle } from '@doriansmith/kiln';
// SideNav manages its own open/collapse state internally
<AppLayout
topBar={<Nav logo={logo} items={navItems} actions={<ThemeToggle />} />}
sideBar={<SideNav groups={navGroups} activeId={activeId} onSelect={setActiveId} />}
breadcrumbs={[{ label: 'Home', href: '/' }, { label: 'Dashboard' }]}
header={<h1>Dashboard</h1>}
>
<YourPageContent />
</AppLayout>Slots
| Prop | Description |
|---|---|
| topBar | Sticky header (renders inside <header role="banner">) |
| sideBar | Collapsible left panel — pass a <SideNav> that manages its own open/collapse state |
| toolsPanel | Collapsible right panel — toggle via toolsOpen / onToolsChange |
| header | Page header rendered above children inside the content column |
| children | Main page content (rendered inside <main>) |
| splitPanel | Expandable bottom panel with its own toggle button |
Notifications
Pass an array of AppLayoutNotification objects for dismissible banners above the content area:
const [notes, setNotes] = useState([
{ id: '1', type: 'success', message: 'Deployed.', dismissible: true,
onDismiss: (id) => setNotes((n) => n.filter((x) => x.id !== id)) },
]);
<AppLayout notifications={notes}>...</AppLayout>type is 'info' | 'success' | 'warning' | 'error'.
CSS token overrides
Override panel widths via inline style:
<AppLayout
style={{
'--kiln-app-layout-sidebar-width': '200px',
'--kiln-tools-panel-width': '320px',
} as React.CSSProperties}
>
...
</AppLayout>Responsive behaviour
- Desktop (≥ 768px): sideBar and toolsPanel slide in/out without overlapping content.
- Mobile (< 768px): both panels become fixed overlay drawers with a backdrop. SideBar closes on Escape or backdrop click.
Header
A consistent page and section heading block. Eliminates custom per-page hero layouts — use Header everywhere a heading is needed and spacing is automatically correct.
import { Header, Button } from '@doriansmith/kiln';
// Page-level header with decorative brand tagline
<section aria-labelledby="page-heading">
<Header
id="page-heading"
variant="h1"
tagline="Analytics"
description="Monitor usage, performance, and billing across all workspaces."
>
Dashboard
</Header>
</section>
// Section header with inline actions
<Header
variant="h2"
description="Manage access for your organisation."
actions={
<>
<Button variant="secondary">Export CSV</Button>
<Button variant="primary">Add user</Button>
</>
}
divider
>
Users
</Header>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | 'h1' \| 'h2' \| 'h3' | 'h2' | Heading level and visual size. h1 centres content for page heroes; h2/h3 are left-aligned for sections. |
| tagline | string | — | Large decorative display text above the heading, rendered with the Kiln brand gradient. Hidden from assistive technology. |
| description | React.ReactNode | — | Subtitle rendered below the heading in muted text. |
| actions | React.ReactNode | — | Right-aligned slot for buttons or badges. Centres below the description for h1. |
| divider | boolean | false | Renders a hairline separator below the header block. |
| id | string | — | Forwards to the heading element — pair with aria-labelledby on the parent <section>. |
| style | React.CSSProperties | — | Use --kiln-header-max-width (default 1100px) and --kiln-header-padding-x to customise the container. |
Card — coming-soon variant
Pass variant="coming-soon" to render a self-contained "work in progress" placeholder. No children required.
import { Card } from '@doriansmith/kiln';
// Default text
<Card variant="coming-soon" />
// Custom text
<Card
variant="coming-soon"
title="Under construction"
description="This section will be ready soon."
/>Use --kiln-card-coming-soon-max-width to override the default 440 px maximum width.
NavMenu + MobileNav
Use these when Nav's layout doesn't fit your design and you need to assemble your own nav bar.
NavMenu — the desktop-only link strip (a <nav> with styled links and active-state handling).MobileNav — a self-contained hamburger button + focus-trapped slide-out drawer.
import { NavMenu, MobileNav } from '@doriansmith/kiln';
// Desktop link strip
<NavMenu
items={NAV_ITEMS}
isActive={(href) => location.pathname === href}
onNavigate={(href) => navigate(href)}
/>
// Mobile hamburger + drawer
<MobileNav
items={NAV_ITEMS}
logo={<img src="/logo.png" alt="MyApp" style={{ height: 32 }} />}
isActive={(href) => location.pathname === href}
onNavigate={(href) => navigate(href)}
/>For most cases, use Nav — it handles all of the above automatically.
Breadcrumbs
Hierarchical navigation trail. Last item is the current page (no link, aria-current="page").
import { Breadcrumbs } from '@doriansmith/kiln';
<Breadcrumbs
items={[
{ label: 'Home', href: '/' },
{ label: 'Projects', href: '/projects' },
{ label: 'Kiln' },
]}
/>NotificationBar
Stacked dismissible banners. Pass dismissible: true and onDismiss to enable per-item removal. Announces changes via aria-live.
import { NotificationBar } from '@doriansmith/kiln';
const [items, setItems] = useState([
{ id: '1', type: 'success', message: 'Deployed successfully.', dismissible: true },
]);
const dismiss = (id) => setItems((n) => n.filter((x) => x.id !== id));
<NotificationBar items={items.map((n) => ({ ...n, onDismiss: dismiss }))} />type is 'info' | 'success' | 'warning' | 'error'.
RadioButton
Single-selection control for mutually exclusive options. Custom-styled indicator with micro-interactions, optional description text, and full support for controlled and uncontrolled modes.
import { RadioButton } from '@doriansmith/kiln';
import { useState } from 'react';
// Uncontrolled
<RadioButton name="size">Small</RadioButton>
<RadioButton name="size" defaultChecked>Medium</RadioButton>
// With helper description
<RadioButton name="plan" description="Unlimited projects, $12/mo" defaultChecked>
Pro
</RadioButton>
// Controlled group
const [plan, setPlan] = useState('pro');
<RadioButton name="plan" value="starter" checked={plan === 'starter'} onChange={() => setPlan('starter')} description="Up to 3 projects, free forever">Starter</RadioButton>
<RadioButton name="plan" value="pro" checked={plan === 'pro'} onChange={() => setPlan('pro')} description="Unlimited projects, $12/mo">Pro</RadioButton>
<RadioButton name="plan" value="team" checked={plan === 'team'} onChange={() => setPlan('team')} description="Custom limits, contact sales">Team</RadioButton>
// Disabled / read-only
<RadioButton name="locked" disabled>Disabled</RadioButton>
<RadioButton name="locked" readOnly defaultChecked>Read-only</RadioButton>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children | React.ReactNode | — | Label text rendered beside the indicator |
| checked | boolean | — | Controlled checked state |
| defaultChecked | boolean | false | Initial checked state (uncontrolled) |
| onChange | (event: ChangeEvent<HTMLInputElement>) => void | — | Fired when the radio is selected |
| disabled | boolean | false | Prevents interaction and dims the control |
| readOnly | boolean | false | Visually normal but blocks user interaction |
| description | string | — | Helper text below the label; auto-linked via aria-describedby |
| name | string | — | Radio group name — required for mutual exclusion |
| value | string | — | Value submitted with the form |
| id | string | auto | Explicit id; auto-generated when omitted |
| className | string | '' | Additional CSS classes on the root element |
| style | React.CSSProperties | — | Inline styles; use for CSS custom property overrides |
SplitPanel
Expandable bottom panel with drag-to-resize handle and keyboard resize (↑/↓ arrow keys in 20px steps).
import { SplitPanel } from '@doriansmith/kiln';
<SplitPanel header="Logs" defaultOpen defaultHeight={240} resizable>
<LogViewer />
</SplitPanel>ToolsPanel
Collapsible right-side panel anchored to the right edge.
import { ToolsPanel } from '@doriansmith/kiln';
<ToolsPanel header="Help" defaultOpen>
<HelpArticle />
</ToolsPanel>Token override: --kiln-tools-panel-width (default 280px).
Grid
Two modes, zero breakpoint config.
Fixed columns — cols
Declare the max column count. Kiln collapses it automatically: 4 cols at desktop, 2 at tablet, 1 on mobile.
import { Grid, GridItem } from '@doriansmith/kiln';
<Grid cols={4} gap="md">
{cards}
</Grid>Auto-fit — minColWidth
Tell Kiln how wide each item should be. The browser calculates the column count from the container width — no breakpoints, no config. Works inside sidebars, modals, and any nested layout.
// 1100px container → ~4 cols. 600px → 2 cols. 300px → 1 col. No code change.
<Grid minColWidth={260} gap="md">
{cards}
</Grid>GridItem — spanning cells
<Grid cols={3} gap="md">
<GridItem colSpan={2}><Card>Wide</Card></GridItem>
<Card>Narrow</Card>
</Grid>colSpan is responsive-safe: it caps to 2 at tablet and resets to 1 on mobile so items never overflow implicit columns.
Dense packing
<Grid cols={4} gap="sm" dense>
{photos}
</Grid>dense enables grid-auto-flow: dense — fills gaps when items vary in height, useful for image galleries.
Gap tokens
| gap prop | Value |
|---|---|
| none | 0 |
| xs | 0.5rem |
| sm | 1rem |
| md | 1.5rem (default) |
| lg | 2rem |
| xl | 3rem |
Override per-instance with the --kiln-grid-gap CSS token:
<Grid cols={3} style={{ '--kiln-grid-gap': '2rem' } as React.CSSProperties}>
{cards}
</Grid>CopyToClipboard
A zero-intrusion wrapper that copies value to the clipboard when any child element is clicked, then shows a small confirmation tooltip immediately adjacent to the trigger. The child element keeps full ownership of its role, aria-label, and keyboard handling.
import { CopyToClipboard, Button } from '@doriansmith/kiln';
// Wrap any element — one prop is all that's required
<CopyToClipboard value="<SearchIcon />">
<Button variant="ghost" aria-label="Copy SearchIcon import">
<SearchIcon />
</Button>
</CopyToClipboard>
// Tooltip placement
<CopyToClipboard value={shareUrl} placement="right" successMessage="Link copied!">
<button type="button" aria-label="Copy share link">
<LinkIcon size={16} />
</button>
</CopyToClipboard>
// Inline code snippet
<CopyToClipboard value="npm install @doriansmith/kiln" placement="bottom">
<code style={{ cursor: 'pointer' }}>npm install @doriansmith/kiln</code>
</CopyToClipboard>
// Callbacks and custom duration
<CopyToClipboard
value={apiKey}
placement="right"
successMessage="API key copied!"
duration={3000}
onCopy={(v) => analytics.track('copy', { value: v })}
onError={(err) => console.error(err)}
>
<Input label="API key" value={apiKey} readOnly />
</CopyToClipboard>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | Required. String written to the clipboard on click. |
| children | React.ReactNode | — | Required. The trigger element. Retains its own role, label, and keyboard events. |
| placement | 'top' \| 'bottom' \| 'left' \| 'right' | 'top' | Which side of the trigger the tooltip appears on. |
| duration | number | 2000 | Milliseconds the tooltip stays visible before fading out. |
| successMessage | string | 'Copied!' | Tooltip text after a successful clipboard write. |
| errorMessage | string | 'Failed to copy' | Tooltip text when the write fails. |
| onCopy | (value: string) => void | — | Called with the copied value after success. |
| onError | (err: unknown) => void | — | Called with the caught error on failure. |
| className | string | — | Extra classes on the wrapper <span>. |
Accessibility
- An always-mounted
role="status"+aria-live="polite"region announces the result to screen readers without racing against DOM mount timing. - The visual tooltip is marked
aria-hidden="true"to prevent double-announcement. - Copy is triggered via event bubbling — child buttons handle their own keyboard (
Enter/Space) events naturally.
Dark mode
The tooltip automatically inverts to a light surface when data-theme="dark" is set on <html> — no extra configuration needed.
Blockquote
A styled pull quote with a left accent border, italic serif text, and optional attribution.
import { Blockquote } from '@doriansmith/kiln';
// Default — primary accent border
<Blockquote cite="Dieter Rams">
Good design is as little design as possible.
</Blockquote>
// Accent — border + tinted background
<Blockquote variant="accent" cite="Paul Rand">
Design is the silent ambassador of your brand.
</Blockquote>
// Subtle — gray border, no background
<Blockquote variant="subtle">
The details are not the details. They make the design.
</Blockquote>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children | React.ReactNode | — | Required. The quoted content |
| cite | React.ReactNode | — | Attribution — author or source. Rendered in a <footer> with an em-dash prefix. |
| variant | 'default' \| 'accent' \| 'subtle' | 'default' | Visual treatment of the left border and background |
| className | string | '' | Additional CSS classes |
| style | React.CSSProperties | — | Inline styles. Use --kiln-blockquote-border-color, --kiln-blockquote-border-width, --kiln-blockquote-bg to override tokens. |
Section
A full-width layout primitive for page structure. Use it to alternate background colors between page sections, constrain content width, and optionally mark sections as ARIA landmarks.
import { Section } from '@doriansmith/kiln';
<Section background="default" padding="lg" aria-label="Features">
<h2>Everything you need</h2>
<p>Ship fast without compromise.</p>
</Section>
<Section background="subtle" padding="lg" aria-label="Pricing">
<PricingTable />
</Section>
<Section background="emphasis" padding="md" maxWidth="800px">
<Testimonial />
</Section>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| background | 'default' \| 'subtle' \| 'emphasis' \| 'transparent' | 'default' | Background fill variant |
| padding | 'none' \| 'sm' \| 'md' \| 'lg' | 'md' | Vertical padding scale |
| maxWidth | string \| 'full' | '1200px' | Max-width of the inner content container. Pass 'full' to disable. |
| aria-label | string | — | Accessible label. When set, adds role="region" automatically — making the section a navigable landmark. |
| className | string | '' | Additional CSS classes |
| style | React.CSSProperties | — | Inline styles. Use --kiln-section-bg to override the background token. |
| children | React.ReactNode | — | Required. Section content |
GradientText
An inline or block gradient text element using background-clip: text. Renders as any HTML element — default span for inline use inside a heading, or set as="h1" to render a standalone gradient heading.
import { GradientText } from '@doriansmith/kiln';
// Inline accent inside a heading
<h1>
Ship fast <GradientText variant="brand">without compromise</GradientText>
</h1>
// Standalone gradient heading
<GradientText as="h1" variant="brand" style={{ fontSize: '3rem', fontWeight: 800 }}>
Your tagline here
</GradientText>
// Other presets
<GradientText variant="warm">Golden hour</GradientText>
<GradientText variant="cool">Indie developers</GradientText>
<GradientText variant="sunset">Zero config</GradientText>
// Custom colours via CSS tokens
<GradientText
variant="custom"
style={{
'--kiln-gradient-text-from': '#10b981',
'--kiln-gradient-text-to': '#3b82f6',
}}
>
Your brand colors
</GradientText>
// Custom angle
<GradientText variant="brand" angle={90}>Vertical sweep</GradientText>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | 'brand' \| 'warm' \| 'cool' \| 'sunset' \| 'custom' | 'brand' | Gradient preset. 'custom' reads --kiln-gradient-text-from and --kiln-gradient-text-to from inline style or CSS. |
| as | 'span' \| 'p' \| 'h1'–'h6' \| 'div' \| 'strong' \| 'em' | 'span' | HTML element to render. |
| angle | number | 135 | Gradient direction in degrees. |
| className | string | — | Additional CSS classes. |
| style | React.CSSProperties | — | Inline styles. Pass --kiln-gradient-text-from / --kiln-gradient-text-to for the custom variant. |
| children | React.ReactNode | — | Required. Text content. |
ScrollIndicator
A pulsing scroll cue — typically placed at the bottom of a full-page hero. Renders a mouse icon with an animated wheel dot and an uppercase label. Automatically chooses its element type: <a> when href is set, <button> when onClick is set, or a decorative <div> when neither is provided.
import { ScrollIndicator } from '@doriansmith/kiln';
// Decorative — no interaction
<ScrollIndicator />
// Anchor scroll
<ScrollIndicator href="#features" label="See features" />
// Button with smooth-scroll
<ScrollIndicator
onClick={() => document.getElementById('content')?.scrollIntoView({ behavior: 'smooth' })}
/>
// Light variant for dark/gradient backgrounds
<ScrollIndicator variant="light" href="#about" />
// Inside a full-page Hero actions slot
<Hero
fullPage navOffset variant="gradient"
title="Your product. Shipped."
actions={
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--kiln-space-6)' }}>
<Button variant="primary" size="lg">Get started</Button>
<ScrollIndicator variant="light" href="#features" label="See what we build" />
</div>
}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | 'default' \| 'light' \| 'ghost' | 'default' | Visual style. 'light' uses white tones for dark/gradient backgrounds. |
| label | string | 'Scroll down' | Accessible label and visible text. |
| href | string | — | Anchor href. When set, renders as <a>. |
| onClick | () => void | — | Click handler. When set (no href), renders as <button>. |
| className | string | — | Additional CSS classes. |
| style | React.CSSProperties | — | Inline styles. Token overrides: --kiln-scroll-indicator-color, --kiln-scroll-indicator-size, --kiln-scroll-indicator-pulse-color. |
Prose
A reading-optimized typography container. Drop any HTML content inside and get consistent heading hierarchy, paragraph spacing, link styles, blockquote treatment, inline code, and table styles — without writing custom CSS.
import { Prose } from '@doriansmith/kiln';
<Prose>
<h1>Article title</h1>
<p className="lead">A short intro paragraph styled larger and lighter.</p>
<p>Body copy with <a href="#">links</a> and <code>inline code</code>.</p>
<blockquote>
<p>A pull quote with the Kiln accent border.</p>
</blockquote>
<h2>Sub-heading</h2>
<ul>
<li>Bullet one</li>
<li>Bullet two</li>
</ul>
</Prose>
// Smaller text for annotations
<Prose size="sm" maxWidth="full">
<p>Terms and conditions apply.</p>
</Prose>
// Full-width (disables the 68ch line-length cap)
<Prose size="lg" maxWidth="full">
<MDXContent />
</Prose>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children | React.ReactNode | — | Required. Any HTML content — headings, paragraphs, lists, code, blockquotes, images, tables |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Base font size. Scales the entire type hierarchy proportionally. |
| maxWidth | string \| 'full' | '68ch' | Reading line length. 68ch is optimal for body copy. Pass 'full' to disable. |
| className | string | '' | Additional CSS classes |
| style | React.CSSProperties | — | Inline styles. Token overrides: --kiln-prose-color, --kiln-prose-heading-color, --kiln-prose-link-color, --kiln-prose-lead-color. |
Using with MDX
import { Section, Prose } from '@doriansmith/kiln';
export function MDXLayout({ children }) {
return (
<Section background="default" padding="lg">
<Prose>{children}</Prose>
</Section>
);
}Avatar
Circular user avatar for navbars and user-facing UIs. Shows an image, auto-derived initials, or a fallback icon. Becomes a trigger for a dropdown menu when menuItems is provided.
import { Avatar } from '@doriansmith/kiln';
// Initials — derived from name automatically
<Avatar name="Dorian Smith" size="md" />
// Image (falls back to initials on error)
<Avatar src="/avatar.jpg" name="Dorian Smith" size="md" />
// Navbar user menu — the most common pattern
<Avatar
name={user.name}
src={user.avatarUrl}
size="sm"
menuItems={[
{ label: 'Profile', onSelect: () => navigate('/profile') },
{ label: 'Settings', onSelect: () => navigate('/settings') },
{ type: 'separator' },
{ label: 'Sign out', onSelect: () => auth.signOut(), variant: 'danger' },
]}
menuAlign="end"
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| name | string | — | Full name. Initials are derived automatically (first letter of first and last word). Also the accessible label. |
| initials | string | — | Explicit 1–2 character initials. Overrides derivation from name. |
| src | string | — | Image URL. Takes priority over initials. Falls back to initials on load error. |
| alt | string | name | Alt text for the avatar image. |
| size | 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' | 'md' | Circle diameter. |
| menuItems | DropdownMenuEntry[] | — | When provided, the avatar becomes a button that opens a dropdown menu. Same entry format as DropdownMenu. |
| menuAlign | 'start' \| 'end' | 'end' | Horizontal alignment of the dropdown. |
| menuSide | 'bottom' \| 'top' | 'bottom' | Which side the dropdown opens on. |
| menuAriaLabel | string | '[name] menu' | Accessible label for the trigger button and menu. |
Checkbox
A custom-styled checkbox with animated checkmark, indeterminate state, helper/error text, and full WCAG AA compliance.
import { Checkbox } from '@doriansmith/kiln';
import { useState } from 'react';
// Uncontrolled
<Checkbox label="Remember me" defaultChecked />
// Controlled with error
const [agreed, setAgreed] = useState(false);
<Checkbox
label="Accept terms"
checked={agreed}
onChange={(val) => setAgreed(val)}
errorText={!agreed ? 'You must accept to continue.' : undefined}
/>
// With helper text
<Checkbox
label="Subscribe to updates"
helperText="We'll only send important product news."
/>
// Sizes
<Checkbox label="Small" size="sm" />
<Checkbox label="Medium" size="md" /> {/* default */}
<Checkbox label="Large" size="lg" />
// Indeterminate — select-all pattern
<Checkbox
label="Select all"
indeterminate={someSelected && !allSelected}
checked={allSelected}
onChange={(val) => setAllSelected(val)}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | — | Visible label text and accessible name. |
| labelHidden | boolean | false | Hide the label visually while keeping it for screen readers. |
| checked | boolean | — | Controlled checked state. |
| defaultChecked | boolean | false | Uncontrolled initial state. |
| indeterminate | boolean | false | Shows a dash instead of a tick. Use for select-all when only some items are selected. |
| onChange | (checked: boolean, e: ChangeEvent) => void | — | Fired on every state change. |
| disabled | boolean | false | Prevents interaction and dims the control. |
| helperText | string | — | Helper text below the label. Hidden when errorText is set. |
| errorText | string | — | Error message. Sets aria-invalid and shows a red shake state. |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Visual size of the box and label. |
| id | string | auto | id for the underlying input. Auto-generated when omitted. |
| className | string | — | Additional CSS classes on the root wrapper. |
TypeScript
All props are fully typed. Named type exports:
import type {
AvatarProps, AvatarSize,
ButtonVariant, ButtonSize,
CardVariant,
BadgeVariant, BadgeSeverity, BadgeStatus, BadgeSize,
CheckboxProps, CheckboxSize,
RadioButtonProps,
TabItem, NavItem, CodeBlockProps,
BreadcrumbItem,
NotificationBarItem, NotificationBarType,
ToolsPanelProps, SplitPanelProps,
AppLayoutBreadcrumb, AppLayoutNotification,
CopyToClipboardProps, CopyStatus, CopyPlacement,
BlockquoteProps, BlockquoteVariant,
GradientTextProps, GradientTextVariant, GradientTextAs,
ScrollIndicatorProps, ScrollIndicatorVariant,
SectionProps, SectionBackground,
ProseProps, ProseSize,
} from '@doriansmith/kiln';No generic annotations required anywhere.
Bundle size
| Artifact | Minified | Gzipped |
|---|---|---|
| kiln.css | ~74 KB | ~10 KB |
| index.js (ESM) | ~36 KB | ~13 KB |
Total gzipped: < 26 KB. Budget: 50 KB gzipped.
Status
v0.3.0 — Stable. APIs are stable. The visual style is opinionated and will not change without a major version bump.
Roadmap
Future releases
- Storybook component explorer
- GSAP-enhanced animation variants (optional peer dependency — base components remain CSS-only)
- Additional primitives: Select, Combobox, DatePicker
- Component-level token documentation
License
MIT
