npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

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/kiln

Peer deps (already in your project):

npm install react react-dom

Usage

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 switch

Compile 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 renderer

Simple 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 defaults

What 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 dark variant 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-scheme automatically. Wire that up yourself and call applyTheme() 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