Skip to content
Primitives

Button

Squared technical button with signal, ghost, outline-tech, destructive-hazard, and data variants, plus a built-in loading state.

● LIVE
npx shadcn@latest add https://okibi.cndr.dev/r/button.json

Installation

Install the theme first, then add the component:

npx shadcn@latest add https://okibi.cndr.dev/r/button.json

Install the required dependencies:

npm install @radix-ui/react-slot class-variance-authority

Copy the source from the Code tab above into components/ui/button.tsx. It uses the cn helper and the okibi theme tokens — install the theme first if you haven't.

Usage

import { Button } from "@/components/ui/button"

<Button variant="signal">Transmit</Button>

Examples

Sizes

Four sizes share the same 2px keyline and grid tracking: sm, md (default), lg, and a square icon. Match the size to the surrounding density — lg for primary page actions, icon for toolbars.

● LIVE

With icon

Pair a label with a leading or trailing icon. Icons auto-size to the text (1.1em) and are spaced by the built-in gap, so no extra classes are needed. Lead with an icon for create/add actions; trail it for forward navigation.

● LIVE

Icon only

Square, label-less buttons for dense toolbars. Each one must carry an aria-label — there is no visible text to name the action for assistive technology.

● LIVE

Loading

Set loading to run the signal transmission rail, mark the button aria-busy, and disable it while an async action is in flight. The label stays in place (no layout shift) and the rail works at every size.

● LIVE

Disabled

The native disabled attribute blocks all interaction and dims the control to 60% across every variant. Use it for actions that are currently unavailable; prefer loading for actions that are in progress.

● LIVE

As child

Not a visual state — a composition prop. asChild applies the button styling to its single child instead of rendering a button, so a link can look like a button while staying a real, navigable a element. Exactly one child; loading is ignored in this mode.

● LIVE

API Reference

Button

PropTypeDefaultDescription
variant"signal" | "ghost" | "outline-tech" | "destructive-hazard" | "data""signal"Visual style.
size"sm" | "md" | "lg" | "icon""md"Control size. icon is square — pass an aria-label.
loadingbooleanfalseRuns the transmission rail, sets aria-busy and disables the button.
asChildbooleanfalseRender the styles on the child element instead of a <button>.

Accessibility

  • Renders a native button by default; focus, Enter/Space activation and disabled semantics come for free.
  • Icon-only buttons (size='icon') carry no text — pass an aria-label so assistive tech can name them.
  • loading sets aria-busy, applies native disabled, and keeps the label in place — no layout shift and no silent state change.
  • The pending transmission rail is aria-hidden and reduced-motion-safe — the global rule freezes it to static signal dashes.
  • Focus-visible draws a 2px signal ring offset from the control; aria-invalid swaps the border to destructive.
  • With asChild, semantics come from the child you pass — give an a element an href and loading is ignored.

Guidelines

Do

  • Keep one signal button as the primary action per view; demote the rest to outline-tech, ghost, or data.
  • Reach for lg (48px) on primary touch targets — fingers occlude smaller controls.
  • Use loading to gate a submit while a request is in flight.
  • Keep labels terse — they never wrap (whitespace-nowrap) and will overflow a tight slot.

Don't

  • Don't disable a button to signal progress — set loading instead so the busy state is announced.
  • Don't ship an icon button without an aria-label.
  • Don't pass loading alongside asChild — Slot accepts one child, so the rail is dropped (dev warns).
  • Don't stack multiple signal buttons in one row; the primary action stops reading as primary.

On this page