Skip to content

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>