Skip to content
kurobeni design
Sign in
kurobeni design

Components

Reference

Import from @kurobeni/design/react, or write the classes yourself. A check on every commit confirms that each class a component emits exists in the CSS.

Page

Root, bands, column and chrome

Kurobeni

The root, once per page in the root layout. Applies a stored theme before paint, writes its knobs as custom properties and mounts the tooltip handler. Renders no wrapper; the page ground is on body. knobsCss(knobs, selector) returns the same CSS for another scope. motion none stops all transitions except the spinner and skeleton.

<Kurobeni accent="#c2163b">
  <ToastProvider>{children}</ToastProvider>
</Kurobeni>
<!-- applies a stored theme before first paint -->
<script src="https://cdn.kurobeni.jp/design/head.js"></script>
<link rel="stylesheet" href="https://cdn.kurobeni.jp/design/kurobeni.css" />

<!-- knobs are custom properties, on :root or any scope -->
<style>
  :root { --kb-accent: #c2163b; --kb-round: 1; }
</style>

<!-- runtime for the interactive components -->
<script src="https://cdn.kurobeni.jp/design/kurobeni.js" defer></script>
PropTypeNote
accentstringany CSS colour with 4.5:1 contrast against white text
densitynumbermultiplies every space step; controls keep their size
roundnumbermultiplies every radius; 0 is square
fontSans, fontDisplay, fontMonostringfont-family lists
motion'full' | 'none'default 'full'
langstringpage language; 'ja' puts the built-in strings of client components (theme switch, toasts, dialogs, scroll hint, empty lists) in Japanese; others take a label prop
Band

Full-width section in one tone, with content in a Col. The tone re-mixes every colour inside it, so contrast holds. Any other tone name works if you define --kb-ground, --kb-surface and --kb-ink for it.

<Band tone="deep" id="notes" labelledBy="notes-title">
  <Col>
    <Head title="Notes" id="notes-title" />
  </Col>
</Band>

A panel on sunk

Healthy

A panel on deep

Healthy

A panel on accent

Healthy
<!-- data-tone: sunk, deep or accent; omit for the page ground.
     kb-band--fill fills the viewport, for app screens. -->
<section class="kb-band" data-tone="deep" id="notes" aria-labelledby="notes-title">
  <div class="kb-col">
    <h2 class="kb-head__title" id="notes-title">Notes</h2>
  </div>
</section>
PropTypeNote
tone'sunk' | 'deep' | 'accent'omit for the page ground
fillbooleanfills the viewport, for app screens
density'compact' | 'comfortable' | 'auto'scales the space; auto is compact under 768px
asElementTypedefault 'section'
labelledBystringid of the heading inside
Col

Centred content column, 1200px wide.

<div class="kb-col">…</div>
PropTypeNote
classNamestring
Head with a mark

Head for a person or entity, with an avatar or other mark. The mark is centred on the whole block, not the title baseline.

AT

Aoi Tanaka

Member

@aoi

<div class="kb-head kb-head--mark">
  <div class="kb-head__mark">…</div>
  <div class="kb-head__body">
    <div class="kb-head__line">
      <h2 class="kb-head__title">Aoi Tanaka</h2>
      <p class="kb-head__aside">Member</p>
    </div>
    <p class="kb-head__note">@aoi</p>
  </div>
</div>
Rail

Main column with a 280px side track. at sets which side the track is on. Below about 960px of space they stack in source order.

The main column takes the rest of the width.

<div class="kb-rail">
  <div class="kb-rail__main">…</div>
  <aside class="kb-rail__side">…</aside>
</div>
PropTypeNote
sideReactNode
at'start' | 'end'default 'end'
Panel

Surface with a hairline border, for a form, table or work pane. Do not nest panels.

A panel. PanelBody gives it its inset.

<div class="kb-panel">…</div>
PropTypeNote
asElementTypedefault 'div'
classNamestring
Main

The main landmark.

<main id="main">…</main>
PropTypeNote
idstringdefault 'main'
Bar

Sticky site header: brand, section links, actions and children. Deep tone with a beni rule under it unless tone is set. brand.seal puts a seal before the name. The current link is shown inverted. With menu, phones show the brand, the primary action and a menu button that opens a sheet in the bar's tone: large ruled links, any SiteMenuGroups, menu.actions as full-width buttons, and menu.foot for language and theme. In the sheet the current page is inverted and a current: 'section' link gets the beni bar. Without menu, links move to their own row on phones and scroll sideways if needed. variant slim makes every control small, for a page with a link or two; pair it with a mark brand. The system sets the scroll padding the bar needs.

<Bar
  brand={{ name: 'kurobeni design', seal: '紅', href: '/' }}
  items={items}
  link={Link}
  actions={
    <>
      <ButtonLink href="/sign-in" size="sm">Sign in</ButtonLink>
      <ButtonLink href="/sign-up" size="sm" variant="primary">Sign up</ButtonLink>
    </>
  }
  menu={{
    children: <SiteMenuGroup label="Account" items={account} link={Link} />,
    actions: <ButtonLink href="/sign-up" size="lg" variant="primary" block>Sign up</ButtonLink>,
    foot: (
      <>
        <LangLink href="/ja/" lang="ja">日本語</LangLink>
        <ThemeSwitch text />
      </>
    ),
  }}
>
  <LangLink href="/ja/" lang="ja">日本語</LangLink>
  <ThemeSwitch />
</Bar>
<!-- data-tone="deep": dark bar with a beni rule under it -->
<header class="kb-bar" data-tone="deep">
  <a class="kb-bar__brand" href="/">
    <span class="kb-seal" aria-hidden="true">紅</span>
    <span class="kb-bar__name">kurobeni design</span>
    <span class="kb-bar__domain">kurobeni.jp</span>
  </a>
  <nav class="kb-bar__nav" aria-label="Primary">
    <a href="/works" aria-current="page">Works</a>
    <a href="/notes">Notes</a>
  </nav>
  <div class="kb-cluster kb-bar__actions">
    <a class="kb-btn kb-btn--sm kb-btn--primary" href="/sign-in">Sign in</a>
  </div>
  <!-- runtime adds the icon and click handler -->
  <button class="kb-btn kb-btn--icon kb-btn--quiet" type="button" data-kb-theme aria-label="Theme"></button>
  <!-- last: on phones only the brand, the primary button and this stay -->
  <button class="kb-btn kb-btn--icon kb-btn--quiet kb-bar__menu" type="button"
    data-kb-dialog="site-menu" aria-haspopup="dialog" aria-controls="site-menu" aria-label="Menu"></button>
</header>

<!-- menu sheet; following a link closes it -->
<dialog class="kb-dialog kb-drawer kb-sitemenu" id="site-menu" aria-label="Menu" data-tone="deep">
  <div class="kb-dialog__head">
    <a class="kb-bar__brand" href="/">…</a>
    <button class="kb-btn kb-btn--icon kb-btn--quiet" type="button" data-kb-close aria-label="Close"></button>
  </div>
  <div class="kb-dialog__body kb-drawer__body">
    <nav aria-label="Primary">
      <ul class="kb-sitemenu__nav">
        <li><a href="/works" aria-current="page">Works</a></li>
        <li><a href="/notes">Notes</a></li>
      </ul>
    </nav>
    <div class="kb-sitemenu__group">
      <p class="kb-label">Account</p>
      <ul class="kb-sitemenu__nav">…</ul>
    </div>
    <div class="kb-sitemenu__actions">
      <a class="kb-btn kb-btn--lg kb-btn--block kb-btn--primary" href="/sign-in">Sign in</a>
    </div>
  </div>
  <div class="kb-dialog__foot">
    <a class="kb-lang" href="/ja/" hreflang="ja" lang="ja">日本語</a>
    <button class="kb-btn kb-btn--quiet" type="button" data-kb-theme="text"></button>
  </div>
</dialog>

<!-- kb-bar--full: full width, for app screens -->
<header class="kb-bar kb-bar--full" data-tone="deep">…</header>

<!-- kb-bar--slim: small controls, for a link or two and the theme switch -->
<header class="kb-bar kb-bar--slim" data-tone="deep">
  <a class="kb-bar__brand kb-bar__brand--mark" href="/" aria-label="Home">…</a>
  <nav class="kb-bar__nav" aria-label="Primary">…</nav>
</header>
PropTypeNote
brand{ href, name?, domain?, seal?, mark?, label? }a mark needs a label
itemsNavItem[]{ href, label, current?: true | 'section' }
actionsReactNodee.g. sign in, sign up, account menu
menu{ children?, actions?, foot?, label?, id? }phone menu sheet
variant'slim' | 'full'slim: small controls; full: full width
toneTone | nulldefault 'deep'; null for the page's ground
labelstringnav label, default 'Primary'
linkLinkComponent | 'a'next/link or a plain anchor
Seal

Decorative and hidden from assistive technology, so put the name in text beside it. 28px, or 48px with size. Inverts on the accent tone.

<Seal>紅</Seal>
<!-- square beni mark: one character or two letters -->
<span class="kb-seal" aria-hidden="true">紅</span>
<span class="kb-seal kb-seal--lg" aria-hidden="true">紅</span>
PropTypeNote
childrenReactNodeone character or two letters
size'lg'48px
ThemeSwitch

Button that shows the current theme and toggles it. The choice is stored and set on the document; until one is made the page follows the OS. text shows the theme name instead of an icon, for the foot of the bar's menu sheet.

<!-- runtime adds the icon, tooltip and label -->
<button class="kb-btn kb-btn--icon kb-btn--quiet" type="button" data-kb-theme aria-label="Theme"></button>

<!-- or the theme name as text -->
<button class="kb-btn kb-btn--sm" type="button" data-kb-theme="text"></button>
PropTypeNote
textbooleantheme name instead of an icon
classNamestringadded to the button
useTheme

Returns [theme, setTheme] for a custom theme control. The theme is the data-theme attribute if one was chosen, else the OS setting. Setting it updates the document and stores the choice, so other theme controls follow. Returns light before hydration. currentTheme() reads it outside React; themeScript is the head snippet.

'use client';
import { useTheme } from '@kurobeni/design/react';

export function ThemeChoice() {
  const [theme, setTheme] = useTheme();
  return (
    <Tray label="Theme">
      <Button aria-pressed={theme === 'light'} onClick={() => setTheme('light')}>Light</Button>
      <Button aria-pressed={theme === 'dark'} onClick={() => setTheme('dark')}>Dark</Button>
    </Tray>
  );
}
<!-- without React, set the attribute and store the choice -->
<script>
  document.documentElement.dataset.theme = "dark";
  localStorage.setItem("kb-theme", "dark");
</script>
Invert

Block in the opposite theme to the page. The theme is set after mount, since the server cannot know the page theme.

Healthy
<!-- runtime sets the opposite theme and updates it when the page theme changes -->
<div class="kb-nested" data-kb-invert>…</div>
PropTypeNote
classNamestring

Blocks

Hero, tiles, collections, stats and long-form blocks

Hero

Opening band: large title with name under it, then intro and actions, all left-aligned. Renders its own Band.

<Hero
  title="kurobeni design"
  intro="Tokens, CSS and a React layer."
  actions={
    <>
      <ButtonLink href="/install" variant="primary">Install</ButtonLink>
      <ButtonLink href="/components">Components</ButtonLink>
    </>
  }
/>
<section class="kb-band" aria-labelledby="hero-title">
  <div class="kb-col kb-hero">
    <div>
      <h1 class="kb-hero__title" id="hero-title">kurobeni design</h1>
      <p class="kb-hero__name">Design system</p>
    </div>
    <div class="kb-hero__aside">
      <p class="kb-hero__intro">Tokens, CSS and a React layer.</p>
      <div class="kb-cluster">
        <a class="kb-btn kb-btn--primary" href="/install">Install</a>
      </div>
    </div>
  </div>
</section>
PropTypeNote
titleReactNode
nameReactNode
introReactNode
actionsReactNodebuttons or links
idstringheading id, default 'hero-title'
Tile

A whole-tile link, inside Tiles.

<div class="kb-tiles">
  <a class="kb-tile" href="/storage">
    <p class="kb-tile__meta"><span>Tool</span><span class="kb-num">2026</span></p>
    <h3 class="kb-tile__title">storage</h3>
    <p class="kb-tile__desc">Object storage for build artefacts.</p>
  </a>
</div>
PropTypeNote
hrefstring
kindReactNode
yearReactNode
titleReactNode
levelHeadingLeveldefault 3
smallboolean
linkLinkComponent | 'a'next/link or a plain anchor
childrenReactNodethe description
Collection, Item

A list of items of one kind. The tile layout puts them in columns, a hairline over each. Each item leads with its title. marks is the line under the title; aside sits at the end of the title row. empty is shown when there are no items.

kurobeni

142 starsupdated 2 days ago
PublicTypeScript

The design system for kurobeni.jp.

storage

8 starsupdated 5 weeks ago
PrivateGo

Object storage for build artefacts.

<!-- no panel around the list; space and a hairline between items -->
<div class="kb-collection">
  <article class="kb-item">
    <div class="kb-item__row">
      <h3 class="kb-item__title"><a href="/kurobeni">kurobeni</a></h3>
      <div class="kb-cluster kb-cluster--wide kb-muted kb-small">
        <span class="kb-num">142 stars</span>
        <span>updated 2 days ago</span>
      </div>
    </div>
    <span class="kb-cluster kb-cluster--tight">
      <span class="kb-status kb-status--ok">Open</span>
    </span>
    <p class="kb-item__text">The design system for kurobeni.jp.</p>
  </article>
</div>

<!-- kb-collection--tile: two columns, one under 768px -->
<div class="kb-collection kb-collection--tile">…</div>
PropTypeNote
Collection layout'row' | 'tile'default 'row'
Collection emptyReactNodeshown as an Empty when there are no items
titleReactNode
hrefstringmakes the title a link
levelHeadingLeveldefault 3
marksReactNode
asideReactNode
linkLinkComponent | 'a'next/link or a plain anchor
childrenReactNodeItemText is the description
Stat

A labelled figure, inside Stats. direction draws the arrow; sentiment colours it. size raises the figure one or two type steps. Four per row, two in a narrow container. href or onSelect makes the whole stat a control.

Requests3.76MUp 12.4%
Failed deploys1Up 1 since yesterday
Uptime99.97%30d
Carbon dioxide418ppm
Temperature21.4°C
Carbon dioxide418ppm+23 in 15 min
Temperature21.4°CSteady
Humidity47%Steady
VOC index102+14
Air quality42
<div class="kb-stats">
  <div class="kb-stat">
    <span class="kb-stat__label">Requests</span>
    <span class="kb-stat__value">3.76M</span>
    <span class="kb-stat__delta kb-is-up">12.4%</span>
  </div>
</div>

<!-- unit and bar; the bar is aria-hidden since the value is already read out -->
<div class="kb-stat kb-stat--lg">
  <span class="kb-stat__label">Carbon dioxide</span>
  <span class="kb-stat__value">418<span class="kb-stat__unit">ppm</span></span>
  <span class="kb-stat__bar" aria-hidden="true" style="--kb-progress-value: 62%"><span></span></span>
</div>

<!-- flush, for pages without panels: kb-stats--flush on the grid, kb-stat--flush on a single stat -->
<div class="kb-stats kb-stats--flush">
  <div class="kb-stat">
    <span class="kb-stat__label">Humidity</span>
    <span class="kb-stat__value">47<span class="kb-stat__unit">%</span></span>
  </div>
</div>
PropTypeNote
valueReactNode
unitReactNodeafter the value, in secondary ink
labelReactNode
deltaReactNode
direction'up' | 'down'
directionLabelstringscreen reader text for the arrow
sentiment'good' | 'bad' | 'neutral'default 'neutral'
size'lg' | 'xl'the figure at 3xl or 4xl, default 2xl
barnumbera percentage, drawn under the value
hrefstringmakes the whole stat a link
onSelect() => voidmakes the whole stat a button
linkLinkComponent | 'a'next/link or a plain anchor
Stats flushbooleanremoves the surface, border and inset from each stat
Cta

Call-to-action band, in the accent tone unless tone is set. On the accent tone the primary button is white with beni text. Renders its own Band.

<section class="kb-band" data-tone="accent" aria-labelledby="cta-title">
  <div class="kb-col kb-cta">
    <div>
      <h2 class="kb-head__title" id="cta-title">Say hello</h2>
      <p class="kb-cta__text">Tell me what you are making.</p>
    </div>
    <div class="kb-cluster kb-cta__actions">
      <a class="kb-btn kb-btn--primary kb-btn--lg" href="/contact">Email</a>
    </div>
  </div>
</section>
PropTypeNote
titleReactNode
textReactNode
actionsReactNode
tone'sunk' | 'deep' | 'accent'default 'accent'
idstringheading id, default 'cta-title'
Plans

Pricing plans side by side: name plate, price, ruled rows of what each includes, and the action at the foot. Rows align across plans, so give every plan the same facts in the same order. On phones a row of plan names sits above and one plan shows at a time, the pick first; the plain runtime builds that row, and without it the plans stack. pick puts one plan in beni and tag says why in words, so it is not shown by colour alone. Yes and No mark a feature with a word for screen readers; Japanese pages pass ○ and あり.

Personal

¥0 a month

Projects
3
Single sign-on
Not included

Team Recommended

¥2,400 a seat a month

Projects
Unlimited
Single sign-on
Included
<!-- rows align across plans on wide screens; plans stack on phones -->
<div class="kb-plans">
  <section class="kb-plan" aria-labelledby="plan-personal">
    <h3 class="kb-plan__name" id="plan-personal" data-tone="deep">Personal</h3>
    <p class="kb-plan__price"><span class="kb-plan__figure">¥0</span> <span class="kb-plan__per">a month</span></p>
    <dl class="kb-plan__facts">
      <div><dt>Projects</dt><dd>3</dd></div>
      <div><dt>Single sign-on</dt><dd><span aria-hidden="true">—</span><span class="kb-vh">Not included</span></dd></div>
    </dl>
    <div class="kb-plan__action"><a class="kb-btn kb-btn--lg kb-btn--block" href="/sign-up">Start free</a></div>
  </section>
  <!-- recommended plan: kb-plan--pick and an accent-tone name -->
  <section class="kb-plan kb-plan--pick" aria-labelledby="plan-team">
    <h3 class="kb-plan__name" id="plan-team" data-tone="accent">Team <span class="kb-plan__tag">Recommended</span></h3>
    …
  </section>
</div>
PropTypeNote
labelstringPlans: label for the row of plan names, default 'Plan'
idstringthe section id
nameReactNode
priceReactNodea figure or a word
perReactNodee.g. 'a month', 'a seat a month'
facts[label, value][]the same rows in every plan
actionReactNodea ButtonLink size="lg" block
pickbooleanat most one per set
tagReactNodeshort label on the plate, e.g. 'Recommended'
level1 to 6default 3
Prose

Long-form text: headings, lists, figures, tables and code get their spacing back. Sits on the band's ground; panel puts it on its own surface. tight is for a few lines: a smaller title, and a smaller inset on a panel. Spacing rules use direct-child selectors, so pass pre-rendered markup through html to set it on the block itself; a wrapper element would take the spacing instead. Article puts a Prose and a Toc side by side. Bare code and pre inside are styled too. document is for rendered files: no measure, since file viewers use the full width, and a > quote is a plain blockquote, not a pull quote. Mark an admonition with data-kb-alert on the blockquote and put its label in the first paragraph.

On margins

Space comes in named steps, from 2 to 128px.

Every gap and padding is one of them, and density scales them all. --kb-space-md

Rules

  • Controls on one row share one height.
  • Muted text clears 4.5:1 or becomes ink.

Rendered elsewhere

This block came back from a Markdown renderer as a string of HTML. It is set on the prose element itself, so .kb-prose > * + * reaches these children.

  • Headings, lists and code keep their spacing.
  • A wrapper around them would not.

Looks right to me. The gutter lands where blame needs it, and it stays put on a wide file.

One thing: the ceiling should be shared across both charts or the second one reads as bigger than it is.

<!-- long-form text: sets the measure and styles the elements inside, which need no classes -->
<article class="kb-prose">
  <h2 class="kb-prose__title">On margins</h2>
  <p class="kb-lede">The opening line, larger than the body.</p>
  <p>Ordinary paragraphs, lists and blockquotes all take their type from the block.</p>
</article>

<!-- kb-panel: on its own surface, with an inset -->
<div class="kb-prose kb-panel">…</div>

<!-- kb-prose--tight: a few lines, such as a comment or a description -->
<div class="kb-prose kb-prose--tight kb-panel">…</div>

<!-- kb-prose--doc: a rendered file; no measure, and quotes are plain blockquotes -->
<div class="kb-prose kb-prose--doc">
  <blockquote data-kb-alert="note">
    <p>Note</p>
    <p>The renderer sets the kind: note, tip, important, warning or caution.</p>
  </blockquote>
</div>
PropTypeNote
asElementTypedefault 'article'
panelbooleanown surface, with an inset
documentbooleanrendered file: no measure, plain blockquotes
tightbooleana few lines, not a document
htmlstringpre-rendered HTML, set on the block itself
classNamestring
Toc

On-this-page links beside a hairline, sticky next to the prose. Hidden below about 840px of space.

<div class="kb-article">
  <div class="kb-prose">…</div>
  <!-- runtime marks the entry for the section in view -->
  <nav class="kb-toc" aria-label="On this page">
    <span class="kb-label">On this page</span>
    <ul class="kb-nav">
      <li><a class="kb-nav__item" href="#margins">Margins</a></li>
    </ul>
  </nav>
</div>
PropTypeNote
items{ href, label, current? }[]current gets the beni edge; the plain runtime tracks scrolling
labelstringdefault 'On this page'
linkLinkComponent | 'a'next/link or a plain anchor
Sparkline

Trend line the height of a line of text, with no axes or ticks. Stroke is currentColor. Rendered on the server, with no client script. A flat run is a straight line; one value is a point.

Line
Area
Flat, and a single value
<!-- no runtime equivalent: generate the path from your data -->
<svg class="kb-spark" viewBox="0 0 120 32" width="120" height="32" role="img" aria-label="PM 2.5, 24 hours, 8 to 42">
  <path class="kb-spark__line" d="M1.5 24 L60 8 L118.5 3" vector-effect="non-scaling-stroke" />
  <circle class="kb-spark__point" cx="118.5" cy="3" r="2" />
</svg>
PropTypeNote
valuesnumber[]
labelstringdescribes the trend and range for screen readers
widthnumberdefault 120
heightnumberdefault 32
areabooleanfills under the line
pointbooleanmarks the last value, default true
Series

Eight categorical hues, --kb-series-1 to --kb-series-8, for things that differ from each other, such as branches in a commit graph or lines in a legend. The order is fixed: series n is always the same colour, and the first two are the most distinct. Each hue is mixed with the tone's ink, so it darkens on light grounds and lightens on dark ones, and each holds 3:1 against the ground and surface of every tone except accent, in both themes. On the accent tone charts use the ink. A swatch shows the colour; a text label carries the meaning.

Eight flows in series order, one colour each, so a branch can be traced back to where it started.

<!-- use the tokens directly in your own SVG -->
<svg viewBox="0 0 68 72" width="68" height="72" aria-hidden="true" fill="none" stroke-width="2">
  <path d="M6 0 v72" stroke="var(--kb-series-1)" />
  <path d="M18 0 v24 l12 12 v36" stroke="var(--kb-series-2)" />
  <path d="M30 0 v20" stroke="var(--kb-series-3)" />
</svg>
PropTypeNote
--kb-series-1indigo
--kb-series-2ochre
--kb-series-3pine
--kb-series-4plum
--kb-series-5violet
--kb-series-6moss
--kb-series-7vermilion
--kb-series-8teal
Columns

Column chart over time, one slot per period. All columns share one ceiling, so slots are comparable; a Scale's parts are shares of their own track, and a Sparkline shows trend but not magnitude. Typically two series, such as added and removed or opened and closed, paired inside each slot in the given order. Colours are the series tokens, so the first two are the most distinct. A zero draws nothing; any other value keeps a visible minimum height. Columns stop widening at 24px and the plot stays left-aligned, since very wide columns read as blocks rather than a chart.

12 weeks agoToday
  • Added
  • Removed
AprilSeptember
  • Opened
  • Closed
AugustSeptember
  • Opened
  • Closed
<!-- no runtime equivalent: set heights from your data. --kb-column is the share of the
     ceiling. --kb-columns-slot caps each column at 24px and --kb-columns-span caps the plot
     width, so a few slots do not stretch across the panel. -->
<div class="kb-columns" style="--kb-columns-slot: 49px; --kb-columns-span: 314px">
  <div class="kb-columns__plot" role="img" aria-label="Lines a week, 6 weeks">
    <div class="kb-columns__slot" title="Week of 4 August: 820 added, 210 removed">
      <span class="kb-columns__bar" style="--kb-column: 69%"></span>
      <span class="kb-columns__bar" style="--kb-column: 18%"></span>
    </div>
    <div class="kb-columns__slot" title="Week of 11 August: 140 added, 90 removed">
      <span class="kb-columns__bar" style="--kb-column: 12%"></span>
      <span class="kb-columns__bar" style="--kb-column: 8%"></span>
    </div>
  </div>
  <div class="kb-columns__ticks"><span>6 weeks ago</span><span>Today</span></div>
  <ul class="kb-key">
    <li><span class="kb-key__swatch"></span>Added</li>
    <li><span class="kb-key__swatch"></span>Removed</li>
  </ul>
</div>
PropTypeNote
seriesColumnSeries[]up to eight; colours repeat from the ninth
ColumnSeries namestringshown in the key
ColumnSeries valuesnumber[]one per slot, oldest first
labelstringwhat the columns measure
ticksReactNode[]first and last label the span
titlesstring[]one per slot, shown on hover
maxnumbershared ceiling; defaults to the largest value
heightnumberplot height in px, default 96
Key

Legend for a plot. Columns draws one from its series names; a Scale draws one from its parts when legend is set, in the same ink steps as the parts. The name carries the meaning, so colour is never the only cue.

  • Added
  • Removed
  • Unchanged
  • TypeScript 44.6%
  • HTML 23.3%
  • CSS 20.9%
<ul class="kb-key">
  <li><span class="kb-key__swatch"></span>Added</li>
  <li><span class="kb-key__swatch"></span>Removed</li>
</ul>

<!-- ink steps, matching a scale's parts in order -->
<ul class="kb-key kb-key--ink">
  <li><span class="kb-key__swatch"></span>TypeScript 44.6%</li>
  <li><span class="kb-key__swatch"></span>HTML 23.3%</li>
</ul>
PropTypeNote
itemsReactNode[]in plot order
inkbooleanink steps instead of the series palette
Calendar

A year of days, shaded in ink steps by value. Columns are weeks, rows are weekdays. days may be sparse and unordered: the grid fills gaps, buckets values against the highest day, and labels each month at the week it starts. Days are at most 14px square. On phones the year scrolls sideways; it never wraps.

LessMore
<figure class="kb-calendar">
  <div class="kb-calendar__scroll">
    <div class="kb-calendar__months" aria-hidden="true">
      <span class="kb-calendar__month">Sept</span>…
    </div>
    <div class="kb-calendar__grid" role="img" aria-label="Contributions: 1204">
      <span class="kb-calendar__day" data-kb-level="0" title="0 on 7 Sept 2026"></span>
      <span class="kb-calendar__day" data-kb-level="3" title="9 on 8 Sept 2026"></span>…
    </div>
  </div>
  <figcaption class="kb-calendar__key">Less … More</figcaption>
</figure>
PropTypeNote
days{ date: string; value: number }[]
labelstringaccessible name for the grid
weeksnumberdefault 53
legend[ReactNode, ReactNode]key under the grid, low end first; default ['Less', 'More']
localestringmonth names and dates; default 'en-GB', 'ja-JP' on a Japanese page
tip(value, day) => stringtooltip per day; default '3 on 14 Oct 2026'
Listing

A header strip and its rows in one box. The box clips, so the strip and last row keep its rounded corners. Rows are flush and separated by rules; for spaced items use Collection. The body takes rows or a table.

tsuki merged the release branch2b70b37, 5 hours ago
Commits on 11 Sept 2026
the tray makes its own groundtsuki committed 5 hours ago8f2b16d
a head can carry a marktsuki committed 7 hours ago59e8abd
<div class="kb-listing">
  <div class="kb-listing__head">
    <span class="kb-listing__line">tsuki merged the release branch</span>
    <span class="kb-listing__meta">2b70b37, 5 hours ago</span>
  </div>
  <div class="kb-listing__group">Commits on 11 Sept 2026</div>
  <div class="kb-listing__row">
    <span class="kb-listing__body">
      <span class="kb-listing__line"><a href="#">the tray makes its own ground</a></span>
      <span class="kb-listing__note">tsuki committed 5 hours ago</span>
    </span>
    <span class="kb-listing__meta">8f2b16d</span>
  </div>
</div>
PropTypeNote
Listing headReactNodethe strip above the rows
ListingHead figureReactNode
ListingHead metaReactNodetrailing, in secondary ink
ListingHead actionReactNode
ListingRow figureReactNode
ListingRow titleReactNodeprimary line; fills the width and truncates
ListingRow hrefstring
ListingRow linkLinkComponent | 'a'next/link or a plain anchor
ListingRow noteReactNodea second line, in secondary ink
ListingRow metaReactNode
ListingRow actionReactNode
ListingGroup childrenReactNodeheading for the rows below it, such as a date
ListingTable childrenReactNodetable body, for aligned columns
Log

Log stream: time and tag in a gutter, request and message on one wrapping line. For entries without a shared set of columns, where a table would leave rows half empty. Each line is its own grid, so the tag column width is set once, from the longest tag or from gutter. logKind() derives the kind from the level, then the status class.

  1. Today
  2. 502GET /api/hosts upstream timed out (110: Connection timed out) while reading response header from upstream
  3. 200GET /api/hosts
  4. 404GET /.env
  5. warncould not build optimal proxy_headers_hash, you should increase either proxy_headers_hash_max_size: 512 or proxy_headers_hash_bucket_size: 64
  6. 200GET /hosts
  1. 9 September
  2. 4f9c1ae2b0a scale holds shares of a whole jacob
  3. 7b4b8c3d19a tooltip takes the edge jacob
<!-- --kb-log-tag on the list sets the tag column width -->
<ol class="kb-log" style="--kb-log-tag: 3ch">
  <li class="kb-log__day">Today</li>
  <li class="kb-log__line kb-log__line--err">
    <time class="kb-log__at">21:47:03</time>
    <span class="kb-log__tag">502</span>
    <span class="kb-log__body"><span class="kb-log__verb">GET</span> /api/hosts <span class="kb-log__msg">upstream timed out (110: Connection timed out)</span></span>
  </li>
</ol>
PropTypeNote
Log labelstringaccessible name for the list
Log gutterstringtag column width; measured from the tags when omitted
LogLine atReactNodeformatted time
LogLine dateTimestringmachine-readable time, when at is not
LogLine tagReactNodestatus or level, in the gutter
LogLine kind'warn' | 'err'colours the tag only
LogLine methodReactNode
LogLine pathReactNode
LogLine childrenReactNodemessage, in secondary ink
LogDay childrenReactNodedate line in the stream
Source

Source file with line numbers. Numbers and code are two columns of one block, which sets the type for both so lines stay aligned (the reset shrinks a bare code element). anchor gives every line a URL. Pass lines if they are already highlighted. gutter adds an annotation column, one entry per line, outside the scrolling area, such as blame; a blank entry leaves that line unannotated. gutterWidth is the width of an entry, not of the whole track; the default 10ch fits a ten-character sha.

.kb-log { --kb-log-tag: 2.25rem; } .kb-log__line { display: grid; grid-template-columns: auto var(--kb-log-tag) minmax(0, 1fr); gap: var(--kb-space-sm); }
1d0d0c65ae7b4b8c3
.kb-log { --kb-log-tag: 2.25rem; } .kb-log__line { display: grid; grid-template-columns: auto var(--kb-log-tag) minmax(0, 1fr); gap: var(--kb-space-sm); }
<div class="kb-source">
  <!-- each number links to its own line -->
  <div class="kb-source__nums">
    <a id="L1" href="#L1">1</a>
    <a id="L2" href="#L2">2</a>
    <a id="L3" href="#L3">3</a>
  </div>
  <div class="kb-source__code" tabindex="0" role="region" aria-label="log.css"><code>.kb-log__line {
  grid-template-columns: auto var(--kb-log-tag) minmax(0, 1fr);
}</code></div>
</div>

<!-- annotated, such as blame: the gutter stays fixed while the code scrolls sideways.
     --kb-source-gutter sets its width. -->
<div class="kb-source kb-source--gutter" style="--kb-source-gutter: 10ch">
  <div class="kb-source__gutter">
    <span><a href="/commit/1d0d0c65ae">1d0d0c65ae</a></span>
    <span></span>
  </div>
  <div class="kb-source__nums"><span>1</span><span>2</span></div>
  <div class="kb-source__code" tabindex="0" role="region" aria-label="Blame of log.css"><code>.kb-log {
  --kb-log-tag: 2.25rem;</code></div>
</div>
PropTypeNote
codestringone line per newline
linesReactNode[]pre-highlighted lines
startnumberfirst line number, default 1
anchorstringmakes each number a link, at #L1
gutterReactNode[]one entry per line, before the numbers
gutterWidthstringan entry's width, default 10ch
labelstringaccessible name for the block
Diff

Unified diff, one surface per file. Rows are add, del, ctx or hunk. Each row has a tint and a sign in the gutter, so colour is not the only cue. note replaces the rows for binary or truncated files.

src/css/components/log.css
Changed+1 −1
@@ -12,7 +12,7 @@
1212.kb-log__line {
13− grid-template-columns: auto 2.25rem minmax(0, 1fr);
13+ grid-template-columns: auto var(--kb-log-tag) minmax(0, 1fr);
1414 gap: var(--kb-space-sm);

412 more lines not shown.

docs/screens/stream.png → docs/screens/log.pngRenamed

Binary file.

<div class="kb-diff">
  <section class="kb-diff__file">
    <div class="kb-diff__head">
      <span class="kb-diff__path">src/css/components/log.css</span>
      <span class="kb-cluster kb-cluster--tight">
        <span class="kb-status kb-status--ok">+1</span>
        <span class="kb-status kb-status--err">−1</span>
      </span>
    </div>
    <div class="kb-diff__body" tabindex="0" role="region" aria-label="src/css/components/log.css">
      <table>
        <tbody>
          <tr class="kb-diff__row--hunk">
            <td class="kb-diff__text" colspan="4">@@ -12,7 +12,7 @@</td>
          </tr>
          <tr>
            <td class="kb-diff__num">12</td>
            <td class="kb-diff__num">12</td>
            <td class="kb-diff__sign"></td>
            <td class="kb-diff__text">.kb-log__line {</td>
          </tr>
          <!-- the sign is in the markup so the tint is not the only cue -->
          <tr class="kb-diff__row--del">
            <td class="kb-diff__num">13</td>
            <td class="kb-diff__num"></td>
            <td class="kb-diff__sign">−</td>
            <td class="kb-diff__text">  grid-template-columns: auto 2.25rem minmax(0, 1fr);</td>
          </tr>
          <tr class="kb-diff__row--add">
            <td class="kb-diff__num"></td>
            <td class="kb-diff__num">13</td>
            <td class="kb-diff__sign">+</td>
            <td class="kb-diff__text">  grid-template-columns: auto var(--kb-log-tag) minmax(0, 1fr);</td>
          </tr>
        </tbody>
      </table>
    </div>
    <p class="kb-diff__note">412 more lines not shown.</p>
  </section>
</div>
PropTypeNote
DiffFile pathReactNode
DiffFile fromReactNodeold path, for a moved file
DiffFile actionsReactNodeline counts and state
DiffFile noteReactNodeshown in place of omitted rows
DiffRow kind'add' | 'del' | 'ctx' | 'hunk'default 'ctx'
DiffRow beforeReactNodeold line number
DiffRow afterReactNodenew line number
Scale

A value on a named range. With runs instead of value it shows a history, one mark per period, oldest first: a normal period is a short tick on the baseline, warn is half height and err full height, so problems show by height before hue. none is a period with no reading. A run's label is its tooltip, short and plain, such as Down on 3 October; hovering one dims the rest. The strip's own label carries the summary, so nothing depends on the tooltips. With parts it shows shares of a whole, and legend names them under the track.

0 Good100 Moderate200 Poor300+
0620 GB used1 TB
  • TypeScript 44.6%
  • HTML 23.3%
  • CSS 20.9%
  • JavaScript 11.2%
90 days ago99.98% uptimeToday
<div class="kb-scale" style="--kb-scale-at: 14%">
  <div class="kb-scale__track kb-scale__track--zones" role="img" aria-label="Air quality: 42 of 0 to 300">
    <span class="kb-scale__marker"></span>
  </div>
  <div class="kb-scale__ticks">
    <span>0 Good</span><span>100 Moderate</span><span>200 Poor</span><span>300+</span>
  </div>
</div>

<!-- history, one mark per period, oldest first: normal periods are short ticks, problems
     are taller the worse they are. data-kb-tip says what happened. -->
<div class="kb-scale">
  <div class="kb-scale__runs" role="img" aria-label="Builds, last 90 days: 99.97% uptime, queued on 12 September">
    <span class="kb-scale__run"></span>
    <span class="kb-scale__run kb-scale__run--warn" data-kb-tip="Builds queued on 12 September"></span>
    <span class="kb-scale__run kb-scale__run--err" data-kb-tip="Down on 3 October"></span>
    <span class="kb-scale__run kb-scale__run--none" data-kb-tip="No reading on 9 October"></span>
  </div>
  <div class="kb-scale__ticks" aria-hidden="true"><span>90 days ago</span><span>Today</span></div>
</div>

<!-- shares of a whole: one part per share, width set per part -->
<div class="kb-scale kb-scale--parts">
  <div class="kb-scale__track" role="img" aria-label="Languages">
    <span class="kb-scale__part" style="--kb-scale-part: 44.6%"></span>
    <span class="kb-scale__part" style="--kb-scale-part: 23.3%"></span>
  </div>
  <ul class="kb-key kb-key--ink">
    <li><span class="kb-key__swatch"></span>TypeScript 44.6%</li>
    <li><span class="kb-key__swatch"></span>HTML 23.3%</li>
  </ul>
</div>
PropTypeNote
valuenumberclamped to the range
minnumberdefault 0
maxnumberdefault 100
zonesbooleancolours the range itself; the marker stays neutral
fillbooleanfills from the start instead of marking a point
runsScaleRun[]replaces the track with a history
partsScalePart[]shares of a whole, in descending ink steps
legendbooleanKey of the parts under the track
ticksReactNode[]first and last label the range
labelstringwhat the range measures
Facts

Name and value pairs in columns, one column per subject. Use instead of a table, which would be mostly empty across a full band.

Dew point
9.4 °C
Heat index
20.8 °C
Absolute humidity
8.1 g/m³
Dose rate, peak 24 h
0.19 µSv/h
Total dose, 24 h
2.84 µSv
Annual estimate
0.96 mSv
Sunrise
06:52
Sunset
19:04
Daylight left
7h 42m
<dl class="kb-facts">
  <div class="kb-facts__group">
    <dt>Dew point</dt><dd>9.4 &deg;C</dd>
    <dt>Heat index</dt><dd>20.8 &deg;C</dd>
  </div>
  <div class="kb-facts__group">
    <dt>Sunrise</dt><dd>06:52</dd>
    <dt>Sunset</dt><dd>19:04</dd>
  </div>
</dl>
PropTypeNote
childrenFactGroup elements
Callout

Boxed message, neutral or tinted by kind. live makes it a status region, announced when it appears in response to an action; a callout present on page load is not announced.

NeutralMaintenance on Sunday 02:00 JST.
NoticeA new version is available.
SavedChanges are live.
Certificate expires in 6 daysRenewal is automatic.
Deploy failedHealth check timed out after 30 s.
<p class="kb-callout kb-callout--warn">
  <strong>Certificate expires in 6 days</strong>
  Renew before Friday.
</p>
PropTypeNote
kind'neutral' | 'info' | 'notice' | 'ok' | 'warn' | 'err'default 'neutral'
titleReactNode
livebooleanrole=status: announced when it appears
alertbooleanrole=alert: an error that blocks a task, announced immediately
idstringtarget for a field's aria-describedby
Disclosure, Accordion

Native details element: the browser handles open state, keyboard and find-in-page, with no JS. A shared name keeps one open at a time. Accordion groups and spaces them.

Where does the CSS come from?
One stylesheet at @kurobeni/design/css, built from the tokens. Import it once in the root layout.
Can I use it without React?
Yes. The classes are the system; @kurobeni/design/js wires the parts that need behaviour.
How do I change a colour?
Set --kb-accent, or give a tone its three colours. Everything else is mixed from them, and pnpm test measures the result.
<div class="kb-accordion">
  <!-- shared name: opening one closes the others -->
  <details class="kb-disclosure" name="faq" open>
    <summary class="kb-disclosure__summary">Where does the CSS come from?</summary>
    <div class="kb-disclosure__body">One stylesheet, built from the tokens.</div>
  </details>
  <details class="kb-disclosure" name="faq">
    <summary class="kb-disclosure__summary">Can I use it without React?</summary>
    <div class="kb-disclosure__body">Yes. The classes are the system.</div>
  </details>
</div>
PropTypeNote
summaryReactNodethe always-visible line
namestringshared name closes the others
openbooleanopen initially
childrenReactNodecontent
Empty

Empty state for a list or view.

No deployments yet. Push to main to create one.

<div class="kb-empty">
  <p>No deployments yet.</p>
</div>
Sheet

Specimen rows divided by hairlines, one per component family, each labelled. SheetRow takes label and id.

<div class="kb-sheet">
  <div class="kb-sheet__row" id="buttons">
    <span class="kb-label">Buttons</span>
    …
  </div>
</div>

Controls

Buttons, text, identity and layout

Button

Primary uses the accent: one per view, and at most one in the bar. Secondary is the default; quiet is for toolbars and rows; danger is for actions that cannot be undone. Passes all button attributes through. ButtonLink is the same on an anchor. LinkButton looks like a link; href renders an anchor.

Link as button
<button class="kb-btn kb-btn--primary" type="button">Save</button>
<button class="kb-btn" type="button">Cancel</button>
<button class="kb-btn kb-btn--quiet" type="button">Archive</button>
<button class="kb-btn kb-btn--danger" type="button">Delete</button>
<a class="kb-btn kb-btn--sm" href="/notes">Link as button</a>
<button class="kb-btn-link" type="button">A link in running text</button>
PropTypeNote
variant'primary' | 'secondary' | 'quiet' | 'danger'default 'secondary'
size'sm' | 'md' | 'lg'32, 36, 44px; 40, 44, 48px on touch
blockbooleanfull width of its container
iconbooleansquare, one icon; needs aria-label
tipstringtooltip on hover and focus
pushbooleanmargin-left: auto
Tray

Segmented control for picking one option, such as a range or a view. The selected item is shown inverted, like the current page elsewhere. Buttons inside use aria-pressed, links aria-current. The tray has its own recessed background, so it works on the page or on a panel.

<!-- selection: aria-pressed on buttons, aria-current on links, aria-selected on tabs -->
<div class="kb-tray" role="group" aria-label="Range">
  <button class="kb-btn" type="button" aria-pressed="false">24h</button>
  <button class="kb-btn" type="button" aria-pressed="true">7d</button>
</div>
<div class="kb-tray" role="group" aria-label="Section">
  <a class="kb-btn" href="#overview" aria-current="page">Overview</a>
  <a class="kb-btn" href="#activity">Activity</a>
</div>
PropTypeNote
labelstring
role'group' | 'tablist'default 'group'
Status

A word with a small coloured mark. The word carries the meaning, not the colour.

HealthyDegradedFailedBuilding
<span class="kb-status kb-status--ok">Healthy</span>
<span class="kb-status kb-status--warn">Degraded</span>
<span class="kb-status kb-status--err">Failed</span>
PropTypeNote
kind'neutral' | 'ok' | 'warn' | 'err'default 'neutral'
Text, VisuallyHidden

Text roles without hand-written classes. size is a type scale step, xs to 4xl; 2xl and up are set as titles. muted is secondary ink, small the interface size, label a short sentence-case name for what follows, and num sets tabular figures. VisuallyHidden is for screen readers only.

2xl, the section title size

Secondary ink, for supporting text

A label for what follows

1,482 runs

<p class="kb-text-2xl">A section title size</p>
<p class="kb-muted kb-small">Secondary, small.</p>
<p class="kb-label">Region</p>

<!-- screen readers only -->
<span class="kb-vh">Up</span>
PropTypeNote
asElementTypedefault 'p'
size'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'
muted, small, label, numboolean
Figure, FigureSwatch

Image, video or chart with a caption. Pass the media as children, so next/image, img and svg all work. FigureSwatch is a placeholder.

Fig. 1. Deploy duration, last thirty days.
<figure class="kb-figure">
  <img src="/chart.png" alt="Requests over seven days" />
  <figcaption>Requests, 7 days.</figcaption>
</figure>
PropTypeNote
captionReactNode
labelstringFigureSwatch: accessible name for the placeholder
Kbd, Code, Pre

Keys, inline code and code blocks. Syntax colours are the seven code roles, mapped to Shiki's css-variables theme, highlight.js and Chroma, so output from any of the three looks the same.

CtrlK--kb-space-xs: 8px
.card {
  border-radius: var(--kb-radius-lg);
}
<span class="kb-cluster kb-cluster--tight">
  <kbd class="kb-kbd">Ctrl</kbd><kbd class="kb-kbd">K</kbd>
</span>

<code class="kb-code">--unit: 8px</code>

<!-- focusable so keyboard users can scroll long lines -->
<pre class="kb-pre" tabindex="0"><code>pnpm add @kurobeni/design</code></pre>
Avatar

Initials in a circle, optionally covered by an image. src is the image; the initials stay behind it as the fallback if it fails to load. With name it is announced as an image; without, it is hidden, since the surrounding control carries the name. AvatarStack overlaps several.

JBKBKBKBJBTS+3
<span class="kb-avatar">JB</span>
<span class="kb-avatar kb-avatar--lg" role="img" aria-label="Jacob">JB</span>

<!-- the image covers the initials; empty alt, so the initials show if it fails to load -->
<span class="kb-avatar" role="img" aria-label="Jacob">
  JB<img class="kb-avatar__img" src="/avatars/jacob" alt="" loading="lazy" />
</span>

<span class="kb-avatar-stack">
  <span class="kb-avatar">JB</span><span class="kb-avatar">TS</span><span class="kb-avatar">+3</span>
</span>
PropTypeNote
initialsReactNode
srcstringimage URL; initials are the fallback
namestring
size'md' | 'lg'
Crumbs

Breadcrumb trail. The last entry has no href and marks the current page.

  1. auth
  2. Deployments
  3. storage
<!-- the last crumb is the current page, not a link -->
<ol class="kb-crumbs">
  <li><a href="/auth">auth</a></li>
  <li><span aria-current="page">storage</span></li>
</ol>
PropTypeNote
trail{ href?, label }[]
linkLinkComponent | 'a'next/link or a plain anchor
Pages

Pagination. A null entry renders a gap.

<nav class="kb-pages" aria-label="Pagination">
  <a href="?p=1" aria-label="Previous page">‹</a>
  <a href="?p=1" aria-current="page">1</a>
  <a href="?p=2">2</a>
  <span>…</span>
  <a href="?p=12">12</a>
  <a href="?p=2" aria-label="Next page">›</a>
</nav>
PropTypeNote
pages({ href, label, current? } | null)[]
prevstring
nextstring
prevLabelstringdefault 'Previous page'
nextLabelstringdefault 'Next page'
labelstringdefault 'Pagination'
pushboolean
linkLinkComponent | 'a'next/link or a plain anchor
Progress

Progress bar. Skeleton is a shimmering placeholder, one per line of text.

Build, 25%
Upload, 70%
Loading
<progress class="kb-progress" value="25" max="100" aria-label="Build"></progress>
<div class="kb-skeleton" style="width: 80%"></div>
PropTypeNote
valuenumber
maxnumberdefault 100
labelstring
Spinner

Loading indicator. The label is for screen readers only. Uses the current ink; motion none does not stop it.

Loadingbeside anything
<span class="kb-spin" aria-hidden="true"></span>
<span class="kb-vh" role="status">Loading</span>
PropTypeNote
labelstringscreen reader text
InputIcon

Icon inside a field, at either end. Wraps the control; the icon ignores pointer events.

<div class="kb-input-icon">
  <input class="kb-input" id="q" placeholder="Services" />
  <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">…</svg>
</div>
PropTypeNote
iconReactNode
side'start' | 'end'default 'start'
childrenReactNodethe control
Icons

Icons used by the components, generated from Lucide. Stroke is currentColor.

IconSun
IconMoon
IconClose
IconArrowUp
IconArrowDown
IconSearch
IconChevronDown
IconCheck
IconMore
IconMenu
IconFile
IconFolder
IconBranch
IconCommit
IconIssue
IconPullRequest
IconTag
<!-- the runtime has the same set; any svg with a currentColor stroke works -->
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
  <path d="m6 9 6 6 6-6" />
</svg>
PropTypeNote
sizenumberdefault 18
...restSVGPropspassed to the svg
Cluster, Stack, Row, Split, Divider

Cluster is a wrapping row, Stack a column, Row 2 or 3 columns of fields, Split two halves, Divider a hairline. All take as and pass other attributes to that element, so a Cluster can be a form with action and method. Props are typed from as, so only that element's attributes type-check: href on an a, src on an img.

<div class="kb-cluster">…</div>
<div class="kb-stack kb-stack--tight">…</div>
<div class="kb-row kb-row--3">…</div>
<div class="kb-split">…</div>
<hr class="kb-divider" />

<!-- the class works on any element -->
<form class="kb-cluster" action="/search" method="get">…</form>

<!-- kb-push takes the free space, moving it and what follows to the end -->
<div class="kb-cluster"><span>Left</span><span class="kb-push">Right</span></div>
PropTypeNote
asElementTypedefault 'div', 'hr' for Divider
classNamestring
...restComponentPropsWithoutRef<'as'>set on the element
Cluster gap'tight' | 'wide'
Cluster pushboolean
Stack tightboolean
Row cols2 | 3default 2

Forms

Field wiring, inputs, choices and the dropdown

Field

Label, hint and error for a control, which it passes id, aria-describedby and aria-invalid. Renders an Input by default; pass children for a Select or Textarea. aside puts a link at the end of the label row. Form and Fieldset wrap fields; FormActions is the button row. A Fieldset is an unboxed form section: when there is room its legend and description sit beside the fields, and a hairline separates it from the next.

Identity
Shown in the index.
Enter a full address.
<div class="kb-field">
  <label class="kb-field__label" for="email">Email</label>
  <input class="kb-input" id="email" name="email" type="email" aria-describedby="email-hint" />
  <span class="kb-field__hint" id="email-hint">Only used to reply.</span>
</div>

<!-- invalid: state class on the field, error linked by id -->
<div class="kb-field kb-is-invalid">
  <label class="kb-field__label" for="host">Host</label>
  <input class="kb-input" id="host" aria-describedby="host-err" aria-invalid="true" />
  <span class="kb-field__error" id="host-err">Not a hostname.</span>
</div>

<!-- aside: a link at the end of the label row -->
<div class="kb-field">
  <div class="kb-field__top">
    <label class="kb-field__label" for="pass">Password</label>
    <a href="/reset">Forgotten your password?</a>
  </div>
  <input class="kb-input" id="pass" type="password" />
</div>
PropTypeNote
idstringgenerated when omitted
labelReactNode
hintReactNode
errorReactNodemarks the field invalid
requiredboolean
widebooleanspans the row
asideReactNodelink at the end of the label row
children(wiring) => ReactNodeomit for an Input
...inputInputHTMLAttributespassed to the default Input
Fieldset legendReactNode
Fieldset descriptionReactNodeunder the legend
PasswordInput

Password field with a show/hide button. The button reads Show or Hide, with accessible names Show password and Hide password (表示 and 非表示 in Japanese). The password is hidden again when the form is submitted. Takes the wiring a Field passes its children.

<Field label="Password">
  {(w) => <PasswordInput {...w} name="password" autoComplete="current-password" required />}
</Field>
<!-- hidden until the runtime labels and enables the button -->
<div class="kb-reveal">
  <input class="kb-input" id="pass" name="password" type="password" autocomplete="current-password" />
  <button class="kb-btn kb-btn--quiet" type="button" data-kb-reveal="pass" hidden></button>
</div>
PropTypeNote
idstringfrom the Field
...inputInputHTMLAttributesname, autoComplete, required
Input, Textarea

Styled native inputs. All attributes pass through.

<input class="kb-input" id="name" name="name" />
<input class="kb-input kb-input--sm" id="q" type="search" />
<textarea class="kb-textarea" id="note" rows="4"></textarea>
PropTypeNote
size'sm' | 'md'Input only, 32 or 36px; taller on touch
Group

Input joined to fixed text or a button before or after it.

.kurobeni.jp
https://
<!-- the outer corners are rounded on the first and last visible piece -->
<div class="kb-group">
  <span class="kb-group__addon">https://</span>
  <input class="kb-input" id="slug" value="storage" />
  <span class="kb-group__addon">.example.com</span>
</div>
PropTypeNote
beforeReactNode
afterReactNode
size'sm' | 'md'
Select

Native select for forms, shown as a custom button and listbox. Supports arrow keys, Home, End, typeahead and Escape. Opens upwards when there is no room below.

Selected: Osaka
<!-- runtime replaces it with a button and listbox, and keeps the select in sync -->
<select class="kb-select" id="region" name="region">
  <option value="lhr">London</option>
  <option value="fra">Frankfurt</option>
</select>
PropTypeNote
options{ value, label, disabled? }[]
valuestringcontrolled
defaultValuestring
onChange(value: string) => void
size'sm' | 'md'
namestring
disabledboolean
Combobox

Select with a text field, for lists too long to scroll. Typing filters; arrows and Enter choose; Escape closes and restores the value. Opens on the side with more room. With name, the value is submitted through a hidden input.

Type to narrow the list
<!-- React only. Without React, use a select; the runtime gives it a listbox with typeahead. -->
<select class="kb-select" id="region">
  <option value="lhr">London</option>
</select>
PropTypeNote
options{ value, label, disabled? }[]
valuestringcontrolled
onChange(value: string) => void
placeholderstring
emptyLabelstringdefault 'No matches'
size'sm' | 'md'
namestringsubmits on a hidden input
disabledboolean
Choice, Switch

Checkbox, radio and switch, styled in CSS. Checked controls fill with the accent. A switch shows its state by knob position and a check on the knob; the switch role makes screen readers announce on or off.

Visibility

Who can find the project.

Features

Changes take effect when you save.


<label class="kb-choice">
  <input type="checkbox" name="remember" />
  <span class="kb-choice__text">Keep me signed in</span>
</label>

<label class="kb-choice kb-choice--switch">
  <input class="kb-switch" type="checkbox" role="switch" name="alerts" />
  <span class="kb-choice__text">Alerts</span>
</label>
PropTypeNote
type'checkbox' | 'radio'Choice only
labelReactNode
descriptionReactNodesmall type under the label
hideLabelbooleanvisually hidden label, e.g. for select-all
refRef<HTMLInputElement>for indeterminate
Toolbar, HiddenLabel

Row of small controls above a table. HiddenLabel is a visually hidden label for a control whose purpose the layout already shows.

<div class="kb-toolbar">
  <label class="kb-vh" for="q">Search</label>
  <input class="kb-input kb-input--sm" id="q" type="search" placeholder="Search" />
  <button class="kb-btn kb-btn--sm kb-push" type="button">Filter</button>
</div>

Data

Tables and tabs

Table

TableWrap scrolls sideways within its block and shows a hint while it can scroll. Th and Td take a priority: columns hide low first, then mid, then narrow. RowTitle shows the status under the name once the status column is hidden.

ProjectEnvironmentStatusRequestsp95UpdatedActions
apiHealthyproductionHealthy1,204,553182 ms2 min ago
webDegradedstagingDegraded48,210640 ms14 min ago
storageFailedproductionFailed0—3 h ago
Showing 1–3 of 24
<!-- data-kb-scrollable marks the edge that hides columns and shows the caption. -->
<div class="kb-table-wrap" data-kb-scrollable>
  <div class="kb-table-scroll" aria-label="Deployments">
    <table class="kb-table">
      <thead>
        <tr>
          <th>Project</th>
          <!-- priority hides the column as the block narrows: low, then mid, then narrow -->
          <th data-kb-priority="low">Environment</th>
          <th class="kb-is-num">Requests</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>
            <span class="kb-table__primary">api</span>
            <span class="kb-table__sub kb-status kb-status--ok">Healthy</span>
          </td>
          <td data-kb-priority="low">production</td>
          <td class="kb-is-num">1,204,553</td>
        </tr>
      </tbody>
    </table>
  </div>
</div>
<div class="kb-table-foot">…</div>
PropTypeNote
TableWrap labelstringaccessible name for the scroll region
TableWrap hintReactNode
TableWrap prosebooleaninside Prose
Th, Td priority'low' | 'mid' | 'narrow'
Th, Td numbooleanright-aligned, tabular digits
Td mutedboolean
RowTitle status{ kind?, label }
Tabs

ARIA tabs. Arrow keys, Home and End move and select. The selected tab is underlined in beni. A tab list wider than its container scrolls sideways. fill spreads the tabs evenly across the container.

All eight deployments.

<!-- runtime handles arrow keys, roving tabindex and panel visibility -->
<div class="kb-tabs">
  <div class="kb-tabs__list" role="tablist" aria-label="Environment">
    <button class="kb-tabs__tab" type="button" role="tab" id="t-prod" aria-controls="p-prod" aria-selected="true">
      Production <span class="kb-count">9</span>
    </button>
    <button class="kb-tabs__tab" type="button" role="tab" id="t-stage" aria-controls="p-stage" aria-selected="false">
      Staging <span class="kb-count">3</span>
    </button>
  </div>
  <div class="kb-tabs__panel" role="tabpanel" id="p-prod" aria-labelledby="t-prod">…</div>
  <div class="kb-tabs__panel" role="tabpanel" id="p-stage" aria-labelledby="t-stage" hidden>…</div>
</div>
PropTypeNote
labelstring
items{ id?, label, count?, children }[]
initialnumberdefault 0
fillbooleantabs fill the container width
onChange(index: number) => void

Frame

App page layout

App, AppSide, AppMain, AppTop

App screen with a sidebar and a main pane. App takes sections and renders them twice: as the sidebar, and under 1024px as a Menu on AppTop's title with the current section ticked. With density auto, spacing shrinks by a quarter under 768px; controls keep their size.

<App
  sections={[
    { label: 'Platform', items: [{ href: '/', label: 'Overview', current: true }] },
    { label: 'Account', items: [{ href: '/settings', label: 'Settings' }] },
  ]}
>
  <AppSide />
  <AppMain>
    <AppTop title="Deployments">
      <Status kind="ok">Healthy</Status>
    </AppTop>
  </AppMain>
</App>
<!-- data-density auto tightens spacing under 768px. Below 1024px the sidebar is hidden
     and the second title, with the section menu, is shown instead. -->
<main class="kb-band kb-band--fill" id="main" tabindex="-1" data-density="auto">
  <div class="kb-col">
    <div class="kb-app">
      <aside class="kb-app__side" aria-label="Sections">
        <ul class="kb-nav" aria-label="Sections">
          <li><a class="kb-nav__item" href="/overview" aria-current="page">Overview</a></li>
        </ul>
      </aside>
      <!-- add kb-panel to either to put it on a surface -->
      <div class="kb-app__main">
        <div class="kb-app__top">
          <h2 class="kb-app__title">Overview</h2>
          <h2 class="kb-app__title kb-app__title--pick">
            <button class="kb-app__pick" type="button" data-kb-menu="sections">Overview</button>
            <div class="kb-menu" id="sections" aria-label="Sections">…</div>
          </h2>
          <span class="kb-push"></span>
        </div>
        …
      </div>
    </div>
  </div>
</main>
PropTypeNote
sectionsNavSection[]App, { label?, items }
labelstringApp: accessible name for the region and the menu
densityDensity | 'default'compact, comfortable or auto; App's default is auto
titleReactNodeon AppTop
level1 to 6heading level, default 1
panelbooleanAppSide and AppMain: put on a surface
linkLinkComponent | 'a'next/link or a plain anchor
Section

Titled block inside a pane, without a full-width background or the 1200px column.

<section class="kb-section">
  <div class="kb-section__head">
    <h2 class="kb-section__title">Domains</h2>
    <p class="kb-section__aside">4 in use</p>
  </div>
  …
</section>
PropTypeNote
titleReactNode
asideReactNodeat the end of the head
level1 to 6default 2
asElementTypedefault 'section'
PanelHead, PanelBody, PanelFoot

A panel with a header and an optional footer.

frankfurt-1

14 variables

Values are written at deploy time.

<!-- hairlines separate the head and foot from the body; the foot is recessed -->
<div class="kb-panel">
  <div class="kb-panel__head">
    <h3>frankfurt-1</h3>
  </div>
  <div class="kb-panel__body">…</div>
  <div class="kb-panel__foot">…</div>
</div>
PropTypeNote
titleReactNodeon PanelHead
level1 to 6default 2
Auth

Standalone form page outside the app, such as sign in, sign up or password reset. brand and pitch add a deep-tone side panel: brand at the top, what the service does in the middle, small print at the foot, and a beni rule between it and the form. On phones the panel shrinks to a bar above the form with the brand and language link. Without either, the form is centred. Auth renders the page's main and h1. Or puts a word between two rules, to separate sign-in methods. A failed sign-in shows an error Callout (alert) above the form, with both fields marked invalid and described by it, without saying which was wrong; PasswordInput lets people check what they typed. In dark theme the panel is one step lighter, to set it apart from the form.

<Auth
  brand={{ href: '/', name: 'Example', seal: 'E' }}
  pitch={{ title: 'Deploy web apps from a git push', points: [...] }}
  lang={<LangLink href="/ja/sign-in" lang="ja">日本語</LangLink>}
  title="Sign in"
  intro="Welcome back to Example."
  foot={<>No account yet? <a href="/pricing">Start free</a></>}
>
  <ButtonLink href="/sso" size="lg" block>Continue with GitHub</ButtonLink>
  <Or>or with email</Or>
  <Form>…</Form>
</Auth>
<!-- standalone page with no bar. Without the aside, the form is centred. -->
<div class="kb-auth">
  <aside class="kb-auth__mark" data-tone="deep">
    <a class="kb-auth__brand" href="/"><span class="kb-seal" aria-hidden="true">紅</span>kurobeni design</a>
    <!-- language link position on phones -->
    <a class="kb-lang kb-auth__lang" href="/ja/sign-in" hreflang="ja" lang="ja">日本語</a>
    <div class="kb-auth__pitch">
      <p class="kb-auth__title">Deploy web apps from a git push</p>
      <ul class="kb-list kb-list--check">
        <li>A preview address for every branch</li>
      </ul>
    </div>
    <p class="kb-auth__small">© 2026 Example Ltd</p>
  </aside>
  <div class="kb-auth__form">
    <div class="kb-auth__top"><a class="kb-lang" href="/ja/sign-in" hreflang="ja" lang="ja">日本語</a></div>
    <main id="main" tabindex="-1">
      <div class="kb-auth__head">
        <h1 class="kb-text-2xl">Sign in</h1>
        <p>Welcome back to Example.</p>
      </div>
      <!-- on failed sign-in: shown, with both fields aria-invalid and described by it -->
      <div class="kb-callout kb-callout--err" id="sign-in-error" role="alert">
        <strong>Email or password not recognised</strong>
        <p>Check them and try again, or <a href="/reset">reset your password</a>.</p>
      </div>
      <a class="kb-btn kb-btn--lg kb-btn--block" href="/sso">Continue with GitHub</a>
      <p class="kb-or">or with email</p>
      <form class="kb-form" action="/sign-in" method="post">…</form>
      <p class="kb-auth__foot">No account yet? <a href="/pricing">Start free</a></p>
    </main>
    <footer class="kb-auth__end"><a href="/terms">Terms</a> <a href="/privacy">Privacy</a></footer>
  </div>
</div>
PropTypeNote
titleReactNodethe page's h1
introReactNodethe line under it
brand{ href, name, seal? }top of the side panel
pitch{ title, points? }what the service does
smallReactNodefoot of the side panel
langReactNodea LangLink
footReactNodethe line under the form
endReactNodeterms, privacy, status
toneToneside panel tone, default 'deep'
linkLinkComponent | 'a'next/link or a plain anchor

Feedback

Dialogs and toasts

Dialog

Native dialog. Open with ref.current.showModal(). The close button and any button with data-kb-close close it. For destructive actions, useTypedConfirm(word) returns inputProps and confirmed.

Edit deployment

Delete storage?

This removes the project, its deployments and its subdomain.

<button class="kb-btn" type="button" data-kb-dialog="edit">Edit</button>

<dialog class="kb-dialog" id="edit" aria-labelledby="edit-title">
  <form method="dialog">
    <div class="kb-dialog__head">
      <h2 id="edit-title">Edit service</h2>
      <!-- left empty: the runtime adds the close icon -->
      <button class="kb-btn kb-btn--icon" type="button" data-kb-close aria-label="Close"></button>
    </div>
    <div class="kb-dialog__body">…</div>
    <div class="kb-dialog__foot">
      <button class="kb-btn" type="button" data-kb-close>Cancel</button>
      <button class="kb-btn kb-btn--primary" type="button" data-kb-ok>Save</button>
    </div>
  </form>
</dialog>
PropTypeNote
idstring
titleReactNode
actionsReactNodefooter buttons
refRef<HTMLDialogElement>
closeLabelstringdefault 'Close'
Drawer

Dialog that slides in from an edge, for filters or a detail pane. Opens and closes like Dialog. bottom is a sheet: full width on phones, a centred column on larger screens, sized to its content. Edges touching the screen are padded by the safe-area inset.

Filters

Filters, or a detail pane.

Share

Anyone with the link sees this run.

<button class="kb-btn" type="button" data-kb-dialog="filters">Filters</button>

<!-- right edge by default; kb-drawer--start for the left, kb-drawer--bottom for a bottom sheet -->
<dialog class="kb-dialog kb-drawer kb-drawer--start" id="filters" aria-labelledby="filters-title">
  <form method="dialog">
    <div class="kb-dialog__head">
      <h2 id="filters-title">Filters</h2>
      <button class="kb-btn kb-btn--icon" type="button" data-kb-close aria-label="Close"></button>
    </div>
    <div class="kb-dialog__body kb-drawer__body">…</div>
  </form>
</dialog>
PropTypeNote
titleReactNode
actionsReactNodefooter buttons
side'start' | 'end' | 'bottom'default 'end'
refRef<HTMLDialogElement>
ToastProvider, useToast

ToastProvider renders toasts in a viewport corner. useToast() returns toast(message, kind?). At most three at once, five seconds each, paused on hover.

<!-- the live region, once per page -->
<div class="kb-toasts" aria-live="polite"></div>

<!-- any element with data-kb-toast shows one; data-kb-kind is ok or err -->
<button class="kb-btn" type="button" data-kb-toast="Deploy queued" data-kb-kind="ok">
  New deploy
</button>
PropTypeNote
kind'ok' | 'err'optional

Whole page

All of the above, combined on one page.