Actions
Buttons and the pieces that join them into controls.
Buttons
.k-btn works on a and
button alike. --ghost is bordered and
transparent, --soft uses the soft accent,
--secondary uses the secondary color,
--danger uses the danger status color,
--small and --large step the padding
and type down or up. --large is for hero CTAs —
the one action a section is built around. --pill
rounds the corners fully into a capsule, and --block
spans the full width of its container, for a stacked mobile CTA or
a form submit. --link
strips the button back to underlined accent text, for when an
action must read as prose while staying a real
button. A disabled
button[disabled] dims and blocks the cursor —
note that only real button elements can be
disabled, not links. Keyboard focus gets its own ring: on
:focus-visible the button swaps its shadow for a
soft accent ring instead of the default outline.
Live demo
Markup
<button class="k-btn">Primary</button>
<button class="k-btn k-btn--ghost">Ghost</button>
<button class="k-btn k-btn--soft">Soft</button>
<button class="k-btn k-btn--secondary">Secondary</button>
<button class="k-btn k-btn--tertiary">Accent</button>
<button class="k-btn k-btn--danger">Danger</button>
<button class="k-btn k-btn--small">Small</button>
<button class="k-btn k-btn--large">Large</button>
<button class="k-btn k-btn--pill">Pill</button>
<button class="k-btn k-btn--link">Link</button>
<button class="k-btn" disabled>Disabled</button>
<!-- links take the same classes -->
<a class="k-btn" href="…">Get started</a>
--block is full-width, so it stands on its own
rather than sitting in a row. It fills whatever container holds it —
here a narrow box, but the same button spans a whole card or a
mobile screen.
Live demo
Markup
<button class="k-btn k-btn--block">Continue</button>
States
Every variant carries the full set of states, all styled from
tokens — hover darkens toward the strong shade, keyboard focus
swaps the shadow for a soft accent ring, :active
presses the button down one pixel, [disabled] dims
and blocks the cursor, and aria-busy="true" dims and
blocks the pointer while a .k-spinner turns. Two of
these are browser-managed and can't be forced from a stylesheet:
Tab through and hover the row above to feel the real
focus ring and hover. In the matrix below those two
columns are honest fakes — static buttons wearing the actual ring
and press styles inline, each labeled (simulated). The
disabled and busy columns are the real attributes.
Live demo
| Variant | Resting | Focus ring (simulated) | Active (simulated) | Disabled (real) | Busy (real) |
|---|---|---|---|---|---|
.k-btn |
|||||
--ghost |
|||||
--soft |
|||||
--secondary |
The focus ring is always the accent ring —
0 0 0 3px var(--k-accent-trans-20) — on every
variant, so keyboard users get one consistent signal. Note the
ring stays accent-colored even on --secondary and
--danger.
Markup
<!-- resting — hover, focus, and active need no markup at all -->
<button class="k-btn">Primary</button>
<!-- disabled — real attribute; buttons only, links can't be disabled -->
<button class="k-btn" disabled>Primary</button>
<!-- busy — real attribute: dimmed, pointer blocked, announced -->
<button class="k-btn" aria-busy="true">
<span class="k-spinner" aria-hidden="true"></span> Primary
</button>
<!-- the focus-ring and active cells above are simulated for display:
:focus-visible and :active belong to the browser and can't be
forced from CSS. The inline styles are the real state styles:
focus ring — box-shadow: 0 0 0 3px var(--k-accent-trans-20)
active — translate: 0 1px -->
Your own variants
Every button reads local knobs with token defaults —
--k-btn-bg, --k-btn-fg,
--k-btn-bg-hover, --k-btn-border,
--k-btn-radius. The shipped modifiers
(--ghost, --soft, …) do nothing but set
those knobs, and a variant of your own is the same few lines of
plain CSS. This is keel's mixin system — native, no build step.
The naming grammar is BEM: k-block__element--modifier;
your variants join it as ordinary classes. Because every knob
defaults to a token, a custom variant stays on-system
automatically — retheme the tokens and it follows. The two extra
buttons below are defined in this page's own style
block:
Live demo
Markup
<style>
.btn-sale {
--k-btn-bg: var(--k-warn);
--k-btn-bg-hover: color-mix(in oklab, var(--k-warn), black 15%);
}
.btn-quiet {
--k-btn-bg: var(--k-accent-trans-10);
--k-btn-fg: var(--k-accent-strong);
--k-btn-bg-hover: var(--k-accent-trans-20);
}
</style>
<button class="k-btn btn-sale">On sale</button>
<button class="k-btn btn-quiet">Quiet</button>
Button group
.k-btn-group joins buttons into one row — inner
corners lose their radius, only the ends keep it. Adjacent
--ghost buttons also collapse the doubled border
between them. Solid buttons read as segments of one control, so
group buttons that belong to one choice, not unrelated actions.
Live demo
Markup
<div class="k-btn-group">
<button class="k-btn k-btn--ghost">Day</button>
<button class="k-btn k-btn--ghost">Week</button>
<button class="k-btn k-btn--ghost">Month</button>
</div>
Input group
.k-input-group joins an input and a
.k-btn into one row — the shared corner loses its
radius on both sides. The classic use is an email-subscribe row.
Keep the label; .k-visually-hidden hides it without
taking it from screen readers.
Live demo
Markup
<form class="k-input-group" action="/subscribe" method="post">
<label class="k-visually-hidden" for="subscribe">Email</label>
<input id="subscribe" name="email" type="email" placeholder="you@example.com">
<button class="k-btn">Subscribe</button>
</form>
Close button
.k-close is a quiet square button for dismissing
things — toasts, dialogs, banners. It renders whatever character
you put in it (usually ×). It's icon-only, so the
aria-label is not optional.
Live demo
A dismissible alert.
Markup
<button class="k-close" type="button" aria-label="Close">×</button>
Icon button
.k-btn--icon squares a button off for a single
.k-icon — equal padding, a 1:1 aspect ratio, the
icon centered. It combines with the other button variants.
There's no visible text, so the aria-label is not
optional. A busy button takes aria-busy="true":
keel dims it and blocks the pointer, and the attribute tells
assistive tech the same thing the dimming tells everyone else.
Live demo
Markup
<button class="k-btn k-btn--icon" type="button" aria-label="Search">
<span class="k-icon k-icon--search" aria-hidden="true"></span>
</button>
<!-- busy state — dimmed, pointer blocked, announced -->
<button class="k-btn" type="button" aria-busy="true">
<span class="k-spinner" aria-hidden="true"></span> Saving…
</button>