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.cssand use the BEM classes (klods-page,klods-stack, …). No JS required. - Typed builders —
import { 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-cssInstall — 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.
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>Sidebar on the trailing side
Use `sidebarPosition: "trailing"` to place the sidebar on the inline-end side.
TypeScript
page({ sidebar: true, sidebarPosition: "trailing" }, [
header("Header"),
sidebar("Sidebar"),
content("Main content"),
footer("Footer"),
]);Ruby
page({ sidebar: true, sidebar_position: "trailing" }, [
header("Header"),
sidebar("Sidebar"),
content("Main content"),
footer("Footer"),
])HTML
<div class="klods-page klods-page--with-sidebar klods-page--sidebar-trailing">
<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.
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>Sticky header
`stickyHeader: true` keeps the header pinned to the top of the viewport as the page scrolls.
TypeScript
page({ stickyHeader: true }, [
header("Sticky header"),
content("Main content"),
footer("Footer"),
]);Ruby
page({ sticky_header: true }, [
header("Sticky header"),
content("Main content"),
footer("Footer"),
])HTML
<div class="klods-page klods-page--sticky-header">
<header class="klods-header">
Sticky header
</header>
<main class="klods-content">
Main content
</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.
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
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.
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
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
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
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
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
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
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
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
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).
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
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.
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
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
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
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>Breadcrumb
TypeScript
breadcrumbs([
crumb({ href: "#" }, "Home"),
crumb({ href: "#" }, "Products"),
crumb("Widget Pro"),
]);Ruby
breadcrumbs([
crumb({ href: "#" }, "Home"),
crumb({ href: "#" }, "Products"),
crumb("Widget Pro"),
])HTML
<nav aria-label="Breadcrumb">
<ol class="klods-breadcrumb__list">
<li class="klods-breadcrumb__item">
<a href="#" class="klods-breadcrumb__link">
Home
</a>
</li>
<li class="klods-breadcrumb__item">
<a href="#" class="klods-breadcrumb__link">
Products
</a>
</li>
<li class="klods-breadcrumb__item" aria-current="page">
Widget Pro
</li>
</ol>
</nav>Breadcrumb — two levels
TypeScript
breadcrumbs([
crumb({ href: "#" }, "Docs"),
crumb("Getting started"),
]);Ruby
breadcrumbs([
crumb({ href: "#" }, "Docs"),
crumb("Getting started"),
])HTML
<nav aria-label="Breadcrumb">
<ol class="klods-breadcrumb__list">
<li class="klods-breadcrumb__item">
<a href="#" class="klods-breadcrumb__link">
Docs
</a>
</li>
<li class="klods-breadcrumb__item" aria-current="page">
Getting started
</li>
</ol>
</nav>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>Button as link
Pass href and the builder renders an <a> instead of a <button> — same classes, valid HTML. All variants and extra attributes still work.
TypeScript
cluster([
button({ href: "#" }, "Default link"),
button({ href: "#", variant: "primary" }, "Primary link"),
button({ href: "#", variant: "ghost" }, "Ghost link"),
]);Ruby
cluster([
button({ href: "#" }, "Default link"),
button({ href: "#", variant: "primary" }, "Primary link"),
button({ href: "#", variant: "ghost" }, "Ghost link"),
])HTML
<div class="klods-cluster">
<a href="#" class="klods-button">
Default link
</a>
<a href="#" class="klods-button klods-button--primary">
Primary link
</a>
<a href="#" class="klods-button klods-button--ghost">
Ghost link
</a>
</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
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
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 "klods-js";
page([
header("Hello"),
content("World"),
]).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 'klods-js'
</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 <details> 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 <details>/<summary> 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,
})
endHTML
<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" })
endHTML
<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'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.
Please enter a valid email address.
Message must be at least 20 characters.
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…" })
endHTML
<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.
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>Link
TypeScript
link({ href: "#" }, "A styled link");Ruby
link({ href: "#" }, "A styled link")HTML
<a href="#" class="klods-link">
A styled link
</a>Link — plain
Inherits color and removes underline. Use for wordmarks, logo links, and nav items where the link context is implied.
TypeScript
link({ href: "#", plain: true }, strong("WatchThis"));Ruby
link({ href: "#", plain: true }, strong("WatchThis"))HTML
<a href="#" class="klods-link klods-link--plain">
<strong>
WatchThis
</strong>
</a>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 — 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>Modal
Built on the native `<dialog>` element. Use `modalTrigger` to open, `modalClose` to dismiss with the × button, and `modalDismiss` for action buttons — all wire up automatically.
TypeScript
div([
modalTrigger({ variant: "primary" }, "Open modal"),
modal(
modalPanel([
modalHeader([modalTitle("Confirm action"), modalClose()]),
modalBody(
"Are you sure you want to continue? This action cannot be undone."
),
modalActions([
modalDismiss({ variant: "primary" }, "Confirm"),
modalDismiss("Cancel"),
]),
])
),
]);Ruby
div([
modal_trigger({ variant: "primary" }, "Open modal"),
modal(
modal_panel([
modal_header([modal_title("Confirm action"), modal_close()]),
modal_body(
"Are you sure you want to continue? This action cannot be undone."
),
modal_actions([
modal_dismiss({ variant: "primary" }, "Confirm"),
modal_dismiss("Cancel"),
]),
])
),
])HTML
<div>
<button type="button" class="klods-button klods-button--primary">
Open modal
</button>
<dialog class="klods-modal">
<div class="klods-modal__panel">
<div class="klods-modal__header">
<h2 class="klods-modal__title">
Confirm action
</h2>
<button type="button" aria-label="Close" class="klods-modal__close">
</button>
</div>
<div class="klods-modal__body">
Are you sure you want to continue? This action cannot be undone.
</div>
<div class="klods-modal__actions">
<button type="button" class="klods-button klods-button--primary">
Confirm
</button>
<button type="button" class="klods-button">
Cancel
</button>
</div>
</div>
</dialog>
</div>Modal — info
A simpler modal with no footer actions — just a dismiss button in the header.
TypeScript
div([
modalTrigger("Show info"),
modal(
modalPanel([
modalHeader([modalTitle("What is klods?"), modalClose()]),
modalBody([
p(
"klods is a tiny, opinionated, fully themeable HTML/CSS/JS component library."
),
p(
"It ships two packages — klods-css for styles and klods-js for TypeScript builders."
),
]),
])
),
]);Ruby
div([
modal_trigger("Show info"),
modal(
modal_panel([
modal_header([modal_title("What is klods?"), modal_close()]),
modal_body([
p(
"klods is a tiny, opinionated, fully themeable HTML/CSS/JS component library."
),
p(
"It ships two packages — klods-css for styles and klods-js for TypeScript builders."
),
]),
])
),
])HTML
<div>
<button type="button" class="klods-button">
Show info
</button>
<dialog class="klods-modal">
<div class="klods-modal__panel">
<div class="klods-modal__header">
<h2 class="klods-modal__title">
What is klods?
</h2>
<button type="button" aria-label="Close" class="klods-modal__close">
</button>
</div>
<div class="klods-modal__body">
<p>
klods is a tiny, opinionated, fully themeable HTML/CSS/JS component library.
</p>
<p>
It ships two packages — klods-css for styles and klods-js for TypeScript builders.
</p>
</div>
</div>
</dialog>
</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>
<div class="klods-prose">…</div>
</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
| Name | Role | Status |
|---|---|---|
| Alice | Engineer | active |
| Bob | Designer | away |
| Carol | Manager | inactive |
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
| Country | Capital | Population |
|---|---|---|
| Denmark | Copenhagen | 5.9M |
| Sweden | Stockholm | 10.5M |
| Norway | Oslo | 5.5M |
| Finland | Helsinki | 5.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.
| Name | Role | Department | Location | Start date | Contract | Salary | Status |
|---|---|---|---|---|---|---|---|
| Alice | Engineer | Platform | Copenhagen | Jan 2022 | Full-time | £72,000 | active |
| Bob | Designer | Product | Stockholm | Mar 2023 | Part-time | £48,000 | away |
| Carol | Manager | Engineering | Oslo | Aug 2019 | Full-time | £95,000 | inactive |
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.
Update your password and security settings.
Configure how and when you receive notifications.
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 — active link
TypeScript
toc([
tocItem(tocLink({ href: "#" }, "Introduction")),
tocItem(
tocLink({ href: "#", active: true }, "Getting started")
),
tocItem(tocLink({ href: "#" }, "API reference")),
]);Ruby
toc([
toc_item(toc_link({ href: "#" }, "Introduction")),
toc_item(
toc_link({ href: "#", active: true }, "Getting started")
),
toc_item(toc_link({ href: "#" }, "API reference")),
])HTML
<ul class="klods-toc">
<li>
<a href="#" class="klods-toc__link">
Introduction
</a>
</li>
<li>
<a href="#" class="klods-toc__link klods-toc__link--active">
Getting started
</a>
</li>
<li>
<a href="#" class="klods-toc__link">
API reference
</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`.
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.
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.
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
checkCircleIconchevDownIconchevLeftIconchevRightIconchevUpIconcloseIconcopyIcondangerCircleIconeditIconexternalLinkIconeyeIconeyeOffIconinfoCircleIconmenuIconplusIconsearchIcontrashIconuserIconwarningIconTypeScript
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-transitionPer-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
Standard spacing — --klods-space-4 is 1rem (16px).
Compact density
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: avoidto 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.