klodsv3.6.0
Vanilla HTML →

klods

Tiny, opinionated, fully themeable HTML/CSS/JS pieces that snap together like lego.

klods (Danish for block) gives you two ways to build:

  • Plain HTML — drop in klods.css and use the BEM classes (klods-page, klods-stack, …). No JS required.
  • Typed buildersimport { page, header, … } from "klods-js" for a lego-like API that produces both DOM and HTML strings.

Install — JavaScript / TypeScript

npm install klods-js klods-css

Install — Ruby / Rails

gem "klods-ruby"

The Railtie makes all builders available in every ERB view — no imports needed. See klods-ruby on GitHub for full documentation.

Install — Vanilla HTML

<link rel="stylesheet" href="https://unpkg.com/klods-css/dist/klods.min.css">

Drop in BEM classes — no build step, no JavaScript required.

Layout

The four corners of every page: header, sidebar, content, footer.

Page with a sidebar

Add `sidebar: true` to grow a sidebar column.

Header
Main content
Footer
TypeScript
page({ sidebar: true }, [
  header("Header"),
  sidebar("Sidebar"),
  content("Main content"),
  footer("Footer"),
]);
Ruby
page({ sidebar: true }, [
  header("Header"),
  sidebar("Sidebar"),
  content("Main content"),
  footer("Footer"),
])
HTML
<div class="klods-page klods-page--with-sidebar">
  <header class="klods-header">
    Header
  </header>
  <aside class="klods-sidebar">
    Sidebar
  </aside>
  <main class="klods-content">
    Main content
  </main>
  <footer class="klods-footer">
    Footer
  </footer>
</div>

Narrow content

`content({ narrow: true })` caps the main column at --klods-content-max and centres it.

Header
Narrow column — capped and centred.
Footer
TypeScript
page([
  header("Header"),
  content(
    { narrow: true },
    box("Narrow column — capped and centred.")
  ),
  footer("Footer"),
]);
Ruby
page([
  header("Header"),
  content(
    { narrow: true },
    box("Narrow column — capped and centred.")
  ),
  footer("Footer"),
])
HTML
<div class="klods-page">
  <header class="klods-header">
    Header
  </header>
  <main class="klods-content klods-content--narrow">
    <div class="klods-box">
      Narrow column — capped and centred.
    </div>
  </main>
  <footer class="klods-footer">
    Footer
  </footer>
</div>

Section

Sibling sections are automatically separated by a top border and vertical spacing. Each section also has scroll-margin-top so that anchor links clear any sticky header.

First section
Second section
Third section
TypeScript
stack({ gap: 0 }, [
  section(box("First section")),
  section(box("Second section")),
  section(box("Third section")),
]);
Ruby
stack({ gap: 0 }, [
  section(box("First section")),
  section(box("Second section")),
  section(box("Third section")),
])
HTML
<div class="klods-stack klods-stack--gap-0">
  <section class="klods-section">
    <div class="klods-box">
      First section
    </div>
  </section>
  <section class="klods-section">
    <div class="klods-box">
      Second section
    </div>
  </section>
  <section class="klods-section">
    <div class="klods-box">
      Third section
    </div>
  </section>
</div>

Utilities

The “I always forget how to do this in CSS” classes.

Stack — vertical with gap

one
two
three
TypeScript
stack({ gap: 3 }, [box("one"), box("two"), box("three")]);
Ruby
stack({ gap: 3 }, [box("one"), box("two"), box("three")])
HTML
<div class="klods-stack klods-stack--gap-3">
  <div class="klods-box">
    one
  </div>
  <div class="klods-box">
    two
  </div>
  <div class="klods-box">
    three
  </div>
</div>

Stack — narrow

Constrains width and centers — useful for forms and single-column flows.

one
two
three
TypeScript
stack({ gap: 3, narrow: true }, [
  box("one"),
  box("two"),
  box("three"),
]);
Ruby
stack({ gap: 3, narrow: true }, [
  box("one"),
  box("two"),
  box("three"),
])
HTML
<div class="klods-stack klods-stack--gap-3 klods-stack--narrow">
  <div class="klods-box">
    one
  </div>
  <div class="klods-box">
    two
  </div>
  <div class="klods-box">
    three
  </div>
</div>

Row — horizontal, no wrap

left
middle
right
TypeScript
row({ gap: 3 }, [box("left"), box("middle"), box("right")]);
Ruby
row({ gap: 3 }, [box("left"), box("middle"), box("right")])
HTML
<div class="klods-row klods-row--gap-3">
  <div class="klods-box">
    left
  </div>
  <div class="klods-box">
    middle
  </div>
  <div class="klods-box">
    right
  </div>
</div>

Cluster — horizontal, wraps

JavaScript
TypeScript
CSS
Accessibility
Performance
HTML
Responsive
Animation
Grid
Flexbox
Dark mode
Forms
Typography
Tokens
Theming
TypeScript
cluster({ gap: 3 }, [
  box("JavaScript"),
  box("TypeScript"),
  box("CSS"),
  box("Accessibility"),
  box("Performance"),
  box("HTML"),
  box("Responsive"),
  box("Animation"),
  box("Grid"),
  box("Flexbox"),
  box("Dark mode"),
  box("Forms"),
  box("Typography"),
  box("Tokens"),
  box("Theming"),
]);
Ruby
cluster({ gap: 3 }, [
  box("JavaScript"),
  box("TypeScript"),
  box("CSS"),
  box("Accessibility"),
  box("Performance"),
  box("HTML"),
  box("Responsive"),
  box("Animation"),
  box("Grid"),
  box("Flexbox"),
  box("Dark mode"),
  box("Forms"),
  box("Typography"),
  box("Tokens"),
  box("Theming"),
])
HTML
<div class="klods-cluster klods-cluster--gap-3">
  <div class="klods-box">
    JavaScript
  </div>
  <div class="klods-box">
    TypeScript
  </div>
  <div class="klods-box">
    CSS
  </div>
  <div class="klods-box">
    Accessibility
  </div>
  <div class="klods-box">
    Performance
  </div>
  <div class="klods-box">
    HTML
  </div>
  <div class="klods-box">
    Responsive
  </div>
  <div class="klods-box">
    Animation
  </div>
  <div class="klods-box">
    Grid
  </div>
  <div class="klods-box">
    Flexbox
  </div>
  <div class="klods-box">
    Dark mode
  </div>
  <div class="klods-box">
    Forms
  </div>
  <div class="klods-box">
    Typography
  </div>
  <div class="klods-box">
    Tokens
  </div>
  <div class="klods-box">
    Theming
  </div>
</div>

Push — move siblings to the end of a flex row

start
end
TypeScript
cluster([box("start"), push(), box("end")]);
Ruby
cluster([box("start"), push(), box("end")])
HTML
<div class="klods-cluster">
  <div class="klods-box">
    start
  </div>
  <span class="klods-push">
  </span>
  <div class="klods-box">
    end
  </div>
</div>

Spread — push children apart

start
end
TypeScript
spread([box("start"), box("end")]);
Ruby
spread([box("start"), box("end")])
HTML
<div class="klods-spread">
  <div class="klods-box">
    start
  </div>
  <div class="klods-box">
    end
  </div>
</div>

Fill — grow to fill available space

fill
fill
fixed
fixed
TypeScript
cluster([
  fill([box("fill"), box("fill")]),
  box("fixed"),
  box("fixed"),
]);
Ruby
cluster([
  fill([box("fill"), box("fill")]),
  box("fixed"),
  box("fixed"),
])
HTML
<div class="klods-cluster">
  <div class="klods-fill">
    <div class="klods-box">
      fill
    </div>
    <div class="klods-box">
      fill
    </div>
  </div>
  <div class="klods-box">
    fixed
  </div>
  <div class="klods-box">
    fixed
  </div>
</div>

Fill with push — push content to the end

pushed to end
TypeScript
fill([push(), box("pushed to end")]);
Ruby
fill([push(), box("pushed to end")])
HTML
<div class="klods-fill">
  <span class="klods-push">
  </span>
  <div class="klods-box">
    pushed to end
  </div>
</div>

Fill for left, center, right layout

left
center
right
TypeScript
header([
  fill(box("left")),
  box("center"),
  fill([push(), box("right")]),
]);
Ruby
header([
  fill(box("left")),
  box("center"),
  fill([push(), box("right")]),
])
HTML
<header class="klods-header">
  <div class="klods-fill">
    <div class="klods-box">
      left
    </div>
  </div>
  <div class="klods-box">
    center
  </div>
  <div class="klods-fill">
    <span class="klods-push">
    </span>
    <div class="klods-box">
      right
    </div>
  </div>
</header>

Grid — equal columns

1
2
3
4
5
6
TypeScript
grid({ cols: 3, gap: 3 }, [
  box("1"),
  box("2"),
  box("3"),
  box("4"),
  box("5"),
  box("6"),
]);
Ruby
grid({ cols: 3, gap: 3 }, [
  box("1"),
  box("2"),
  box("3"),
  box("4"),
  box("5"),
  box("6"),
])
HTML
<div class="klods-grid klods-grid--cols-3 klods-grid--gap-3">
  <div class="klods-box">
    1
  </div>
  <div class="klods-box">
    2
  </div>
  <div class="klods-box">
    3
  </div>
  <div class="klods-box">
    4
  </div>
  <div class="klods-box">
    5
  </div>
  <div class="klods-box">
    6
  </div>
</div>

Grid — fit — auto-responsive

Sets the minimum item width via `--klods-grid-min` (defaults to 16rem).

a
b
c
d
TypeScript
grid({ fit: true, gap: 3 }, [
  box("a"),
  box("b"),
  box("c"),
  box("d"),
]);
Ruby
grid({ fit: true, gap: 3 }, [
  box("a"),
  box("b"),
  box("c"),
  box("d"),
])
HTML
<div class="klods-grid klods-grid--fit klods-grid--gap-3">
  <div class="klods-box">
    a
  </div>
  <div class="klods-box">
    b
  </div>
  <div class="klods-box">
    c
  </div>
  <div class="klods-box">
    d
  </div>
</div>

Center — center everything

centered
TypeScript
center({ style: "min-height: 10rem;" }, box("centered"));
Ruby
center({ style: "min-height: 10rem;" }, box("centered"))
HTML
<div style="min-height: 10rem;" class="klods-center">
  <div class="klods-box">
    centered
  </div>
</div>

Hide (responsive) — hide-mobile and hide-tablet

klods-hide-mobile hides an element on screens ≤ 480 px (phones). klods-hide-tablet hides an element on screens ≤ 768 px (phones and tablets). Resize the window to see the badges appear and disappear.

Always visible
Hidden on mobile (≤ 480 px)Hidden on tablet + mobile (≤ 768 px)
TypeScript
cluster([
  box("Always visible"),
  badge(
    { class: "klods-hide-mobile", variant: "accent" },
    "Hidden on mobile (≤ 480 px)"
  ),
  badge(
    { class: "klods-hide-tablet", variant: "success" },
    "Hidden on tablet + mobile (≤ 768 px)"
  ),
]);
Ruby
cluster([
  box("Always visible"),
  badge(
    { class: "klods-hide-mobile", variant: "accent" },
    "Hidden on mobile (≤ 480 px)"
  ),
  badge(
    { class: "klods-hide-tablet", variant: "success" },
    "Hidden on tablet + mobile (≤ 768 px)"
  ),
])
HTML
<div class="klods-cluster">
  <div class="klods-box">
    Always visible
  </div>
  <span class="klods-badge klods-badge--accent klods-hide-mobile">
    Hidden on mobile (≤ 480 px)
  </span>
  <span class="klods-badge klods-badge--success klods-hide-tablet">
    Hidden on tablet + mobile (≤ 768 px)
  </span>
</div>

Components

Opinionated defaults; extend any of them with class, id, data-* etc.

Alert

TypeScript
stack([
  alert({ variant: "info" }, "Heads up — info."),
  alert({ variant: "success" }, "All good."),
  alert({ variant: "warning" }, "Be careful."),
  alert({ variant: "danger" }, "Something broke."),
]);
Ruby
stack([
  alert({ variant: "info" }, "Heads up — info."),
  alert({ variant: "success" }, "All good."),
  alert({ variant: "warning" }, "Be careful."),
  alert({ variant: "danger" }, "Something broke."),
])
HTML
<div class="klods-stack">
  <div role="alert" class="klods-alert klods-alert--info">
    Heads up — info.
  </div>
  <div role="alert" class="klods-alert klods-alert--success">
    All good.
  </div>
  <div role="alert" class="klods-alert klods-alert--warning">
    Be careful.
  </div>
  <div role="alert" class="klods-alert klods-alert--danger">
    Something broke.
  </div>
</div>

Avatar

Without a `src`, the avatar displays initials derived from `name`.

TypeScript
cluster({ gap: 3, align: "center" }, [
  avatar({ size: "small", name: "Ari Smith" }),
  avatar({ name: "Ari Smith" }),
  avatar({ size: "large", name: "Ari Smith" }),
]);
Ruby
cluster({ gap: 3, align: "center" }, [
  avatar({ size: "small", name: "Ari Smith" }),
  avatar({ name: "Ari Smith" }),
  avatar({ size: "large", name: "Ari Smith" }),
])
HTML
<div align="center" class="klods-cluster klods-cluster--gap-3">
  <span class="klods-avatar klods-avatar--small klods-avatar--initials" role="img" aria-label="Ari Smith">
    <span aria-hidden="true">
      AS
    </span>
  </span>
  <span class="klods-avatar klods-avatar--initials" role="img" aria-label="Ari Smith">
    <span aria-hidden="true">
      AS
    </span>
  </span>
  <span class="klods-avatar klods-avatar--large klods-avatar--initials" role="img" aria-label="Ari Smith">
    <span aria-hidden="true">
      AS
    </span>
  </span>
</div>

Avatar — with image

Ari SmithAri SmithAri Smith
TypeScript
cluster({ gap: 3, align: "center" }, [
  avatar({
    size: "small",
    src: "https://i.pravatar.cc/96?img=53",
    name: "Ari Smith",
  }),
  avatar({
    src: "https://i.pravatar.cc/96?img=53",
    name: "Ari Smith",
  }),
  avatar({
    size: "large",
    src: "https://i.pravatar.cc/96?img=53",
    name: "Ari Smith",
  }),
]);
Ruby
cluster({ gap: 3, align: "center" }, [
  avatar({
    size: "small",
    src: "https://i.pravatar.cc/96?img=53",
    name: "Ari Smith",
  }),
  avatar({
    src: "https://i.pravatar.cc/96?img=53",
    name: "Ari Smith",
  }),
  avatar({
    size: "large",
    src: "https://i.pravatar.cc/96?img=53",
    name: "Ari Smith",
  }),
])
HTML
<div align="center" class="klods-cluster klods-cluster--gap-3">
  <span class="klods-avatar klods-avatar--small">
    <img src="https://i.pravatar.cc/96?img=53" alt="Ari Smith" class="klods-avatar__img" />
  </span>
  <span class="klods-avatar">
    <img src="https://i.pravatar.cc/96?img=53" alt="Ari Smith" class="klods-avatar__img" />
  </span>
  <span class="klods-avatar klods-avatar--large">
    <img src="https://i.pravatar.cc/96?img=53" alt="Ari Smith" class="klods-avatar__img" />
  </span>
</div>

Avatar — icon fallback

Omit both `src` and `name` to show a generic user icon.

TypeScript
cluster({ gap: 3, align: "center" }, [
  avatar({ size: "small" }),
  avatar(),
  avatar({ size: "large" }),
]);
Ruby
cluster({ gap: 3, align: "center" }, [
  avatar({ size: "small" }),
  avatar(),
  avatar({ size: "large" }),
])
HTML
<div align="center" class="klods-cluster klods-cluster--gap-3">
  <span class="klods-avatar klods-avatar--small">
    <span aria-hidden="true" class="klods-icon">
      <svg width="12" height="12" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <circle cx="8" cy="6" r="2.5" stroke="currentColor" stroke-width="1.5"/>
        <path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" d="M2.5 14c0-2.8 2.5-5 5.5-5s5.5 2.2 5.5 5"/>
      </svg>
    </span>
  </span>
  <span class="klods-avatar">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <circle cx="8" cy="6" r="2.5" stroke="currentColor" stroke-width="1.5"/>
        <path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" d="M2.5 14c0-2.8 2.5-5 5.5-5s5.5 2.2 5.5 5"/>
      </svg>
    </span>
  </span>
  <span class="klods-avatar klods-avatar--large">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <circle cx="8" cy="6" r="2.5" stroke="currentColor" stroke-width="1.5"/>
        <path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" d="M2.5 14c0-2.8 2.5-5 5.5-5s5.5 2.2 5.5 5"/>
      </svg>
    </span>
  </span>
</div>

Badge

defaultaccentsuccesswarninginfodanger
TypeScript
cluster([
  badge("default"),
  badge({ variant: "accent" }, "accent"),
  badge({ variant: "success" }, "success"),
  badge({ variant: "warning" }, "warning"),
  badge({ variant: "info" }, "info"),
  badge({ variant: "danger" }, "danger"),
]);
Ruby
cluster([
  badge("default"),
  badge({ variant: "accent" }, "accent"),
  badge({ variant: "success" }, "success"),
  badge({ variant: "warning" }, "warning"),
  badge({ variant: "info" }, "info"),
  badge({ variant: "danger" }, "danger"),
])
HTML
<div class="klods-cluster">
  <span class="klods-badge">
    default
  </span>
  <span class="klods-badge klods-badge--accent">
    accent
  </span>
  <span class="klods-badge klods-badge--success">
    success
  </span>
  <span class="klods-badge klods-badge--warning">
    warning
  </span>
  <span class="klods-badge klods-badge--info">
    info
  </span>
  <span class="klods-badge klods-badge--danger">
    danger
  </span>
</div>

Box

A padded container with a surface background.
Great for grouping related content, call-outs, or as a visual placeholder.
TypeScript
stack({ gap: 3 }, [
  box("A padded container with a surface background."),
  box(
    "Great for grouping related content, call-outs, or as a visual placeholder."
  ),
]);
Ruby
stack({ gap: 3 }, [
  box("A padded container with a surface background."),
  box(
    "Great for grouping related content, call-outs, or visual placeholder."
  ),
])
HTML
<div class="klods-stack klods-stack--gap-3">
  <div class="klods-box">
    A padded container with a surface background.
  </div>
  <div class="klods-box">
    Great for grouping related content, call-outs, or as a visual placeholder.
  </div>
</div>

Button group

TypeScript
buttonGroup({ role: "group", "aria-label": "View" }, [
  button({ variant: "ghost" }, "Day"),
  button({ variant: "ghost" }, "Week"),
  button({ variant: "ghost" }, "Month"),
]);
Ruby
button_group({ role: "group", "aria-label": "View" }, [
  button({ variant: "ghost" }, "Day"),
  button({ variant: "ghost" }, "Week"),
  button({ variant: "ghost" }, "Month"),
])
HTML
<div role="group" aria-label="View" class="klods-button-group">
  <button type="button" class="klods-button klods-button--ghost">
    Day
  </button>
  <button type="button" class="klods-button klods-button--ghost">
    Week
  </button>
  <button type="button" class="klods-button klods-button--ghost">
    Month
  </button>
</div>

Button group with active state

TypeScript
buttonGroup({ role: "group", "aria-label": "View" }, [
  button({ variant: "ghost", "aria-pressed": "true" }, "Day"),
  button({ variant: "ghost", "aria-pressed": "false" }, "Week"),
  button({ variant: "ghost", "aria-pressed": "false" }, "Month"),
]);
Ruby
button_group({ role: "group", "aria-label": "View" }, [
  button({ variant: "ghost", "aria-pressed": "true" }, "Day"),
  button({ variant: "ghost", "aria-pressed": "false" }, "Week"),
  button({ variant: "ghost", "aria-pressed": "false" }, "Month"),
])
HTML
<div role="group" aria-label="View" class="klods-button-group">
  <button type="button" aria-pressed="true" class="klods-button klods-button--ghost">
    Day
  </button>
  <button type="button" aria-pressed="false" class="klods-button klods-button--ghost">
    Week
  </button>
  <button type="button" aria-pressed="false" class="klods-button klods-button--ghost">
    Month
  </button>
</div>

Button

TypeScript
cluster([
  button("Default"),
  button({ variant: "primary" }, "Primary"),
  button({ variant: "danger" }, "Danger"),
  button({ variant: "ghost" }, "Ghost"),
]);
Ruby
cluster([
  button("Default"),
  button({ variant: "primary" }, "Primary"),
  button({ variant: "danger" }, "Danger"),
  button({ variant: "ghost" }, "Ghost"),
])
HTML
<div class="klods-cluster">
  <button type="button" class="klods-button">
    Default
  </button>
  <button type="button" class="klods-button klods-button--primary">
    Primary
  </button>
  <button type="button" class="klods-button klods-button--danger">
    Danger
  </button>
  <button type="button" class="klods-button klods-button--ghost">
    Ghost
  </button>
</div>

Extending — pass through any attribute

Every builder accepts class, id, data-*, aria-*, style, event handlers, etc.

TypeScript
button(
  {
    variant: "primary",
    id: "save-btn",
    class: "my-tracker",
    "data-event": "click_save",
    "aria-label": "Save the document",
    onClick: () => alert("(no-op in the docs)"),
  },
  "Save"
);
Ruby
button(
  {
    variant: "primary",
    id: "save-btn",
    class: "my-tracker",
    "data-event": "click_save",
    "aria-label": "Save the document"},
  "Save"
)
HTML
<button type="button" id="save-btn" class="klods-button klods-button--primary my-tracker" data-event="click_save" aria-label="Save the document">
  Save
</button>

Card

Cosy card

Cards stack a title, a body and an optional footer with sensible spacing.
TypeScript
card({ style: "max-width: 24rem;" }, [
  cardTitle("Cosy card"),
  cardBody(
    "Cards stack a title, a body and an optional footer with sensible spacing."
  ),
  cardFooter([
    button({ variant: "primary" }, "OK"),
    button("Cancel"),
  ]),
]);
Ruby
card({ style: "max-width: 24rem;" }, [
  card_title("Cosy card"),
  card_body(
    "Cards stack a title, a body and an optional footer with sensible spacing."
  ),
  card_footer([
    button({ variant: "primary" }, "OK"),
    button("Cancel"),
  ]),
])
HTML
<div style="max-width: 24rem;" class="klods-card">
  <h3 class="klods-card__title">
    Cosy card
  </h3>
  <div class="klods-card__body">
    Cards stack a title, a body and an optional footer with sensible spacing.
  </div>
  <div class="klods-card__footer">
    <button type="button" class="klods-button klods-button--primary">
      OK
    </button>
    <button type="button" class="klods-button">
      Cancel
    </button>
  </div>
</div>

Elevated card

Add `elevated: true` to make the card stand out from the background with a shadow.

Cosy card

Cards stack a title, a body and an optional footer with sensible spacing.
TypeScript
card({ elevated: true, style: "max-width: 24rem;" }, [
  cardTitle("Cosy card"),
  cardBody(
    "Cards stack a title, a body and an optional footer with sensible spacing."
  ),
  cardFooter([
    button({ variant: "primary" }, "OK"),
    button("Cancel"),
  ]),
]);
Ruby
card({ elevated: true, style: "max-width: 24rem;" }, [
  card_title("Cosy card"),
  card_body(
    "Cards stack a title, a body and an optional footer with sensible spacing."
  ),
  card_footer([
    button({ variant: "primary" }, "OK"),
    button("Cancel"),
  ]),
])
HTML
<div style="max-width: 24rem;" class="klods-card klods-card--elevated">
  <h3 class="klods-card__title">
    Cosy card
  </h3>
  <div class="klods-card__body">
    Cards stack a title, a body and an optional footer with sensible spacing.
  </div>
  <div class="klods-card__footer">
    <button type="button" class="klods-button klods-button--primary">
      OK
    </button>
    <button type="button" class="klods-button">
      Cancel
    </button>
  </div>
</div>

Code — inline

Run npm install klods-js klods-css to get started.

TypeScript
p([
  "Run ",
  inlineCode("npm install klods-js klods-css"),
  " to get started.",
]);
Ruby
p([
  "Run ",
  inline_code("npm install klods-js klods-css"),
  " to get started.",
])
HTML
<p>
  Run
  <code class="klods-code">
    npm install klods-js klods-css
  </code>
  to get started.
</p>

Code — block

import { page, header, content } from "klods-js";

page([
  header("Hello"),
  content("World"),
]).render(document.body);
TypeScript
codeBlock(
  'import { page, header, content } from "klods-js";\n\npage([\n  header("Hello"),\n  content("World"),\n]).render(document.body);'
);
Ruby
code_block(
  'import { page, header, content } from "klods-js";\n\npage([\n  header("Hello"),\n  content("World"),\n]).render(document.body);'
)
HTML
<pre class="klods-pre">
  <code>
    import { page, header, content } from &quot;klods-js&quot;;

page([
  header(&quot;Hello&quot;),
  content(&quot;World&quot;),
]).render(document.body);
  </code>
</pre>

KBD — keyboard input

Use `kbd` to denote keyboard keys or combinations. Renders with a key-cap style.

Press Cmd + Shift + P to open the command palette.

TypeScript
p([
  "Press ",
  kbd("Cmd"),
  " + ",
  kbd("Shift"),
  " + ",
  kbd("P"),
  " to open the command palette.",
]);
Ruby
p([
  "Press ",
  kbd("Cmd"),
  " + ",
  kbd("Shift"),
  " + ",
  kbd("P"),
  " to open the command palette.",
])
HTML
<p>
  Press
  <kbd class="klods-kbd">
    Cmd
  </kbd>
  +
  <kbd class="klods-kbd">
    Shift
  </kbd>
  +
  <kbd class="klods-kbd">
    P
  </kbd>
  to open the command palette.
</p>

Samp — sample output

Use `samp` for sample output from a program or command.

The command returned: Error: cannot find module 'klods-js'

TypeScript
p([
  "The command returned: ",
  samp("Error: cannot find module 'klods-js'"),
]);
Ruby
p([
  "The command returned: ",
  samp("Error: cannot find module 'klods-js'"),
])
HTML
<p>
  The command returned:
  <samp class="klods-samp">
    Error: cannot find module &#39;klods-js&#39;
  </samp>
</p>

Var — variable name

Use `varEl` to mark up a variable or placeholder in code or prose. Renders in italic monospace.

If count exceeds maxRetries then the process will terminate.

TypeScript
p([
  "If ",
  varEl("count"),
  " exceeds ",
  varEl("maxRetries"),
  " then the process will terminate.",
]);
Ruby
p([
  "If ",
  var_el("count"),
  " exceeds ",
  var_el("maxRetries"),
  " then the process will terminate.",
])
HTML
<p>
  If
  <var class="klods-var">
    count
  </var>
  exceeds
  <var class="klods-var">
    maxRetries
  </var>
  then the process will terminate.
</p>

Code — combined

All five inline code helpers together in context.

To start the dev server, press F5 or run npm run dev. The output Local: http://localhost:5173/ will appear in the terminal. The port is stored in PORT.

TypeScript
p([
  "To start the dev server, press ",
  kbd("F5"),
  " or run ",
  inlineCode("npm run dev"),
  ". The output ",
  samp("Local: http://localhost:5173/"),
  " will appear in the terminal. The port is stored in ",
  varEl("PORT"),
  ".",
]);
Ruby
p([
  "To start the dev server, press ",
  kbd("F5"),
  " or run ",
  inline_code("npm run dev"),
  ". The output ",
  samp("Local: http://localhost:5173/"),
  " will appear in the terminal. The port is stored in ",
  var_el("PORT"),
  ".",
])
HTML
<p>
  To start the dev server, press
  <kbd class="klods-kbd">
    F5
  </kbd>
  or run
  <code class="klods-code">
    npm run dev
  </code>
  . The output
  <samp class="klods-samp">
    Local: http://localhost:5173/
  </samp>
  will appear in the terminal. The port is stored in
  <var class="klods-var">
    PORT
  </var>
  .
</p>

Details

What is klods?

klods is a CSS design system and TypeScript component library for building apps without a heavy JS framework.

TypeScript
details([
  summary("What is klods?"),
  p(
    "klods is a CSS design system and TypeScript component library for building apps without a heavy JS framework."
  ),
]);
Ruby
details([
  summary("What is klods?"),
  p(
    "klods is a CSS design system and TypeScript component library for building apps without a heavy JS framework."
  ),
])
HTML
<details class="klods-details">
  <summary>
    What is klods?
  </summary>
  <p>
    klods is a CSS design system and TypeScript component library for building apps without a heavy JS framework.
  </p>
</details>

Details — open by default

Pass open to expand the panel on first render.

Answer visible on load

The native open attribute is passed straight through to the <details> element.

TypeScript
details({ open: true }, [
  summary("Answer visible on load"),
  p(
    "The native open attribute is passed straight through to the <details> element."
  ),
]);
Ruby
details({ open: true }, [
  summary("Answer visible on load"),
  p(
    "The native open attribute is passed straight through to the <details> element."
  ),
])
HTML
<details open class="klods-details">
  <summary>
    Answer visible on load
  </summary>
  <p>
    The native open attribute is passed straight through to the &lt;details&gt; element.
  </p>
</details>

Details — FAQ group

Stack several details together for a FAQ list. Each item manages its own open/closed state natively — no JS required.

Do I need a build step?

No. You can link klods-css directly from a CDN and use BEM classes in plain HTML.

Can I use it with React or Vue?

Yes. The klods-js builders output plain HTML strings or DOM nodes, so they compose cleanly with any framework.

Is it accessible?

Yes. The native <details>/<summary> elements have built-in keyboard support and are announced correctly by screen readers.

TypeScript
stack({ gap: 2 }, [
  details([
    summary("Do I need a build step?"),
    p(
      "No. You can link klods-css directly from a CDN and use BEM classes in plain HTML."
    ),
  ]),
  details([
    summary("Can I use it with React or Vue?"),
    p(
      "Yes. The klods-js builders output plain HTML strings or DOM nodes, so they compose cleanly with any framework."
    ),
  ]),
  details([
    summary("Is it accessible?"),
    p(
      "Yes. The native <details>/<summary> elements have built-in keyboard support and are announced correctly by screen readers."
    ),
  ]),
]);
Ruby
stack({ gap: 2 }, [
  details([
    summary("Do I need a build step?"),
    p(
      "No. You can link klods-css directly from a CDN and use BEM classes in plain HTML."
    ),
  ]),
  details([
    summary("Can I use it with React or Vue?"),
    p(
      "Yes. The klods-js builders output plain HTML strings or DOM nodes, so they compose cleanly with any framework."
    ),
  ]),
  details([
    summary("Is it accessible?"),
    p(
      "Yes. The native <details>/<summary> elements have built-in keyboard support and are announced correctly by screen readers."
    ),
  ]),
])
HTML
<div class="klods-stack klods-stack--gap-2">
  <details class="klods-details">
    <summary>
      Do I need a build step?
    </summary>
    <p>
      No. You can link klods-css directly from a CDN and use BEM classes in plain HTML.
    </p>
  </details>
  <details class="klods-details">
    <summary>
      Can I use it with React or Vue?
    </summary>
    <p>
      Yes. The klods-js builders output plain HTML strings or DOM nodes, so they compose cleanly with any framework.
    </p>
  </details>
  <details class="klods-details">
    <summary>
      Is it accessible?
    </summary>
    <p>
      Yes. The native &lt;details&gt;/&lt;summary&gt; elements have built-in keyboard support and are announced correctly by screen readers.
    </p>
  </details>
</div>

Description list

Author
Ari Smith
Published
27 June 2026
Category
Design systems
TypeScript
dl([
  dt("Author"),
  dd("Ari Smith"),
  dt("Published"),
  dd("27 June 2026"),
  dt("Category"),
  dd("Design systems"),
]);
Ruby
dl([
  dt("Author"),
  dd("Ari Smith"),
  dt("Published"),
  dd("27 June 2026"),
  dt("Category"),
  dd("Design systems"),
])
HTML
<dl class="klods-dl">
  <dt class="klods-dt">
    Author
  </dt>
  <dd class="klods-dd">
    Ari Smith
  </dd>
  <dt class="klods-dt">
    Published
  </dt>
  <dd class="klods-dd">
    27 June 2026
  </dd>
  <dt class="klods-dt">
    Category
  </dt>
  <dd class="klods-dd">
    Design systems
  </dd>
</dl>

Description list — inline

Add `inline: true` for a two-column layout that pairs each term with its detail on the same row.

Author
Ari Smith
Published
27 June 2026
Category
Design systems
TypeScript
dl({ inline: true }, [
  dt("Author"),
  dd("Ari Smith"),
  dt("Published"),
  dd("27 June 2026"),
  dt("Category"),
  dd("Design systems"),
]);
Ruby
dl({ inline: true }, [
  dt("Author"),
  dd("Ari Smith"),
  dt("Published"),
  dd("27 June 2026"),
  dt("Category"),
  dd("Design systems"),
])
HTML
<dl class="klods-dl klods-dl--inline">
  <dt class="klods-dt">
    Author
  </dt>
  <dd class="klods-dd">
    Ari Smith
  </dd>
  <dt class="klods-dt">
    Published
  </dt>
  <dd class="klods-dd">
    27 June 2026
  </dd>
  <dt class="klods-dt">
    Category
  </dt>
  <dd class="klods-dd">
    Design systems
  </dd>
</dl>

Description list — inside a card

An inline description list pairs naturally with a card to display structured metadata.

Package details

Package
klods-js
Version
v1.0.0
License
MIT
Downloads
12,345
TypeScript
card({ style: "max-width: 24rem;" }, [
  cardTitle("Package details"),
  dl({ inline: true }, [
    dt("Package"),
    dd("klods-js"),
    dt("Version"),
    dd(badge({ variant: "success" }, "v1.0.0")),
    dt("License"),
    dd("MIT"),
    dt("Downloads"),
    dd("12,345"),
  ]),
]);
Ruby
card({ style: "max-width: 24rem;" }, [
  card_title("Package details"),
  dl({ inline: true }, [
    dt("Package"),
    dd("klods-js"),
    dt("Version"),
    dd(badge({ variant: "success" }, "v1.0.0")),
    dt("License"),
    dd("MIT"),
    dt("Downloads"),
    dd("12,345"),
  ]),
])
HTML
<div style="max-width: 24rem;" class="klods-card">
  <h3 class="klods-card__title">
    Package details
  </h3>
  <dl class="klods-dl klods-dl--inline">
    <dt class="klods-dt">
      Package
    </dt>
    <dd class="klods-dd">
      klods-js
    </dd>
    <dt class="klods-dt">
      Version
    </dt>
    <dd class="klods-dd">
      <span class="klods-badge klods-badge--success">
        v1.0.0
      </span>
    </dd>
    <dt class="klods-dt">
      License
    </dt>
    <dd class="klods-dd">
      MIT
    </dd>
    <dt class="klods-dt">
      Downloads
    </dt>
    <dd class="klods-dd">
      12,345
    </dd>
  </dl>
</div>

Form - Text inputs

Basic text controls with placeholder and label.

TypeScript
stack({ gap: 4 }, [
  field({ label: "Full name" }, (id) =>
    input({ id, type: "text", placeholder: "Ari Smith" })
  ),
  field({ label: "Email address" }, (id) =>
    input({ id, type: "email", placeholder: "ari@example.com" })
  ),
  field({ label: "Password" }, (id) =>
    input({ id, type: "password", placeholder: "••••••••" })
  ),
]);
Ruby
stack({ gap: 4 }, [
  field({ label: "Full name" }) do |id|
    input({ id: id, type: "text", placeholder: "Ari Smith" })
  end,
  field({ label: "Email address" }) do |id|
    input({ id: id, type: "email", placeholder: "ari@example.com" })
  end,
  field({ label: "Password" }) do |id|
    input({ id: id, type: "password", placeholder: "••••••••" })
  end,
])
HTML
<div class="klods-stack klods-stack--gap-4">
  <div class="klods-field">
    <label for="klods-field-full-name" class="klods-label">
      Full name
    </label>
    <input type="text" id="klods-field-full-name" placeholder="Ari Smith" class="klods-input" />
  </div>
  <div class="klods-field">
    <label for="klods-field-email-address" class="klods-label">
      Email address
    </label>
    <input type="email" id="klods-field-email-address" placeholder="ari@example.com" class="klods-input" />
  </div>
  <div class="klods-field">
    <label for="klods-field-password" class="klods-label">
      Password
    </label>
    <input type="password" id="klods-field-password" placeholder="••••••••" class="klods-input" />
  </div>
</div>

Form - Required field

Add `required: true` to get the * marker and correct semantics.

TypeScript
field({ label: "Username", required: true }, (id) =>
  input({
    id,
    type: "text",
    placeholder: "yourname",
    required: true,
  })
);
Ruby
field({ label: "Username", required: true }) do |id|
  input({
    id: id,
    type: "text",
    placeholder: "yourname",
    required: true,
  })
end
HTML
<div class="klods-field">
  <label for="klods-field-username" class="klods-label klods-label--required">
    Username
  </label>
  <input type="text" id="klods-field-username" placeholder="yourname" required class="klods-input" />
</div>

Form - Help text

Use `help` to add a hint below the control.

We'll only use this to send your receipt.

TypeScript
field(
  {
    label: "Email address",
    help: "We'll only use this to send your receipt.",
  },
  (id) =>
    input({ id, type: "email", placeholder: "ari@example.com" })
);
Ruby
field(
  {
    label: "Email address",
    help: "We'll only use this to send your receipt.",
  }) do |id|
  input({ id: id, type: "email", placeholder: "ari@example.com" })
end
HTML
<div class="klods-field">
  <label for="klods-field-email-address" class="klods-label">
    Email address
  </label>
  <input type="email" id="klods-field-email-address" placeholder="ari@example.com" class="klods-input" aria-describedby="klods-field-email-address-help" />
  <p id="klods-field-email-address-help" class="klods-help">
    We&#39;ll only use this to send your receipt.
  </p>
</div>

Form - Invalid state

Set `error` to show a validation message and apply the invalid styling. `aria-invalid` and `aria-describedby` are wired automatically.

TypeScript
stack({ gap: 4 }, [
  field(
    {
      label: "Email address",
      error: "Please enter a valid email address.",
    },
    (id) => input({ id, type: "email", value: "not-an-email" })
  ),
  field(
    {
      label: "Message",
      error: "Message must be at least 20 characters.",
    },
    (id) =>
      textarea({ id, placeholder: "Your message…" }, "Too short")
  ),
]);
Ruby
stack({ gap: 4 }, [
  field(
    {
      label: "Email address",
      error: "Please enter a valid email address.",
    }) do |id|
    input({ id: id, type: "email", value: "not-an-email" })
  end,
  field(
    {
      label: "Message",
      error: "Message must be at least 20 characters.",
    }) do |id|
    textarea({ id: id, placeholder: "Your message…" }, "Too short")
  end,
])
HTML
<div class="klods-stack klods-stack--gap-4">
  <div class="klods-field klods-field--invalid">
    <label for="klods-field-email-address" class="klods-label">
      Email address
    </label>
    <input type="email" id="klods-field-email-address" value="not-an-email" class="klods-input" aria-describedby="klods-field-email-address-error" aria-invalid="true" />
    <p id="klods-field-email-address-error" class="klods-error" role="alert">
      Please enter a valid email address.
    </p>
  </div>
  <div class="klods-field klods-field--invalid">
    <label for="klods-field-message" class="klods-label">
      Message
    </label>
    <textarea id="klods-field-message" placeholder="Your message…" class="klods-textarea" aria-describedby="klods-field-message-error" aria-invalid="true">
      Too short
    </textarea>
    <p id="klods-field-message-error" class="klods-error" role="alert">
      Message must be at least 20 characters.
    </p>
  </div>
</div>

Form - Input disabled

Pass `disabled` on the control itself.

TypeScript
stack({ gap: 4 }, [
  field({ label: "Locked field" }, (id) =>
    input({
      id,
      type: "text",
      value: "Cannot edit",
      disabled: true,
    })
  ),
  field({ label: "Role" }, (id) =>
    select({ id, disabled: true }, [
      option({ value: "" }, "Admin"),
      option({ selected: true, value: "viewer" }, "Viewer"),
    ])
  ),
]);
Ruby
stack({ gap: 4 }, [
  field({ label: "Locked field" }) do |id|
    input({
      id: id,
      type: "text",
      value: "Cannot edit",
      disabled: true,
    })
  end,
  field({ label: "Role" }) do |id|
    select({ id: id, disabled: true }, [
      option({ value: "" }, "Admin"),
      option({ selected: true, value: "viewer" }, "Viewer"),
    ])
  end,
])
HTML
<div class="klods-stack klods-stack--gap-4">
  <div class="klods-field">
    <label for="klods-field-locked-field" class="klods-label">
      Locked field
    </label>
    <input type="text" id="klods-field-locked-field" value="Cannot edit" disabled class="klods-input" />
  </div>
  <div class="klods-field">
    <label for="klods-field-role" class="klods-label">
      Role
    </label>
    <div class="klods-select-wrapper">
      <select id="klods-field-role" disabled class="klods-select">
        <option value="">
          Admin
        </option>
        <option selected value="viewer">
          Viewer
        </option>
      </select>
    </div>
  </div>
</div>

Form - Select and option

Use `select` and `option` for dropdowns.

Controls what the user can see and do.

TypeScript
stack({ gap: 4 }, [
  field({ label: "Country" }, (id) =>
    select({ id }, [
      option({ value: "" }, "— choose —"),
      option({ selected: true, value: "dk" }, "Denmark"),
      option({ value: "de" }, "Germany"),
      option({ value: "se" }, "Sweden"),
      option({ value: "no" }, "Norway"),
    ])
  ),
  field(
    {
      label: "Role",
      help: "Controls what the user can see and do.",
    },
    (id) =>
      select({ id }, [
        option({ value: "admin" }, "Admin"),
        option({ value: "editor" }, "Editor"),
        option({ value: "viewer" }, "Viewer"),
      ])
  ),
]);
Ruby
stack({ gap: 4 }, [
  field({ label: "Country" }) do |id|
    select({ id: id }, [
      option({ value: "" }, "— choose —"),
      option({ selected: true, value: "dk" }, "Denmark"),
      option({ value: "de" }, "Germany"),
      option({ value: "se" }, "Sweden"),
      option({ value: "no" }, "Norway"),
    ])
  end,
  field(
    {
      label: "Role",
      help: "Controls what the user can see and do.",
    }) do |id|
    select({ id: id }, [
      option({ value: "admin" }, "Admin"),
      option({ value: "editor" }, "Editor"),
      option({ value: "viewer" }, "Viewer"),
    ])
  end,
])
HTML
<div class="klods-stack klods-stack--gap-4">
  <div class="klods-field">
    <label for="klods-field-country" class="klods-label">
      Country
    </label>
    <div class="klods-select-wrapper">
      <select id="klods-field-country" class="klods-select">
        <option value="">
          — choose —
        </option>
        <option selected value="dk">
          Denmark
        </option>
        <option value="de">
          Germany
        </option>
        <option value="se">
          Sweden
        </option>
        <option value="no">
          Norway
        </option>
      </select>
    </div>
  </div>
  <div class="klods-field">
    <label for="klods-field-role" class="klods-label">
      Role
    </label>
    <div class="klods-select-wrapper">
      <select id="klods-field-role" class="klods-select" aria-describedby="klods-field-role-help">
        <option value="admin">
          Admin
        </option>
        <option value="editor">
          Editor
        </option>
        <option value="viewer">
          Viewer
        </option>
      </select>
    </div>
    <p id="klods-field-role-help" class="klods-help">
      Controls what the user can see and do.
    </p>
  </div>
</div>

Form - Textarea

Up to 500 characters.

TypeScript
field({ label: "Message", help: "Up to 500 characters." }, (id) =>
  textarea({ id, placeholder: "Write something nice…" })
);
Ruby
field({ label: "Message", help: "Up to 500 characters." }) do |id|
  textarea({ id: id, placeholder: "Write something nice…" })
end
HTML
<div class="klods-field">
  <label for="klods-field-message" class="klods-label">
    Message
  </label>
  <textarea id="klods-field-message" placeholder="Write something nice…" class="klods-textarea" aria-describedby="klods-field-message-help">
  </textarea>
  <p id="klods-field-message-help" class="klods-help">
    Up to 500 characters.
  </p>
</div>

Form - Input types

The `input` builder works with any native `type`. Each renders with the same base styling.

60
TypeScript
grid({ fit: true, gap: 4 }, [
  field({ label: "Date" }, (id) => input({ id, type: "date" })),
  field({ label: "Time" }, (id) => input({ id, type: "time" })),
  field({ label: "Datetime-local" }, (id) =>
    input({ id, type: "datetime-local" })
  ),
  field({ label: "Number" }, (id) =>
    input({
      id,
      type: "number",
      min: "1",
      max: "100",
      value: "42",
    })
  ),
  field({ label: "Range" }, (id) =>
    input({
      id,
      type: "range",
      min: "0",
      max: "100",
      value: "60",
    })
  ),
  field({ label: "Color" }, (id) =>
    input({ id, type: "color", value: "#6c63ff" })
  ),
  field({ label: "Tel" }, (id) =>
    input({ id, type: "tel", placeholder: "+45 12 34 56 78" })
  ),
  field({ label: "File" }, (id) => input({ id, type: "file" })),
  field({ label: "URL" }, (id) =>
    input({ id, type: "url", placeholder: "https://example.com" })
  ),
]);
Ruby
grid({ fit: true, gap: 4 }, [
  field({ label: "Date" }) { |id| input({ id: id, type: "date" }) },
  field({ label: "Time" }) { |id| input({ id: id, type: "time" }) },
  field({ label: "Datetime-local" }) do |id|
    input({ id: id, type: "datetime-local" })
  end,
  field({ label: "Number" }) do |id|
    input({
      id: id,
      type: "number",
      min: "1",
      max: "100",
      value: "42",
    })
  end,
  field({ label: "Range" }) do |id|
    input({
      id: id,
      type: "range",
      min: "0",
      max: "100",
      value: "60",
    })
  end,
  field({ label: "Color" }) do |id|
    input({ id: id, type: "color", value: "#6c63ff" })
  end,
  field({ label: "Tel" }) do |id|
    input({ id: id, type: "tel", placeholder: "+45 12 34 56 78" })
  end,
  field({ label: "File" }) { |id| input({ id: id, type: "file" }) },
  field({ label: "URL" }) do |id|
    input({ id: id, type: "url", placeholder: "https://example.com" })
  end,
])
HTML
<div class="klods-grid klods-grid--fit klods-grid--gap-4">
  <div class="klods-field">
    <label for="klods-field-date" class="klods-label">
      Date
    </label>
    <input type="date" id="klods-field-date" class="klods-input" />
  </div>
  <div class="klods-field">
    <label for="klods-field-time" class="klods-label">
      Time
    </label>
    <input type="time" id="klods-field-time" class="klods-input" />
  </div>
  <div class="klods-field">
    <label for="klods-field-datetime-local" class="klods-label">
      Datetime-local
    </label>
    <input type="datetime-local" id="klods-field-datetime-local" class="klods-input" />
  </div>
  <div class="klods-field">
    <label for="klods-field-number" class="klods-label">
      Number
    </label>
    <input type="number" id="klods-field-number" min="1" max="100" value="42" class="klods-input" />
  </div>
  <div class="klods-field">
    <label for="klods-field-range" class="klods-label">
      Range
    </label>
    <span class="klods-input klods-input--range">
      <input type="range" id="klods-field-range" min="0" max="100" value="60" />
      <output for="klods-field-range">
        60
      </output>
    </span>
  </div>
  <div class="klods-field">
    <label for="klods-field-color" class="klods-label">
      Color
    </label>
    <span class="klods-input klods-input--color">
      <input type="color" id="klods-field-color" value="#6c63ff" />
      <input type="text" class="klods-color-hex" value="#6c63ff" maxlength="7" spellcheck="false" aria-label="Hex color value" />
    </span>
  </div>
  <div class="klods-field">
    <label for="klods-field-tel" class="klods-label">
      Tel
    </label>
    <input type="tel" id="klods-field-tel" placeholder="+45 12 34 56 78" class="klods-input" />
  </div>
  <div class="klods-field">
    <label for="klods-field-file" class="klods-label">
      File
    </label>
    <input type="file" id="klods-field-file" class="klods-input" />
  </div>
  <div class="klods-field">
    <label for="klods-field-url" class="klods-label">
      URL
    </label>
    <input type="url" id="klods-field-url" placeholder="https://example.com" class="klods-input" />
  </div>
</div>

Form - Checkbox

TypeScript
stack({ gap: 2 }, [
  checkbox({
    label: "I agree to the terms and conditions",
    name: "terms",
  }),
  checkbox({
    label: "Subscribe to the newsletter",
    name: "newsletter",
    checked: true,
  }),
  checkbox({
    label: "This option is unavailable",
    name: "locked",
    disabled: true,
  }),
]);
Ruby
stack({ gap: 2 }, [
  checkbox({
    label: "I agree to the terms and conditions",
    name: "terms",
  }),
  checkbox({
    label: "Subscribe to the newsletter",
    name: "newsletter",
    checked: true,
  }),
  checkbox({
    label: "This option is unavailable",
    name: "locked",
    disabled: true,
  }),
])
HTML
<div class="klods-stack klods-stack--gap-2">
  <label class="klods-checkbox">
    <input type="checkbox" name="terms" />
    <span>
      I agree to the terms and conditions
    </span>
  </label>
  <label class="klods-checkbox">
    <input type="checkbox" name="newsletter" checked />
    <span>
      Subscribe to the newsletter
    </span>
  </label>
  <label class="klods-checkbox">
    <input type="checkbox" name="locked" disabled />
    <span>
      This option is unavailable
    </span>
  </label>
</div>

Form - Radio group

Wrap radios with `radioGroup` for correct `role="group"` and aria labeling.

Preferred contact method

TypeScript
radioGroup({ legend: "Preferred contact method" }, [
  radio({
    label: "Email",
    name: "contact",
    value: "email",
    checked: true,
  }),
  radio({ label: "Phone", name: "contact", value: "phone" }),
  radio({ label: "Post", name: "contact", value: "post" }),
]);
Ruby
radio_group({ legend: "Preferred contact method" }, [
  radio({
    label: "Email",
    name: "contact",
    value: "email",
    checked: true,
  }),
  radio({ label: "Phone", name: "contact", value: "phone" }),
  radio({ label: "Post", name: "contact", value: "post" }),
])
HTML
<div class="klods-field" role="group" aria-labelledby="klods-rg-preferred-contact-method">
  <p id="klods-rg-preferred-contact-method" class="klods-label">
    Preferred contact method
  </p>
  <label class="klods-radio">
    <input type="radio" name="contact" value="email" checked />
    <span>
      Email
    </span>
  </label>
  <label class="klods-radio">
    <input type="radio" name="contact" value="phone" />
    <span>
      Phone
    </span>
  </label>
  <label class="klods-radio">
    <input type="radio" name="contact" value="post" />
    <span>
      Post
    </span>
  </label>
</div>

Form - Switch

An accessible toggle. Uses `role="switch"` on the underlying checkbox.

TypeScript
stack({ gap: 3 }, [
  switchInput({ label: "Dark mode", name: "dark-mode" }),
  switchInput({
    label: "Email notifications",
    name: "notifications",
    checked: true,
  }),
  switchInput({
    label: "This setting is locked",
    name: "locked",
    disabled: true,
  }),
]);
Ruby
stack({ gap: 3 }, [
  switch_input({ label: "Dark mode", name: "dark-mode" }),
  switch_input({
    label: "Email notifications",
    name: "notifications",
    checked: true,
  }),
  switch_input({
    label: "This setting is locked",
    name: "locked",
    disabled: true,
  }),
])
HTML
<div class="klods-stack klods-stack--gap-3">
  <label class="klods-switch">
    <input type="checkbox" class="klods-switch__input" role="switch" name="dark-mode" />
    <span class="klods-switch__track">
    </span>
    <span class="klods-switch__label">
      Dark mode
    </span>
  </label>
  <label class="klods-switch">
    <input type="checkbox" class="klods-switch__input" role="switch" name="notifications" checked />
    <span class="klods-switch__track">
    </span>
    <span class="klods-switch__label">
      Email notifications
    </span>
  </label>
  <label class="klods-switch">
    <input type="checkbox" class="klods-switch__input" role="switch" name="locked" disabled />
    <span class="klods-switch__track">
    </span>
    <span class="klods-switch__label">
      This setting is locked
    </span>
  </label>
</div>

Form - Switch reverse

Use `reverse: true` to flip the layout — label on the left, toggle on the right. Useful inside settings panels where the label describes the setting.

TypeScript
stack({ gap: 3 }, [
  switchInput({
    label: "Dark mode",
    name: "dark-mode",
    reverse: true,
  }),
  switchInput({
    label: "Email notifications",
    name: "notifications",
    checked: true,
    reverse: true,
  }),
  switchInput({
    label: "This setting is locked",
    name: "locked",
    disabled: true,
    reverse: true,
  }),
]);
Ruby
stack({ gap: 3 }, [
  switch_input({
    label: "Dark mode",
    name: "dark-mode",
    reverse: true,
  }),
  switch_input({
    label: "Email notifications",
    name: "notifications",
    checked: true,
    reverse: true,
  }),
  switch_input({
    label: "This setting is locked",
    name: "locked",
    disabled: true,
    reverse: true,
  }),
])
HTML
<div class="klods-stack klods-stack--gap-3">
  <label class="klods-switch klods-switch--reverse">
    <input type="checkbox" class="klods-switch__input" role="switch" name="dark-mode" />
    <span class="klods-switch__track">
    </span>
    <span class="klods-switch__label">
      Dark mode
    </span>
  </label>
  <label class="klods-switch klods-switch--reverse">
    <input type="checkbox" class="klods-switch__input" role="switch" name="notifications" checked />
    <span class="klods-switch__track">
    </span>
    <span class="klods-switch__label">
      Email notifications
    </span>
  </label>
  <label class="klods-switch klods-switch--reverse">
    <input type="checkbox" class="klods-switch__input" role="switch" name="locked" disabled />
    <span class="klods-switch__track">
    </span>
    <span class="klods-switch__label">
      This setting is locked
    </span>
  </label>
</div>

Contact form example

A complete form using `grid` to lay out pairs of fields side by side. Names and contact details share a row; the message and actions sit full-width below.

TypeScript
card([
  form({ onSubmit: (e: Event) => e.preventDefault() }, [
    grid({ cols: 2, gap: 4 }, [
      field({ label: "First name", required: true }, (id) =>
        input({ id, type: "text", placeholder: "Ari" })
      ),
      field({ label: "Last name", required: true }, (id) =>
        input({ id, type: "text", placeholder: "Smith" })
      ),
    ]),
    grid({ cols: 2, gap: 4 }, [
      field({ label: "Email address", required: true }, (id) =>
        input({
          id,
          type: "email",
          placeholder: "ari@example.com",
        })
      ),
      field({ label: "Subject" }, (id) =>
        select({ id }, [
          option({ value: "" }, "— choose a topic —"),
          option({ value: "general" }, "General enquiry"),
          option({ value: "support" }, "Technical support"),
          option({ value: "billing" }, "Billing"),
        ])
      ),
    ]),
    field({ label: "Message", required: true }, (id) =>
      textarea({ id, placeholder: "How can we help you?" })
    ),
    spread([
      checkbox({
        label: "I agree to the privacy policy",
        name: "privacy",
        required: true,
      }),
      button(
        { variant: "primary", type: "submit" },
        "Send message"
      ),
    ]),
  ]),
]);
Ruby
card([
  form([
    grid({ cols: 2, gap: 4 }, [
      field({ label: "First name", required: true }) do |id|
        input({ id: id, type: "text", placeholder: "Ari" })
      end,
      field({ label: "Last name", required: true }) do |id|
        input({ id: id, type: "text", placeholder: "Smith" })
      end,
    ]),
    grid({ cols: 2, gap: 4 }, [
      field({ label: "Email address", required: true }) do |id|
        input({
          id: id,
          type: "email",
          placeholder: "ari@example.com",
        })
      end,
      field({ label: "Subject" }) do |id|
        select({ id: id }, [
          option({ value: "" }, "— choose a topic —"),
          option({ value: "general" }, "General enquiry"),
          option({ value: "support" }, "Technical support"),
          option({ value: "billing" }, "Billing"),
        ])
      end,
    ]),
    field({ label: "Message", required: true }) do |id|
      textarea({ id: id, placeholder: "How can we help you?" })
    end,
    spread([
      checkbox({
        label: "I agree to the privacy policy",
        name: "privacy",
        required: true,
      }),
      button(
        { variant: "primary", type: "submit" },
        "Send message"
      ),
    ]),
  ]),
])
HTML
<div class="klods-card">
  <form class="klods-form">
    <div class="klods-grid klods-grid--cols-2 klods-grid--gap-4">
      <div class="klods-field">
        <label for="klods-field-first-name" class="klods-label klods-label--required">
          First name
        </label>
        <input type="text" id="klods-field-first-name" placeholder="Ari" class="klods-input" />
      </div>
      <div class="klods-field">
        <label for="klods-field-last-name" class="klods-label klods-label--required">
          Last name
        </label>
        <input type="text" id="klods-field-last-name" placeholder="Smith" class="klods-input" />
      </div>
    </div>
    <div class="klods-grid klods-grid--cols-2 klods-grid--gap-4">
      <div class="klods-field">
        <label for="klods-field-email-address" class="klods-label klods-label--required">
          Email address
        </label>
        <input type="email" id="klods-field-email-address" placeholder="ari@example.com" class="klods-input" />
      </div>
      <div class="klods-field">
        <label for="klods-field-subject" class="klods-label">
          Subject
        </label>
        <div class="klods-select-wrapper">
          <select id="klods-field-subject" class="klods-select">
            <option value="">
              — choose a topic —
            </option>
            <option value="general">
              General enquiry
            </option>
            <option value="support">
              Technical support
            </option>
            <option value="billing">
              Billing
            </option>
          </select>
        </div>
      </div>
    </div>
    <div class="klods-field">
      <label for="klods-field-message" class="klods-label klods-label--required">
        Message
      </label>
      <textarea id="klods-field-message" placeholder="How can we help you?" class="klods-textarea">
      </textarea>
    </div>
    <div class="klods-spread">
      <label class="klods-checkbox">
        <input type="checkbox" name="privacy" required />
        <span>
          I agree to the privacy policy
        </span>
      </label>
      <button type="submit" class="klods-button klods-button--primary">
        Send message
      </button>
    </div>
  </form>
</div>

Settings panel example

A two-column settings card. Each column groups related settings under a `klods-label` heading. `reverse` switches keep the toggle on the right; `cardFooter` with `push` pins the save button to the right edge.

Preferences

Account

Notifications

TypeScript
card([
  cardTitle("Preferences"),
  grid({ cols: 2, gap: 5 }, [
    stack({ gap: 3 }, [
      p({ class: "klods-label" }, "Account"),
      switchInput({ label: "Public profile", name: "public" }),
      field({ label: "Language" }, (id) =>
        select({ id }, [
          option({ value: "en" }, "English"),
          option({ value: "da" }, "Dansk"),
          option({ value: "de" }, "Deutsch"),
        ])
      ),
    ]),
    stack({ gap: 3 }, [
      p({ class: "klods-label" }, "Notifications"),
      switchInput({
        label: "Enable notifications",
        name: "notif",
        checked: true,
      }),
      switchInput({ label: "Email digest", name: "digest" }),
      switchInput({
        label: "Push alerts",
        name: "push",
        checked: true,
      }),
    ]),
  ]),
  cardFooter([
    push(),
    button({ variant: "primary" }, "Save preferences"),
  ]),
]);
Ruby
card([
  card_title("Preferences"),
  grid({ cols: 2, gap: 5 }, [
    stack({ gap: 3 }, [
      p({ class: "klods-label" }, "Account"),
      switch_input({ label: "Public profile", name: "public" }),
      field({ label: "Language" }) do |id|
        select({ id: id }, [
          option({ value: "en" }, "English"),
          option({ value: "da" }, "Dansk"),
          option({ value: "de" }, "Deutsch"),
        ])
      end,
    ]),
    stack({ gap: 3 }, [
      p({ class: "klods-label" }, "Notifications"),
      switch_input({
        label: "Enable notifications",
        name: "notif",
        checked: true,
      }),
      switch_input({ label: "Email digest", name: "digest" }),
      switch_input({
        label: "Push alerts",
        name: "push",
        checked: true,
      }),
    ]),
  ]),
  card_footer([
    push(),
    button({ variant: "primary" }, "Save preferences"),
  ]),
])
HTML
<div class="klods-card">
  <h3 class="klods-card__title">
    Preferences
  </h3>
  <div class="klods-grid klods-grid--cols-2 klods-grid--gap-5">
    <div class="klods-stack klods-stack--gap-3">
      <p class="klods-label">
        Account
      </p>
      <label class="klods-switch">
        <input type="checkbox" class="klods-switch__input" role="switch" name="public" />
        <span class="klods-switch__track">
        </span>
        <span class="klods-switch__label">
          Public profile
        </span>
      </label>
      <div class="klods-field">
        <label for="klods-field-language" class="klods-label">
          Language
        </label>
        <div class="klods-select-wrapper">
          <select id="klods-field-language" class="klods-select">
            <option value="en">
              English
            </option>
            <option value="da">
              Dansk
            </option>
            <option value="de">
              Deutsch
            </option>
          </select>
        </div>
      </div>
    </div>
    <div class="klods-stack klods-stack--gap-3">
      <p class="klods-label">
        Notifications
      </p>
      <label class="klods-switch">
        <input type="checkbox" class="klods-switch__input" role="switch" name="notif" checked />
        <span class="klods-switch__track">
        </span>
        <span class="klods-switch__label">
          Enable notifications
        </span>
      </label>
      <label class="klods-switch">
        <input type="checkbox" class="klods-switch__input" role="switch" name="digest" />
        <span class="klods-switch__track">
        </span>
        <span class="klods-switch__label">
          Email digest
        </span>
      </label>
      <label class="klods-switch">
        <input type="checkbox" class="klods-switch__input" role="switch" name="push" checked />
        <span class="klods-switch__track">
        </span>
        <span class="klods-switch__label">
          Push alerts
        </span>
      </label>
    </div>
  </div>
  <div class="klods-card__footer">
    <span class="klods-push">
    </span>
    <button type="button" class="klods-button klods-button--primary">
      Save preferences
    </button>
  </div>
</div>

Search bar example

An input paired with a button in a row.

TypeScript
card(
  row({ gap: 2 }, [
    input({
      type: "search",
      placeholder: "Search for anything…",
      "aria-label": "Search",
    }),
    button({ variant: "primary" }, "Search"),
  ])
);
Ruby
card(
  row({ gap: 2 }, [
    input({
      type: "search",
      placeholder: "Search for anything…",
      "aria-label": "Search",
    }),
    button({ variant: "primary" }, "Search"),
  ])
)
HTML
<div class="klods-card">
  <div class="klods-row klods-row--gap-2">
    <input type="search" placeholder="Search for anything…" aria-label="Search" id="klods-input-search" class="klods-input" />
    <button type="button" class="klods-button klods-button--primary">
      Search
    </button>
  </div>
</div>

List

  • Apples
  • Bananas
  • Cherries
TypeScript
list([
  listItem("Apples"),
  listItem("Bananas"),
  listItem("Cherries"),
]);
Ruby
list([
  list_item("Apples"),
  list_item("Bananas"),
  list_item("Cherries"),
])
HTML
<ul class="klods-list">
  <li class="klods-list__item">
    Apples
  </li>
  <li class="klods-list__item">
    Bananas
  </li>
  <li class="klods-list__item">
    Cherries
  </li>
</ul>

List — leading slot

  • Account verified
  • ⚠️Payment required
  • Profile incomplete
TypeScript
list([
  listItem({ lead: "✅" }, "Account verified"),
  listItem({ lead: "⚠️" }, "Payment required"),
  listItem({ lead: "❌" }, "Profile incomplete"),
]);
Ruby
list([
  list_item({ lead: "✅" }, "Account verified"),
  list_item({ lead: "⚠️" }, "Payment required"),
  list_item({ lead: "❌" }, "Profile incomplete"),
])
HTML
<ul class="klods-list">
  <li class="klods-list__item">
    <span class="klods-list__lead"></span>
    <span class="klods-list__content">
      Account verified
    </span>
  </li>
  <li class="klods-list__item">
    <span class="klods-list__lead">
      ⚠️
    </span>
    <span class="klods-list__content">
      Payment required
    </span>
  </li>
  <li class="klods-list__item">
    <span class="klods-list__lead"></span>
    <span class="klods-list__content">
      Profile incomplete
    </span>
  </li>
</ul>

List — trailing slot

  • MessagesNew
  • Notifications12
  • Updates3
TypeScript
list([
  listItem(
    { trail: badge({ variant: "accent" }, "New") },
    "Messages"
  ),
  listItem({ trail: badge("12") }, "Notifications"),
  listItem({ trail: badge("3") }, "Updates"),
]);
Ruby
list([
  list_item(
    { trail: badge({ variant: "accent" }, "New") },
    "Messages"
  ),
  list_item({ trail: badge("12") }, "Notifications"),
  list_item({ trail: badge("3") }, "Updates"),
])
HTML
<ul class="klods-list">
  <li class="klods-list__item">
    <span class="klods-list__content">
      Messages
    </span>
    <span class="klods-list__trail">
      <span class="klods-badge klods-badge--accent">
        New
      </span>
    </span>
  </li>
  <li class="klods-list__item">
    <span class="klods-list__content">
      Notifications
    </span>
    <span class="klods-list__trail">
      <span class="klods-badge">
        12
      </span>
    </span>
  </li>
  <li class="klods-list__item">
    <span class="klods-list__content">
      Updates
    </span>
    <span class="klods-list__trail">
      <span class="klods-badge">
        3
      </span>
    </span>
  </li>
</ul>

List — lead and trail

  • JSklods-jsStable
  • CSSklods-cssStable
  • Docsdocs siteBeta
TypeScript
list([
  listItem(
    {
      lead: badge({ variant: "accent" }, "JS"),
      trail: badge({ variant: "success" }, "Stable"),
    },
    "klods-js"
  ),
  listItem(
    {
      lead: badge({ variant: "accent" }, "CSS"),
      trail: badge({ variant: "success" }, "Stable"),
    },
    "klods-css"
  ),
  listItem(
    {
      lead: badge("Docs"),
      trail: badge({ variant: "danger" }, "Beta"),
    },
    "docs site"
  ),
]);
Ruby
list([
  list_item(
    {
      lead: badge({ variant: "accent" }, "JS"),
      trail: badge({ variant: "success" }, "Stable"),
    },
    "klods-js"
  ),
  list_item(
    {
      lead: badge({ variant: "accent" }, "CSS"),
      trail: badge({ variant: "success" }, "Stable"),
    },
    "klods-css"
  ),
  list_item(
    {
      lead: badge("Docs"),
      trail: badge({ variant: "danger" }, "Beta"),
    },
    "docs site"
  ),
])
HTML
<ul class="klods-list">
  <li class="klods-list__item">
    <span class="klods-list__lead">
      <span class="klods-badge klods-badge--accent">
        JS
      </span>
    </span>
    <span class="klods-list__content">
      klods-js
    </span>
    <span class="klods-list__trail">
      <span class="klods-badge klods-badge--success">
        Stable
      </span>
    </span>
  </li>
  <li class="klods-list__item">
    <span class="klods-list__lead">
      <span class="klods-badge klods-badge--accent">
        CSS
      </span>
    </span>
    <span class="klods-list__content">
      klods-css
    </span>
    <span class="klods-list__trail">
      <span class="klods-badge klods-badge--success">
        Stable
      </span>
    </span>
  </li>
  <li class="klods-list__item">
    <span class="klods-list__lead">
      <span class="klods-badge">
        Docs
      </span>
    </span>
    <span class="klods-list__content">
      docs site
    </span>
    <span class="klods-list__trail">
      <span class="klods-badge klods-badge--danger">
        Beta
      </span>
    </span>
  </li>
</ul>

List — navigation

TypeScript
list([
  listItem(
    { href: "#", lead: "📨", trail: chevRightIcon() },
    "Messages"
  ),
  listItem(
    { href: "#", lead: "🔔", trail: chevRightIcon() },
    "Notifications"
  ),
  listItem(
    { href: "#", lead: "⚙️", trail: chevRightIcon() },
    "Settings"
  ),
]);
Ruby
list([
  list_item(
    { href: "#", lead: "📨", trail: chev_right_icon() },
    "Messages"
  ),
  list_item(
    { href: "#", lead: "🔔", trail: chev_right_icon() },
    "Notifications"
  ),
  list_item(
    { href: "#", lead: "⚙️", trail: chev_right_icon() },
    "Settings"
  ),
])
HTML
<ul class="klods-list">
  <li class="klods-list__item klods-list__item--link">
    <a href="#" class="klods-list__link">
      <span class="klods-list__lead">
        📨
      </span>
      <span class="klods-list__content">
        Messages
      </span>
      <span class="klods-list__trail">
        <span aria-hidden="true" class="klods-icon">
          <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M5 2l6 6-6 6"/>
          </svg>
        </span>
      </span>
    </a>
  </li>
  <li class="klods-list__item klods-list__item--link">
    <a href="#" class="klods-list__link">
      <span class="klods-list__lead">
        🔔
      </span>
      <span class="klods-list__content">
        Notifications
      </span>
      <span class="klods-list__trail">
        <span aria-hidden="true" class="klods-icon">
          <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M5 2l6 6-6 6"/>
          </svg>
        </span>
      </span>
    </a>
  </li>
  <li class="klods-list__item klods-list__item--link">
    <a href="#" class="klods-list__link">
      <span class="klods-list__lead">
        ⚙️
      </span>
      <span class="klods-list__content">
        Settings
      </span>
      <span class="klods-list__trail">
        <span aria-hidden="true" class="klods-icon">
          <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M5 2l6 6-6 6"/>
          </svg>
        </span>
      </span>
    </a>
  </li>
</ul>

List — flush

Shopping list

  • Milk
  • Eggs
  • Bread
TypeScript
card([
  cardTitle("Shopping list"),
  list({ flush: true }, [
    listItem("Milk"),
    listItem("Eggs"),
    listItem("Bread"),
  ]),
]);
Ruby
card([
  card_title("Shopping list"),
  list({ flush: true }, [
    list_item("Milk"),
    list_item("Eggs"),
    list_item("Bread"),
  ]),
])
HTML
<div class="klods-card">
  <h3 class="klods-card__title">
    Shopping list
  </h3>
  <ul class="klods-list klods-list--flush">
    <li class="klods-list__item">
      Milk
    </li>
    <li class="klods-list__item">
      Eggs
    </li>
    <li class="klods-list__item">
      Bread
    </li>
  </ul>
</div>

Prose

A heading inside prose

Use klods-prose to give a block of HTML content sensible spacing, heading rhythm, and styled inline code.

Subsequent paragraphs get automatic top margin via the adjacent-sibling selector.

<div class="klods-prose">…</div>
TypeScript
prose([
  h2("A heading inside prose"),
  p([
    "Use ",
    code("klods-prose"),
    " to give a block of HTML content sensible spacing, heading rhythm, and styled inline code.",
  ]),
  p(
    "Subsequent paragraphs get automatic top margin via the adjacent-sibling selector."
  ),
  pre(code('<div class="klods-prose">…</div>')),
]);
Ruby
prose([
  h2("A heading inside prose"),
  p([
    "Use ",
    code("klods-prose"),
    " to give a block of HTML content sensible spacing, heading rhythm, and styled inline code.",
  ]),
  p(
    "Subsequent paragraphs get automatic top margin via the adjacent-sibling selector."
  ),
  pre(code('<div class="klods-prose">…</div>')),
])
HTML
<div class="klods-prose">
  <h2>
    A heading inside prose
  </h2>
  <p>
    Use
    <code>
      klods-prose
    </code>
    to give a block of HTML content sensible spacing, heading rhythm, and styled inline code.
  </p>
  <p>
    Subsequent paragraphs get automatic top margin via the adjacent-sibling selector.
  </p>
  <pre>
    <code>
      &lt;div class=&quot;klods-prose&quot;&gt;&lt;/div&gt;
    </code>
  </pre>
</div>

Lead

A lead paragraph introduces a section with slightly larger, muted text.

TypeScript
lead(
  "A lead paragraph introduces a section with slightly larger, muted text."
);
Ruby
lead(
  "A lead paragraph introduces a section with slightly larger, muted text."
)
HTML
<p class="klods-lead">
  A lead paragraph introduces a section with slightly larger, muted text.
</p>

Muted

Regular text followed by muted text for secondary information.

TypeScript
p([
  "Regular text followed by ",
  muted("muted text"),
  " for secondary information.",
]);
Ruby
p([
  "Regular text followed by ",
  muted("muted text"),
  " for secondary information.",
])
HTML
<p>
  Regular text followed by
  <span class="klods-muted">
    muted text
  </span>
  for secondary information.
</p>

Text center

This text is centred using the text-center utility.

TypeScript
textCenter([
  p("This text is centred using the text-center utility."),
]);
Ruby
text_center([
  p("This text is centred using the text-center utility."),
])
HTML
<div class="klods-text-center">
  <p>
    This text is centred using the text-center utility.
  </p>
</div>

Table

NameRoleStatus
AliceEngineeractive
BobDesigneraway
CarolManagerinactive
TypeScript
table([
  thead(tr([th("Name"), th("Role"), th("Status")])),
  tbody([
    tr([
      td("Alice"),
      td("Engineer"),
      td(badge({ variant: "success" }, "active")),
    ]),
    tr([
      td("Bob"),
      td("Designer"),
      td(badge({ variant: "accent" }, "away")),
    ]),
    tr([
      td("Carol"),
      td("Manager"),
      td(badge({ variant: "danger" }, "inactive")),
    ]),
  ]),
]);
Ruby
table([
  thead(tr([th("Name"), th("Role"), th("Status")])),
  tbody([
    tr([
      td("Alice"),
      td("Engineer"),
      td(badge({ variant: "success" }, "active")),
    ]),
    tr([
      td("Bob"),
      td("Designer"),
      td(badge({ variant: "accent" }, "away")),
    ]),
    tr([
      td("Carol"),
      td("Manager"),
      td(badge({ variant: "danger" }, "inactive")),
    ]),
  ]),
])
HTML
<table class="klods-table">
  <thead>
    <tr>
      <th>
        Name
      </th>
      <th>
        Role
      </th>
      <th>
        Status
      </th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>
        Alice
      </td>
      <td>
        Engineer
      </td>
      <td>
        <span class="klods-badge klods-badge--success">
          active
        </span>
      </td>
    </tr>
    <tr>
      <td>
        Bob
      </td>
      <td>
        Designer
      </td>
      <td>
        <span class="klods-badge klods-badge--accent">
          away
        </span>
      </td>
    </tr>
    <tr>
      <td>
        Carol
      </td>
      <td>
        Manager
      </td>
      <td>
        <span class="klods-badge klods-badge--danger">
          inactive
        </span>
      </td>
    </tr>
  </tbody>
</table>

Table — striped

CountryCapitalPopulation
DenmarkCopenhagen5.9M
SwedenStockholm10.5M
NorwayOslo5.5M
FinlandHelsinki5.6M
TypeScript
table({ striped: true }, [
  thead(tr([th("Country"), th("Capital"), th("Population")])),
  tbody([
    tr([td("Denmark"), td("Copenhagen"), td("5.9M")]),
    tr([td("Sweden"), td("Stockholm"), td("10.5M")]),
    tr([td("Norway"), td("Oslo"), td("5.5M")]),
    tr([td("Finland"), td("Helsinki"), td("5.6M")]),
  ]),
]);
Ruby
table({ striped: true }, [
  thead(tr([th("Country"), th("Capital"), th("Population")])),
  tbody([
    tr([td("Denmark"), td("Copenhagen"), td("5.9M")]),
    tr([td("Sweden"), td("Stockholm"), td("10.5M")]),
    tr([td("Norway"), td("Oslo"), td("5.5M")]),
    tr([td("Finland"), td("Helsinki"), td("5.6M")]),
  ]),
])
HTML
<table class="klods-table klods-table--striped">
  <thead>
    <tr>
      <th>
        Country
      </th>
      <th>
        Capital
      </th>
      <th>
        Population
      </th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>
        Denmark
      </td>
      <td>
        Copenhagen
      </td>
      <td>
        5.9M
      </td>
    </tr>
    <tr>
      <td>
        Sweden
      </td>
      <td>
        Stockholm
      </td>
      <td>
        10.5M
      </td>
    </tr>
    <tr>
      <td>
        Norway
      </td>
      <td>
        Oslo
      </td>
      <td>
        5.5M
      </td>
    </tr>
    <tr>
      <td>
        Finland
      </td>
      <td>
        Helsinki
      </td>
      <td>
        5.6M
      </td>
    </tr>
  </tbody>
</table>

Table — scroll wrapper

Wrap any table in tableWrap() so it scrolls horizontally on small screens instead of overflowing.

NameRoleDepartmentLocationStart dateContractSalaryStatus
AliceEngineerPlatformCopenhagenJan 2022Full-time£72,000active
BobDesignerProductStockholmMar 2023Part-time£48,000away
CarolManagerEngineeringOsloAug 2019Full-time£95,000inactive
TypeScript
tableWrap([
  table({ style: "min-width: 52rem" }, [
    thead(
      tr([
        th("Name"),
        th("Role"),
        th("Department"),
        th("Location"),
        th("Start date"),
        th("Contract"),
        th("Salary"),
        th("Status"),
      ])
    ),
    tbody([
      tr([
        td("Alice"),
        td("Engineer"),
        td("Platform"),
        td("Copenhagen"),
        td("Jan 2022"),
        td("Full-time"),
        td("£72,000"),
        td(badge({ variant: "success" }, "active")),
      ]),
      tr([
        td("Bob"),
        td("Designer"),
        td("Product"),
        td("Stockholm"),
        td("Mar 2023"),
        td("Part-time"),
        td("£48,000"),
        td(badge({ variant: "accent" }, "away")),
      ]),
      tr([
        td("Carol"),
        td("Manager"),
        td("Engineering"),
        td("Oslo"),
        td("Aug 2019"),
        td("Full-time"),
        td("£95,000"),
        td(badge({ variant: "danger" }, "inactive")),
      ]),
    ]),
  ]),
]);
Ruby
table_wrap([
  table({ style: "min-width: 52rem" }, [
    thead(
      tr([
        th("Name"),
        th("Role"),
        th("Department"),
        th("Location"),
        th("Start date"),
        th("Contract"),
        th("Salary"),
        th("Status"),
      ])
    ),
    tbody([
      tr([
        td("Alice"),
        td("Engineer"),
        td("Platform"),
        td("Copenhagen"),
        td("Jan 2022"),
        td("Full-time"),
        td("£72,000"),
        td(badge({ variant: "success" }, "active")),
      ]),
      tr([
        td("Bob"),
        td("Designer"),
        td("Product"),
        td("Stockholm"),
        td("Mar 2023"),
        td("Part-time"),
        td("£48,000"),
        td(badge({ variant: "accent" }, "away")),
      ]),
      tr([
        td("Carol"),
        td("Manager"),
        td("Engineering"),
        td("Oslo"),
        td("Aug 2019"),
        td("Full-time"),
        td("£95,000"),
        td(badge({ variant: "danger" }, "inactive")),
      ]),
    ]),
  ]),
])
HTML
<div class="klods-table-wrap">
  <table style="min-width: 52rem" class="klods-table">
    <thead>
      <tr>
        <th>
          Name
        </th>
        <th>
          Role
        </th>
        <th>
          Department
        </th>
        <th>
          Location
        </th>
        <th>
          Start date
        </th>
        <th>
          Contract
        </th>
        <th>
          Salary
        </th>
        <th>
          Status
        </th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>
          Alice
        </td>
        <td>
          Engineer
        </td>
        <td>
          Platform
        </td>
        <td>
          Copenhagen
        </td>
        <td>
          Jan 2022
        </td>
        <td>
          Full-time
        </td>
        <td>
          £72,000
        </td>
        <td>
          <span class="klods-badge klods-badge--success">
            active
          </span>
        </td>
      </tr>
      <tr>
        <td>
          Bob
        </td>
        <td>
          Designer
        </td>
        <td>
          Product
        </td>
        <td>
          Stockholm
        </td>
        <td>
          Mar 2023
        </td>
        <td>
          Part-time
        </td>
        <td>
          £48,000
        </td>
        <td>
          <span class="klods-badge klods-badge--accent">
            away
          </span>
        </td>
      </tr>
      <tr>
        <td>
          Carol
        </td>
        <td>
          Manager
        </td>
        <td>
          Engineering
        </td>
        <td>
          Oslo
        </td>
        <td>
          Aug 2019
        </td>
        <td>
          Full-time
        </td>
        <td>
          £95,000
        </td>
        <td>
          <span class="klods-badge klods-badge--danger">
            inactive
          </span>
        </td>
      </tr>
    </tbody>
  </table>
</div>

Tabs

Manage your account settings and preferences.

Profile information goes here.
TypeScript
tabs([
  tabPanel(
    { label: "Account" },
    stack({ gap: 3 }, [
      p("Manage your account settings and preferences."),
      box("Profile information goes here."),
    ])
  ),
  tabPanel(
    { label: "Security" },
    stack({ gap: 3 }, [
      p("Update your password and security settings."),
      switchInput({
        label: "Two-factor authentication",
        name: "2fa",
      }),
    ])
  ),
  tabPanel(
    { label: "Notifications" },
    p("Configure how and when you receive notifications.")
  ),
]);
Ruby
tabs([
  tab_panel(
    { label: "Account" },
    stack({ gap: 3 }, [
      p("Manage your account settings and preferences."),
      box("Profile information goes here."),
    ])
  ),
  tab_panel(
    { label: "Security" },
    stack({ gap: 3 }, [
      p("Update your password and security settings."),
      switch_input({
        label: "Two-factor authentication",
        name: "2fa",
      }),
    ])
  ),
  tab_panel(
    { label: "Notifications" },
    p("Configure how and when you receive notifications.")
  ),
])
HTML
<div class="klods-tabs">
  <div class="klods-tabs__list" role="tablist">
    <button type="button" role="tab" id="klods-tabs-tab-account-1" aria-selected="true" aria-controls="klods-tabs-panel-account-1" class="klods-tabs__tab klods-tabs__tab--active">
      Account
    </button>
    <button type="button" role="tab" id="klods-tabs-tab-security-2" aria-selected="false" aria-controls="klods-tabs-panel-security-2" class="klods-tabs__tab" tabindex="-1">
      Security
    </button>
    <button type="button" role="tab" id="klods-tabs-tab-notifications-3" aria-selected="false" aria-controls="klods-tabs-panel-notifications-3" class="klods-tabs__tab" tabindex="-1">
      Notifications
    </button>
  </div>
  <div class="klods-tabs__panel" role="tabpanel" id="klods-tabs-panel-account-1" aria-labelledby="klods-tabs-tab-account-1">
    <div class="klods-stack klods-stack--gap-3">
      <p>
        Manage your account settings and preferences.
      </p>
      <div class="klods-box">
        Profile information goes here.
      </div>
    </div>
  </div>
  <div class="klods-tabs__panel" role="tabpanel" id="klods-tabs-panel-security-2" aria-labelledby="klods-tabs-tab-security-2" hidden>
    <div class="klods-stack klods-stack--gap-3">
      <p>
        Update your password and security settings.
      </p>
      <label class="klods-switch">
        <input type="checkbox" class="klods-switch__input" role="switch" name="2fa" />
        <span class="klods-switch__track">
        </span>
        <span class="klods-switch__label">
          Two-factor authentication
        </span>
      </label>
    </div>
  </div>
  <div class="klods-tabs__panel" role="tabpanel" id="klods-tabs-panel-notifications-3" aria-labelledby="klods-tabs-tab-notifications-3" hidden>
    <p>
      Configure how and when you receive notifications.
    </p>
  </div>
</div>

Toast

Use `toastTrigger(props, label)` to render a button that shows a transient notification. Toasts auto-dismiss after 5s and can be closed early with the × button.

TypeScript
cluster({ gap: 2 }, [
  toastTrigger(
    { message: "File saved successfully." },
    "Default"
  ),
  toastTrigger(
    {
      message: "Your changes have been saved.",
      toastVariant: "success",
    },
    "Success"
  ),
  toastTrigger(
    {
      message: "Your session expires in 5 minutes.",
      toastVariant: "warning",
    },
    "Warning"
  ),
  toastTrigger(
    {
      message: "Something went wrong. Please try again.",
      toastVariant: "danger",
      variant: "danger",
    },
    "Danger"
  ),
  toastTrigger(
    {
      message: "You have a new message from Alex.",
      toastVariant: "info",
    },
    "Info"
  ),
]);
Ruby
cluster({ gap: 2 }, [
  toast_trigger(
    { message: "File saved successfully." },
    "Default"
  ),
  toast_trigger(
    {
      message: "Your changes have been saved.",
      toast_variant: "success",
    },
    "Success"
  ),
  toast_trigger(
    {
      message: "Your session expires in 5 minutes.",
      toast_variant: "warning",
    },
    "Warning"
  ),
  toast_trigger(
    {
      message: "Something went wrong. Please try again.",
      toast_variant: "danger",
      variant: "danger",
    },
    "Danger"
  ),
  toast_trigger(
    {
      message: "You have a new message from Alex.",
      toast_variant: "info",
    },
    "Info"
  ),
])
HTML
<div class="klods-cluster klods-cluster--gap-2">
  <button type="button" class="klods-button">
    Default
  </button>
  <button type="button" class="klods-button">
    Success
  </button>
  <button type="button" class="klods-button">
    Warning
  </button>
  <button type="button" class="klods-button klods-button--danger">
    Danger
  </button>
  <button type="button" class="klods-button">
    Info
  </button>
</div>

Toast — persistent

Pass `duration: 0` to keep the toast visible until the user manually dismisses it.

TypeScript
toastTrigger(
  {
    message:
      "Action required: please review the pending changes.",
    toastVariant: "warning",
    duration: 0,
  },
  "Show persistent toast"
);
Ruby
toast_trigger(
  {
    message:
      "Action required: please review the pending changes.",
    toast_variant: "warning",
    duration: 0,
  },
  "Show persistent toast"
)
HTML
<button type="button" class="klods-button">
  Show persistent toast
</button>

Toast — clear toasts

Use `clearToastsTrigger` to render a button that dismisses all visible toasts at once.

TypeScript
clearToastsTrigger("Clear toasts");
Ruby
clear_toasts_trigger("Clear toasts")
HTML
<button type="button" class="klods-button">
  Clear toasts
</button>

Toast — programmatic

Call `showToast()` directly from any event handler, promise callback, or framework action — no pre-wired button needed. Not supported in klods-ruby (server-rendered pages cannot call client-side JS functions directly; use `toast_trigger` instead).

TypeScript
cluster({ gap: 2 }, [
  button(
    {
      onClick: () =>
        showToast(
          { variant: "info" },
          "Triggered from a callback."
        ),
    },
    "Show toast"
  ),
  button(
    {
      onClick: () =>
        showToast(
          { variant: "warning", duration: 0 },
          "This one stays until dismissed."
        ),
    },
    "Persistent toast"
  ),
  button({ onClick: () => clearToasts() }, "Clear toasts"),
]);
HTML
<div class="klods-cluster klods-cluster--gap-2">
  <button type="button" class="klods-button">
    Show toast
  </button>
  <button type="button" class="klods-button">
    Persistent toast
  </button>
  <button type="button" class="klods-button">
    Clear toasts
  </button>
</div>

Table of contents - toc

TypeScript
toc([
  tocItem(tocLink({ href: "#" }, "Introduction")),
  tocItem(tocLink({ href: "#" }, "Getting started")),
  tocItem(tocLink({ href: "#" }, "Components")),
  tocItem(tocLink({ href: "#" }, "Layout")),
  tocItem(tocLink({ href: "#" }, "Utilities")),
]);
Ruby
toc([
  toc_item(toc_link({ href: "#" }, "Introduction")),
  toc_item(toc_link({ href: "#" }, "Getting started")),
  toc_item(toc_link({ href: "#" }, "Components")),
  toc_item(toc_link({ href: "#" }, "Layout")),
  toc_item(toc_link({ href: "#" }, "Utilities")),
])
HTML
<ul class="klods-toc">
  <li>
    <a href="#" class="klods-toc__link">
      Introduction
    </a>
  </li>
  <li>
    <a href="#" class="klods-toc__link">
      Getting started
    </a>
  </li>
  <li>
    <a href="#" class="klods-toc__link">
      Components
    </a>
  </li>
  <li>
    <a href="#" class="klods-toc__link">
      Layout
    </a>
  </li>
  <li>
    <a href="#" class="klods-toc__link">
      Utilities
    </a>
  </li>
</ul>

Toc with sub-items

TypeScript
toc([
  tocItem(tocLink({ href: "#" }, "Components")),
  tocItem([
    tocLink({ href: "#" }, "Layout"),
    toc({ sub: true }, [
      tocItem(tocLink({ href: "#" }, "Page")),
      tocItem(tocLink({ href: "#" }, "Sidebar")),
      tocItem(tocLink({ href: "#" }, "Header")),
    ]),
  ]),
  tocItem(tocLink({ href: "#" }, "Utilities")),
]);
Ruby
toc([
  toc_item(toc_link({ href: "#" }, "Components")),
  toc_item([
    toc_link({ href: "#" }, "Layout"),
    toc({ sub: true }, [
      toc_item(toc_link({ href: "#" }, "Page")),
      toc_item(toc_link({ href: "#" }, "Sidebar")),
      toc_item(toc_link({ href: "#" }, "Header")),
    ]),
  ]),
  toc_item(toc_link({ href: "#" }, "Utilities")),
])
HTML
<ul class="klods-toc">
  <li>
    <a href="#" class="klods-toc__link">
      Components
    </a>
  </li>
  <li>
    <a href="#" class="klods-toc__link">
      Layout
    </a>
    <ul class="klods-toc klods-toc--sub">
      <li>
        <a href="#" class="klods-toc__link">
          Page
        </a>
      </li>
      <li>
        <a href="#" class="klods-toc__link">
          Sidebar
        </a>
      </li>
      <li>
        <a href="#" class="klods-toc__link">
          Header
        </a>
      </li>
    </ul>
  </li>
  <li>
    <a href="#" class="klods-toc__link">
      Utilities
    </a>
  </li>
</ul>

Toc — active sub-item

TypeScript
toc([
  tocItem(tocLink({ href: "#" }, "Components")),
  tocItem([
    tocLink({ href: "#" }, "Layout"),
    toc({ sub: true }, [
      tocItem(tocLink({ href: "#" }, "Page")),
      tocItem(tocLink({ href: "#", active: true }, "Sidebar")),
      tocItem(tocLink({ href: "#" }, "Header")),
    ]),
  ]),
  tocItem(tocLink({ href: "#" }, "Utilities")),
]);
Ruby
toc([
  toc_item(toc_link({ href: "#" }, "Components")),
  toc_item([
    toc_link({ href: "#" }, "Layout"),
    toc({ sub: true }, [
      toc_item(toc_link({ href: "#" }, "Page")),
      toc_item(toc_link({ href: "#", active: true }, "Sidebar")),
      toc_item(toc_link({ href: "#" }, "Header")),
    ]),
  ]),
  toc_item(toc_link({ href: "#" }, "Utilities")),
])
HTML
<ul class="klods-toc">
  <li>
    <a href="#" class="klods-toc__link">
      Components
    </a>
  </li>
  <li>
    <a href="#" class="klods-toc__link">
      Layout
    </a>
    <ul class="klods-toc klods-toc--sub">
      <li>
        <a href="#" class="klods-toc__link">
          Page
        </a>
      </li>
      <li>
        <a href="#" class="klods-toc__link klods-toc__link--active">
          Sidebar
        </a>
      </li>
      <li>
        <a href="#" class="klods-toc__link">
          Header
        </a>
      </li>
    </ul>
  </li>
  <li>
    <a href="#" class="klods-toc__link">
      Utilities
    </a>
  </li>
</ul>

Tooltip

Wrap any inline content with `tooltip({ tip })` to show a tip bubble on hover and keyboard focus. Uses absolute positioning; visibility is toggled with `data-open`.

Saved to your accountLearn moreOpens in a new tab
TypeScript
cluster({ gap: 4 }, [
  tooltip({ tip: "Saved to your account" }, button("Save")),
  tooltip(
    { tip: "Opens in a new tab" },
    a({ href: "#" }, "Learn more")
  ),
]);
Ruby
cluster({ gap: 4 }, [
  tooltip({ tip: "Saved to your account" }, button("Save")),
  tooltip(
    { tip: "Opens in a new tab" },
    a({ href: "#" }, "Learn more")
  ),
])
HTML
<div class="klods-cluster klods-cluster--gap-4">
  <span class="klods-tooltip">
    <button type="button" class="klods-button" aria-describedby="klods-tip-1">
      Save
    </button>
    <span id="klods-tip-1" role="tooltip" class="klods-tooltip__tip klods-tooltip__tip--above">
      Saved to your account
    </span>
  </span>
  <span class="klods-tooltip">
    <a href="#" aria-describedby="klods-tip-2">
      Learn more
    </a>
    <span id="klods-tip-2" role="tooltip" class="klods-tooltip__tip klods-tooltip__tip--above">
      Opens in a new tab
    </span>
  </span>
</div>

Tooltip — placement

Set `position` to `"above"` (default), `"below"`, `"start"`, or `"end"` to control which side the tip appears on.

Above (default)BelowTo the startTo the end
TypeScript
cluster({ gap: 4 }, [
  tooltip(
    { tip: "Above (default)", position: "above" },
    button("Above")
  ),
  tooltip({ tip: "Below", position: "below" }, button("Below")),
  tooltip(
    { tip: "To the start", position: "start" },
    button("Start")
  ),
  tooltip({ tip: "To the end", position: "end" }, button("End")),
]);
Ruby
cluster({ gap: 4 }, [
  tooltip(
    { tip: "Above (default)", position: "above" },
    button("Above")
  ),
  tooltip({ tip: "Below", position: "below" }, button("Below")),
  tooltip(
    { tip: "To the start", position: "start" },
    button("Start")
  ),
  tooltip({ tip: "To the end", position: "end" }, button("End")),
])
HTML
<div class="klods-cluster klods-cluster--gap-4">
  <span class="klods-tooltip">
    <button type="button" class="klods-button" aria-describedby="klods-tip-3">
      Above
    </button>
    <span id="klods-tip-3" role="tooltip" class="klods-tooltip__tip klods-tooltip__tip--above">
      Above (default)
    </span>
  </span>
  <span class="klods-tooltip">
    <button type="button" class="klods-button" aria-describedby="klods-tip-4">
      Below
    </button>
    <span id="klods-tip-4" role="tooltip" class="klods-tooltip__tip klods-tooltip__tip--below">
      Below
    </span>
  </span>
  <span class="klods-tooltip">
    <button type="button" class="klods-button" aria-describedby="klods-tip-5">
      Start
    </button>
    <span id="klods-tip-5" role="tooltip" class="klods-tooltip__tip klods-tooltip__tip--start">
      To the start
    </span>
  </span>
  <span class="klods-tooltip">
    <button type="button" class="klods-button" aria-describedby="klods-tip-6">
      End
    </button>
    <span id="klods-tip-6" role="tooltip" class="klods-tooltip__tip klods-tooltip__tip--end">
      To the end
    </span>
  </span>
</div>

Tooltip — on plain text

When wrapping non-interactive content, the tooltip wrapper itself becomes focusable so keyboard users can still read the tip.

The slot-machine effectVariable-ratio intermittent reinforcement — a reward schedule that drives compulsive engagement. is widely used in social media design.

TypeScript
p([
  "The ",
  tooltip(
    {
      tip: "Variable-ratio intermittent reinforcement — a reward schedule that drives compulsive engagement.",
    },
    span("slot-machine effect")
  ),
  " is widely used in social media design.",
]);
Ruby
p([
  "The ",
  tooltip(
    {
      tip: "Variable-ratio intermittent reinforcement — a reward schedule that drives compulsive engagement.",
    },
    span("slot-machine effect")
  ),
  " is widely used in social media design.",
])
HTML
<p>
  The
  <span class="klods-tooltip" aria-describedby="klods-tip-7" tabindex="0">
    <span>
      slot-machine effect
    </span>
    <span id="klods-tip-7" role="tooltip" class="klods-tooltip__tip klods-tooltip__tip--above">
      Variable-ratio intermittent reinforcement — a reward schedule that drives compulsive engagement.
    </span>
  </span>
  is widely used in social media design.
</p>

Tooltip — long content

Tips wrap at `max-width: 20rem`. Useful for longer explanatory text.

This action permanently removes the file from all connected devices and cannot be undone. Make sure you have a backup before continuing.
TypeScript
stack({ gap: 3 }, [
  tooltip(
    {
      tip: "This action permanently removes the file from all connected devices and cannot be undone. Make sure you have a backup before continuing.",
    },
    button({ variant: "danger" }, "Delete file")
  ),
]);
Ruby
stack({ gap: 3 }, [
  tooltip(
    {
      tip: "This action permanently removes the file from all connected devices and cannot be undone. Make sure you have a backup before continuing.",
    },
    button({ variant: "danger" }, "Delete file")
  ),
])
HTML
<div class="klods-stack klods-stack--gap-3">
  <span class="klods-tooltip">
    <button type="button" class="klods-button klods-button--danger" aria-describedby="klods-tip-8">
      Delete file
    </button>
    <span id="klods-tip-8" role="tooltip" class="klods-tooltip__tip klods-tooltip__tip--above">
      This action permanently removes the file from all connected devices and cannot be undone. Make sure you have a backup before continuing.
    </span>
  </span>
</div>

Icons

Built-in SVG icons that inherit color from context via currentColor.

Built-in icons

checkCircleIcon
chevDownIcon
chevLeftIcon
chevRightIcon
chevUpIcon
closeIcon
copyIcon
dangerCircleIcon
editIcon
externalLinkIcon
eyeIcon
eyeOffIcon
infoCircleIcon
menuIcon
plusIcon
searchIcon
trashIcon
userIcon
warningIcon
TypeScript
cluster({ gap: 5 }, [
  stack({ gap: 2, inline: true }, [
    checkCircleIcon(),
    code("checkCircleIcon"),
  ]),
  stack({ gap: 2, inline: true }, [
    chevDownIcon(),
    code("chevDownIcon"),
  ]),
  stack({ gap: 2, inline: true }, [
    chevLeftIcon(),
    code("chevLeftIcon"),
  ]),
  stack({ gap: 2, inline: true }, [
    chevRightIcon(),
    code("chevRightIcon"),
  ]),
  stack({ gap: 2, inline: true }, [
    chevUpIcon(),
    code("chevUpIcon"),
  ]),
  stack({ gap: 2, inline: true }, [
    closeIcon(),
    code("closeIcon"),
  ]),
  stack({ gap: 2, inline: true }, [copyIcon(), code("copyIcon")]),
  stack({ gap: 2, inline: true }, [
    dangerCircleIcon(),
    code("dangerCircleIcon"),
  ]),
  stack({ gap: 2, inline: true }, [editIcon(), code("editIcon")]),
  stack({ gap: 2, inline: true }, [
    externalLinkIcon(),
    code("externalLinkIcon"),
  ]),
  stack({ gap: 2, inline: true }, [eyeIcon(), code("eyeIcon")]),
  stack({ gap: 2, inline: true }, [
    eyeOffIcon(),
    code("eyeOffIcon"),
  ]),
  stack({ gap: 2, inline: true }, [
    infoCircleIcon(),
    code("infoCircleIcon"),
  ]),
  stack({ gap: 2, inline: true }, [menuIcon(), code("menuIcon")]),
  stack({ gap: 2, inline: true }, [plusIcon(), code("plusIcon")]),
  stack({ gap: 2, inline: true }, [
    searchIcon(),
    code("searchIcon"),
  ]),
  stack({ gap: 2, inline: true }, [
    trashIcon(),
    code("trashIcon"),
  ]),
  stack({ gap: 2, inline: true }, [userIcon(), code("userIcon")]),
  stack({ gap: 2, inline: true }, [
    warningIcon(),
    code("warningIcon"),
  ]),
]);
Ruby
cluster({ gap: 5 }, [
  stack({ gap: 2, inline: true }, [
    check_circle_icon(),
    code("checkCircleIcon"),
  ]),
  stack({ gap: 2, inline: true }, [
    chev_down_icon(),
    code("chevDownIcon"),
  ]),
  stack({ gap: 2, inline: true }, [
    chev_left_icon(),
    code("chevLeftIcon"),
  ]),
  stack({ gap: 2, inline: true }, [
    chev_right_icon(),
    code("chevRightIcon"),
  ]),
  stack({ gap: 2, inline: true }, [
    chev_up_icon(),
    code("chevUpIcon"),
  ]),
  stack({ gap: 2, inline: true }, [
    close_icon(),
    code("closeIcon"),
  ]),
  stack({ gap: 2, inline: true }, [copy_icon(), code("copyIcon")]),
  stack({ gap: 2, inline: true }, [
    danger_circle_icon(),
    code("dangerCircleIcon"),
  ]),
  stack({ gap: 2, inline: true }, [edit_icon(), code("editIcon")]),
  stack({ gap: 2, inline: true }, [
    external_link_icon(),
    code("externalLinkIcon"),
  ]),
  stack({ gap: 2, inline: true }, [eye_icon(), code("eyeIcon")]),
  stack({ gap: 2, inline: true }, [
    eye_off_icon(),
    code("eyeOffIcon"),
  ]),
  stack({ gap: 2, inline: true }, [
    info_circle_icon(),
    code("infoCircleIcon"),
  ]),
  stack({ gap: 2, inline: true }, [menu_icon(), code("menuIcon")]),
  stack({ gap: 2, inline: true }, [plus_icon(), code("plusIcon")]),
  stack({ gap: 2, inline: true }, [
    search_icon(),
    code("searchIcon"),
  ]),
  stack({ gap: 2, inline: true }, [
    trash_icon(),
    code("trashIcon"),
  ]),
  stack({ gap: 2, inline: true }, [user_icon(), code("userIcon")]),
  stack({ gap: 2, inline: true }, [
    warning_icon(),
    code("warningIcon"),
  ]),
])
HTML
<div class="klods-cluster klods-cluster--gap-5">
  <div inline class="klods-stack klods-stack--gap-2">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.5"/>
        <path stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" d="M5 8l2 2.5 4-4"/>
      </svg>
    </span>
    <code>
      checkCircleIcon
    </code>
  </div>
  <div inline class="klods-stack klods-stack--gap-2">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M2 5l6 6 6-6"/>
      </svg>
    </span>
    <code>
      chevDownIcon
    </code>
  </div>
  <div inline class="klods-stack klods-stack--gap-2">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M11 2l-6 6 6 6"/>
      </svg>
    </span>
    <code>
      chevLeftIcon
    </code>
  </div>
  <div inline class="klods-stack klods-stack--gap-2">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M5 2l6 6-6 6"/>
      </svg>
    </span>
    <code>
      chevRightIcon
    </code>
  </div>
  <div inline class="klods-stack klods-stack--gap-2">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M2 11l6-6 6 6"/>
      </svg>
    </span>
    <code>
      chevUpIcon
    </code>
  </div>
  <div inline class="klods-stack klods-stack--gap-2">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path stroke="currentColor" stroke-linecap="round" stroke-width="1.75" d="M4 4l8 8M12 4L4 12"/>
      </svg>
    </span>
    <code>
      closeIcon
    </code>
  </div>
  <div inline class="klods-stack klods-stack--gap-2">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <rect x="5" y="5" width="8" height="8" rx="1.5" stroke="currentColor" stroke-width="1.5"/>
        <path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M11 5V3.5A1.5 1.5 0 009.5 2h-6A1.5 1.5 0 002 3.5v6A1.5 1.5 0 003.5 11H5"/>
      </svg>
    </span>
    <code>
      copyIcon
    </code>
  </div>
  <div inline class="klods-stack klods-stack--gap-2">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.5"/>
        <path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" d="M5.5 5.5l5 5M10.5 5.5l-5 5"/>
      </svg>
    </span>
    <code>
      dangerCircleIcon
    </code>
  </div>
  <div inline class="klods-stack klods-stack--gap-2">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M11.3 2a2 2 0 112.8 2.8L5 13.8l-3.5 1 1-3.5L11.3 2z"/>
      </svg>
    </span>
    <code>
      editIcon
    </code>
  </div>
  <div inline class="klods-stack klods-stack--gap-2">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M9 3h4v4M13 3L7 9"/>
        <path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M6 4H4a1 1 0 00-1 1v7a1 1 0 001 1h7a1 1 0 001-1v-2"/>
      </svg>
    </span>
    <code>
      externalLinkIcon
    </code>
  </div>
  <div inline class="klods-stack klods-stack--gap-2">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M1 8s2.5-5 7-5 7 5 7 5-2.5 5-7 5-7-5-7-5z"/>
        <circle cx="8" cy="8" r="2" stroke="currentColor" stroke-width="1.5"/>
      </svg>
    </span>
    <code>
      eyeIcon
    </code>
  </div>
  <div inline class="klods-stack klods-stack--gap-2">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" d="M2 2l12 12"/>
        <path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M4 5.3C2.4 6.5 1 8 1 8s2.5 5 7 5c1.4 0 2.7-.4 3.8-1M9 3.3C11.6 4 14.2 6.2 15 8a12 12 0 01-2 2.7"/>
        <path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" d="M9.8 9.8A2 2 0 016.2 6.2"/>
      </svg>
    </span>
    <code>
      eyeOffIcon
    </code>
  </div>
  <div inline class="klods-stack klods-stack--gap-2">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.5"/>
        <path stroke="currentColor" stroke-width="1" stroke-linecap="round" d="M8 7.5v3.5"/>
        <circle cx="8" cy="5.5" r="0.75" fill="currentColor"/>
      </svg>
    </span>
    <code>
      infoCircleIcon
    </code>
  </div>
  <div inline class="klods-stack klods-stack--gap-2">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
        <rect y="3" width="20" height="2" rx="1" fill="currentColor"/>
        <rect y="9" width="20" height="2" rx="1" fill="currentColor"/>
        <rect y="15" width="20" height="2" rx="1" fill="currentColor"/>
      </svg>
    </span>
    <code>
      menuIcon
    </code>
  </div>
  <div inline class="klods-stack klods-stack--gap-2">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M8 3v10M3 8h10"/>
      </svg>
    </span>
    <code>
      plusIcon
    </code>
  </div>
  <div inline class="klods-stack klods-stack--gap-2">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <circle cx="7" cy="7" r="4.5" stroke="currentColor" stroke-width="1.5"/>
        <path stroke="currentColor" stroke-width="1.75" stroke-linecap="round" d="M10.5 10.5L14 14"/>
      </svg>
    </span>
    <code>
      searchIcon
    </code>
  </div>
  <div inline class="klods-stack klods-stack--gap-2">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M2 5h12M6 5V4a1 1 0 011-1h2a1 1 0 011 1v1"/>
        <path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M4.5 5l.7 7.5a1 1 0 001 .9h3.6a1 1 0 001-.9L11.5 5"/>
      </svg>
    </span>
    <code>
      trashIcon
    </code>
  </div>
  <div inline class="klods-stack klods-stack--gap-2">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <circle cx="8" cy="6" r="2.5" stroke="currentColor" stroke-width="1.5"/>
        <path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" d="M2.5 14c0-2.8 2.5-5 5.5-5s5.5 2.2 5.5 5"/>
      </svg>
    </span>
    <code>
      userIcon
    </code>
  </div>
  <div inline class="klods-stack klods-stack--gap-2">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M8 2L1.5 13.5h13L8 2z"/>
        <path stroke="currentColor" stroke-width="1" stroke-linecap="round" d="M8 7v3"/>
        <circle cx="8" cy="12" r="0.75" fill="currentColor"/>
      </svg>
    </span>
    <code>
      warningIcon
    </code>
  </div>
</div>

Built-in icons — sizes

TypeScript
cluster({ align: "center" }, [
  chevRightIcon({ size: "small" }),
  chevRightIcon({ size: "medium" }),
  chevRightIcon({ size: "large" }),
]);
Ruby
cluster({ align: "center" }, [
  chev_right_icon({ size: "small" }),
  chev_right_icon({ size: "medium" }),
  chev_right_icon({ size: "large" }),
])
HTML
<div align="center" class="klods-cluster">
  <span aria-hidden="true" class="klods-icon">
    <svg width="12" height="12" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M5 2l6 6-6 6"/>
    </svg>
  </span>
  <span aria-hidden="true" class="klods-icon">
    <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M5 2l6 6-6 6"/>
    </svg>
  </span>
  <span aria-hidden="true" class="klods-icon">
    <svg width="32" height="32" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M5 2l6 6-6 6"/>
    </svg>
  </span>
</div>

Built-in icons — color

TypeScript
cluster([
  span(
    { style: "color: var(--klods-color-accent)" },
    chevRightIcon()
  ),
  span(
    { style: "color: var(--klods-color-success)" },
    chevRightIcon()
  ),
  span(
    { style: "color: var(--klods-color-danger)" },
    chevRightIcon()
  ),
  span(
    { style: "color: var(--klods-color-muted)" },
    chevRightIcon()
  ),
]);
Ruby
cluster([
  span(
    { style: "color: var(--klods-color-accent)" },
    chev_right_icon()
  ),
  span(
    { style: "color: var(--klods-color-success)" },
    chev_right_icon()
  ),
  span(
    { style: "color: var(--klods-color-danger)" },
    chev_right_icon()
  ),
  span(
    { style: "color: var(--klods-color-muted)" },
    chev_right_icon()
  ),
])
HTML
<div class="klods-cluster">
  <span style="color: var(--klods-color-accent)">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M5 2l6 6-6 6"/>
      </svg>
    </span>
  </span>
  <span style="color: var(--klods-color-success)">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M5 2l6 6-6 6"/>
      </svg>
    </span>
  </span>
  <span style="color: var(--klods-color-danger)">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M5 2l6 6-6 6"/>
      </svg>
    </span>
  </span>
  <span style="color: var(--klods-color-muted)">
    <span aria-hidden="true" class="klods-icon">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M5 2l6 6-6 6"/>
      </svg>
    </span>
  </span>
</div>

Built-in icons — in context

TypeScript
list([
  listItem(
    { href: "#", lead: userIcon(), trail: chevRightIcon() },
    "My account"
  ),
  listItem(
    { href: "#", lead: searchIcon(), trail: chevRightIcon() },
    "Search"
  ),
  listItem(
    { href: "#", lead: trashIcon(), trail: chevRightIcon() },
    "Trash"
  ),
]);
Ruby
list([
  list_item(
    { href: "#", lead: user_icon(), trail: chev_right_icon() },
    "My account"
  ),
  list_item(
    { href: "#", lead: search_icon(), trail: chev_right_icon() },
    "Search"
  ),
  list_item(
    { href: "#", lead: trash_icon(), trail: chev_right_icon() },
    "Trash"
  ),
])
HTML
<ul class="klods-list">
  <li class="klods-list__item klods-list__item--link">
    <a href="#" class="klods-list__link">
      <span class="klods-list__lead">
        <span aria-hidden="true" class="klods-icon">
          <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
            <circle cx="8" cy="6" r="2.5" stroke="currentColor" stroke-width="1.5"/>
            <path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" d="M2.5 14c0-2.8 2.5-5 5.5-5s5.5 2.2 5.5 5"/>
          </svg>
        </span>
      </span>
      <span class="klods-list__content">
        My account
      </span>
      <span class="klods-list__trail">
        <span aria-hidden="true" class="klods-icon">
          <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M5 2l6 6-6 6"/>
          </svg>
        </span>
      </span>
    </a>
  </li>
  <li class="klods-list__item klods-list__item--link">
    <a href="#" class="klods-list__link">
      <span class="klods-list__lead">
        <span aria-hidden="true" class="klods-icon">
          <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
            <circle cx="7" cy="7" r="4.5" stroke="currentColor" stroke-width="1.5"/>
            <path stroke="currentColor" stroke-width="1.75" stroke-linecap="round" d="M10.5 10.5L14 14"/>
          </svg>
        </span>
      </span>
      <span class="klods-list__content">
        Search
      </span>
      <span class="klods-list__trail">
        <span aria-hidden="true" class="klods-icon">
          <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M5 2l6 6-6 6"/>
          </svg>
        </span>
      </span>
    </a>
  </li>
  <li class="klods-list__item klods-list__item--link">
    <a href="#" class="klods-list__link">
      <span class="klods-list__lead">
        <span aria-hidden="true" class="klods-icon">
          <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M2 5h12M6 5V4a1 1 0 011-1h2a1 1 0 011 1v1"/>
            <path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M4.5 5l.7 7.5a1 1 0 001 .9h3.6a1 1 0 001-.9L11.5 5"/>
          </svg>
        </span>
      </span>
      <span class="klods-list__content">
        Trash
      </span>
      <span class="klods-list__trail">
        <span aria-hidden="true" class="klods-icon">
          <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M5 2l6 6-6 6"/>
          </svg>
        </span>
      </span>
    </a>
  </li>
</ul>

Built-in icons — accessible label

TypeScript
p(chevRightIcon({ label: "Next page" }));
Ruby
p(chev_right_icon({ label: "Next page" }))
HTML
<p>
  <span aria-label="Next page" role="img" class="klods-icon">
    <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M5 2l6 6-6 6"/>
    </svg>
  </span>
</p>

Themes

Switch the theme using the buttons in the header — the whole site re-skins.

Themes are pure CSS-variable overrides keyed off [data-theme]. Set data-theme="dark" on <html> (or any container) and every klods component re-skins.

Bundled presets:

  • default — clean, neutral light theme.
  • dark — high-contrast dark theme.
  • playful — pink and round.
  • brutalist — black borders, hard shadows.

Roll your own by overriding any of the --klods-* tokens. No compile step required.

:root[data-theme="ocean"] {
  --klods-color-bg: #001b29;
  --klods-color-fg: #d8f2ff;
  --klods-color-surface: #002c44;
  --klods-color-surface-2: #003a59;
  --klods-color-border: #0a4f76;
  --klods-color-accent: #6cd2ff;
  --klods-color-accent-fg: #001b29;
}

Tokens cheat-sheet

All tokens live in one place and are intended to be overridden:

--klods-color-bg / -fg / -muted / -surface / -surface-2 / -border
--klods-color-accent / -accent-fg / -link / -danger / -success / -warning / -info
--klods-font-sans / -font-mono / -font-size-base / -sm / -lg / -xl / -2xl / -3xl
--klods-line-height-base / -tight
--klods-space-0 .. --klods-space-8
--klods-radius-sm / -md / -lg / -pill
--klods-shadow-sm / -md / -lg
--klods-content-max / -sidebar-width / -header-height / -gutter
--klods-transition

Per-component theming

Key components expose their own scoped tokens that fall back to global design tokens. Set the scoped token anywhere in the DOM — the custom property inherits down — and only that component type is affected inside that scope.

/* Re-skin cards in one section — global tokens stay untouched */
.my-panel {
  --klods-card-bg: color-mix(in srgb, var(--klods-color-accent) 8%, var(--klods-color-bg));
  --klods-card-border: var(--klods-color-accent);
  --klods-card-radius: var(--klods-radius-lg);
}

/* Pill buttons in one form */
.my-form {
  --klods-button-radius: var(--klods-radius-pill);
}

/* Green tabs in a sidebar widget */
.my-widget {
  --klods-tabs-active: var(--klods-color-success);
}

All scoped tokens and their defaults:

/* Card */
--klods-card-bg:     var(--klods-color-surface)
--klods-card-border: var(--klods-color-border)
--klods-card-radius: var(--klods-radius-md)

/* Button (default variant) */
--klods-button-bg:     var(--klods-color-surface-2)
--klods-button-fg:     var(--klods-color-fg)
--klods-button-border: var(--klods-color-border)
--klods-button-radius: var(--klods-radius-sm)

/* Badge (default variant) */
--klods-badge-bg: var(--klods-color-surface-2)
--klods-badge-fg: var(--klods-color-fg)

/* Modal */
--klods-modal-bg:     var(--klods-color-surface)
--klods-modal-radius: var(--klods-radius-lg)

/* Tabs */
--klods-tabs-active: var(--klods-color-accent)

/* Tooltip */
--klods-tooltip-bg: oklch(20% 0.01 264)
--klods-tooltip-fg: oklch(98% 0 0)

Default card

No overrides — uses --klods-card-bg which defaults to --klods-color-surface.

Scoped overrides

Parent sets --klods-card-bg, --klods-card-border, --klods-card-radius, --klods-button-radius.

Compact density

Set data-density="compact" on <html> (or any container) to tighten all spacing tokens to roughly 75% of their defaults. Useful for data-dense UIs or when fitting more content on screen.

document.documentElement.setAttribute("data-density", "compact")

Default density

New

Standard spacing — --klods-space-4 is 1rem (16px).

Compact density

New

Compact spacing — --klods-space-4 is 0.75rem (12px).

Reduced motion

klods respects the system prefers-reduced-motion preference automatically. When it is set to reduce, the --klods-transition token is set to 0ms, eliminating all transitions and shortening animation durations to zero.

You can simulate this in CSS without changing system settings:

:root { --klods-transition: 0ms; }

Print styles

klods ships @layer klods.print with sensible print defaults. The layer sits at the top of the cascade so it wins cleanly without !important.

  • Hides .klods-sidebar, .klods-sidebar-toggle, modals, toasts, and tooltip tips.
  • Collapses the sidebar grid to a single column.
  • Removes sticky header positioning.
  • Expands all closed disclosure elements so their content prints.
  • Removes box shadows.
  • Adds break-inside: avoid to cards, list items, and description lists.

Theme builder

Edit any token below — changes apply live to this entire page. When you're happy, copy the generated :root { … } block into your own stylesheet. Clearing a text field restores the active theme's value. Switching themes re-syncs the color pickers.

Colors

Radius

Typography

Preview card

Changes apply instantly.

Try editing some of the values above and see the changes here.
Default badgeAccent badgeSuccess badgeDanger badge