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