Button
Squared technical button with signal, ghost, outline-tech, destructive-hazard, and data variants, plus a built-in loading state.
import { Button } from "@/registry/okibi/ui/button";export default function ButtonDemo() { return ( <div className="flex flex-wrap items-center justify-center gap-3"> <Button variant="signal">Signal</Button> <Button variant="outline-tech">Outline</Button> <Button variant="ghost">Ghost</Button> <Button variant="data">Data</Button> <Button variant="destructive-hazard">Hazard</Button> </div> );}npx shadcn@latest add https://okibi.cndr.dev/r/button.jsonInstallation
Install the theme first, then add the component:
npx shadcn@latest add https://okibi.cndr.dev/r/button.jsonInstall the required dependencies:
npm install @radix-ui/react-slot class-variance-authorityCopy 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.
import { PlusIcon } from "lucide-react";import { Button } from "@/registry/okibi/ui/button";export default function ButtonSizesDemo() { return ( <div className="flex flex-wrap items-center justify-center gap-3"> <Button size="sm">Small</Button> <Button size="md">Medium</Button> <Button size="lg">Large</Button> <Button size="icon" aria-label="Add"> <PlusIcon /> </Button> </div> );}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.
import { ArrowRightIcon, PlusIcon } from "lucide-react";import { Button } from "@/registry/okibi/ui/button";export default function ButtonWithIconDemo() { return ( <div className="flex flex-wrap items-center justify-center gap-3"> <Button> <PlusIcon /> New </Button> <Button variant="outline-tech"> Continue <ArrowRightIcon /> </Button> </div> );}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.
import { PlayIcon, SettingsIcon, Trash2Icon } from "lucide-react";import { Button } from "@/registry/okibi/ui/button";export default function ButtonIconDemo() { // Icon-only buttons must carry an `aria-label` so the action has an accessible name. return ( <div className="flex flex-wrap items-center justify-center gap-3"> <Button size="icon" aria-label="Run"> <PlayIcon /> </Button> <Button size="icon" variant="outline-tech" aria-label="Settings"> <SettingsIcon /> </Button> <Button size="icon" variant="destructive-hazard" aria-label="Delete"> <Trash2Icon /> </Button> </div> );}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.
import { RefreshCwIcon } from "lucide-react";import { Button } from "@/registry/okibi/ui/button";export default function ButtonLoadingDemo() { // `loading` keeps the label visible, runs the signal transmission rail along the // bottom edge, sets `aria-busy`, and disables the button. return ( <div className="flex flex-wrap items-center justify-center gap-3"> <Button loading>Deploy</Button> <Button variant="outline-tech" loading> Sync </Button> <Button variant="destructive-hazard" loading> Purge </Button> <Button size="icon" aria-label="Refreshing" loading> <RefreshCwIcon /> </Button> </div> );}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.
import { Button } from "@/registry/okibi/ui/button";export default function ButtonDisabledDemo() { return ( <div className="flex flex-wrap items-center justify-center gap-3"> <Button disabled>Signal</Button> <Button variant="outline-tech" disabled> Outline </Button> <Button variant="ghost" disabled> Ghost </Button> <Button variant="data" disabled> Data </Button> </div> );}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.
import { Button } from "@/registry/okibi/ui/button";export default function ButtonAsChildDemo() { // `asChild` applies the button styling to its single child — render any link or // element (e.g. an `<a>` or a framework `<Link>`) and keep the button look. return ( <Button asChild variant="outline-tech"> <a href="#">Documentation</a> </Button> );}API Reference
Button
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
loading | boolean | false | Runs the transmission rail, sets aria-busy and disables the button. |
asChild | boolean | false | Render the styles on the child element instead of a <button>. |
Accessibility
- Renders a native
buttonby default; focus, Enter/Space activation anddisabledsemantics come for free. - Icon-only buttons (
size='icon') carry no text — pass anaria-labelso assistive tech can name them. loadingsetsaria-busy, applies nativedisabled, and keeps the label in place — no layout shift and no silent state change.- The pending transmission rail is
aria-hiddenand reduced-motion-safe — the global rule freezes it to static signal dashes. - Focus-visible draws a 2px signal ring offset from the control;
aria-invalidswaps the border to destructive. - With
asChild, semantics come from the child you pass — give anaelement anhrefandloadingis ignored.
Guidelines
Do
- Keep one
signalbutton as the primary action per view; demote the rest tooutline-tech,ghost, ordata. - Reach for
lg(48px) on primary touch targets — fingers occlude smaller controls. - Use
loadingto 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
loadinginstead so the busy state is announced. - Don't ship an
iconbutton without anaria-label. - Don't pass
loadingalongsideasChild— Slot accepts one child, so the rail is dropped (dev warns). - Don't stack multiple
signalbuttons in one row; the primary action stops reading as primary.