Button
The boogie button. Pill-shaped by default, with six built-in variants, three sizes, loading state, icon slots, a configurable corner radius, and a ButtonGroup primitive for stacking related actions.
Buttons communicate the actions users can take. They show up everywhere — forms, cards, toolbars, modals, navigation bars. Pick a variant that matches the action's emphasis level: solid for the page's primary CTA, primary for inline confirms, ghost / outline for secondary actions, green / yellow for status-coded actions.
Variants
Six built-in variants. primary is the default — a black pill that turns red on hover (the boogie signature). solid is the uppercase CTA pill with a deeper shadow for hero/landing use. ghost and outline are lower-emphasis alternatives.
<Button>Primary</Button>
<Button variant="green">Green</Button>
<Button variant="yellow">Yellow</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="outline">Outline</Button>
<Button variant="solid">Solid</Button>Sizes
Three presets: sm (compact toolbars / inline actions), md (default), lg (hero CTAs).
<Button size="sm">SM</Button>
<Button size="md">MD</Button> {/* default */}
<Button size="lg">LG</Button>Icons
Pass any node to leftIcon or rightIcon. They render in their own slot with sensible spacing — no extra wrapper needed in your JSX. Set iconBadge to render the icon inside a circular badge (matches the boogie.ro CTA style — a small pill on the left of the label).
<Button leftIcon={<PlusIcon />}>New item</Button>
<Button variant="outline" rightIcon={<ArrowRightIcon />}>
Continue
</Button>
<Button variant="ghost" leftIcon={<DownloadIcon />}>
Download
</Button>
{/* iconBadge wraps the icon in a circular pill */}
<Button variant="solid" leftIcon={<ArrowRightIcon />} iconBadge>
Get started
</Button>Loading
Set loading to swap the label for a spinner and disable interaction. The button keeps its dimensions so the layout doesn't shift. Use this for any action that fires a request — submit forms, save, fetch, refresh.
const [loading, setLoading] = useState(false);
async function onSubmit() {
setLoading(true);
try {
await save();
} finally {
setLoading(false);
}
}
<Button variant="primary" loading={loading} onClick={onSubmit}>
Save changes
</Button>Disabled
Standard disabled attribute. The button shows a dimmed appearance and won't fire onClick. Prefer loading over disabled when the reason is "waiting for a request" — loading communicates that the action is in flight, not that it's unavailable.
<Button disabled>Disabled primary</Button>
<Button variant="outline" disabled>Disabled outline</Button>
<Button variant="ghost" disabled leftIcon={<TrashIcon />}>
Disabled ghost
</Button>Corner radius
Buttons inherit --bwo-radius-current by default (matches every other primitive). Override per-button with the radius prop — values are none, sm (4px), md (6px, default), lg (12px), pill (9999px). The boogie signature is pill for everything.
<Button radius="none">none</Button>
<Button radius="sm">sm</Button>
<Button radius="md">md</Button> {/* default */}
<Button radius="lg">lg</Button>
<Button radius="pill">pill</Button>ButtonGroup
Pair related actions in a row. Default ButtonGroup just lays them out with a small gap. Add attached to remove the gap and let adjacent buttons share a seamless edge — useful for segmented controls, formatting toolbars, etc. The radius prop on ButtonGroup cascades to its child buttons via a CSS variable.
import { Button, ButtonGroup } from '@bwo-ui/react';
{/* Standard — small gap between buttons */}
<ButtonGroup>
<Button variant="ghost">Day</Button>
<Button variant="primary">Week</Button>
<Button variant="ghost">Month</Button>
</ButtonGroup>
{/* Attached — seamless segmented control */}
<ButtonGroup attached>
<Button variant="outline">Bold</Button>
<Button variant="outline">Italic</Button>
<Button variant="outline">Underline</Button>
</ButtonGroup>As a link
Button renders a native <button>. To navigate, wrap the button in an anchor (or your router's Link). The anchor handles navigation; the button handles the visual. Reset the anchor's underline.
{/* External link */}
<a href="https://github.com/BoogieCode/bwo-ui" style={{ textDecoration: 'none' }}>
<Button variant="outline" rightIcon={<ArrowRightIcon />}>
GitHub
</Button>
</a>
{/* Next.js / React Router */}
import Link from 'next/link';
<Link href="/dashboard" style={{ textDecoration: 'none' }}>
<Button variant="primary">Open dashboard</Button>
</Link>Handling clicks
Standard onClick. All native <button> props are forwarded — type, form, onMouseEnter, aria-*, etc.
<Button onClick={() => alert('clicked')}>Click me</Button>Button props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'primary' | 'green' | 'yellow' | 'ghost' | 'outline' | 'solid' | 'primary' | Visual variant. `solid` is the uppercase CTA pill from boogie-next. `ghost` / `outline` are lower-emphasis alternatives. |
size | 'sm' | 'md' | 'lg' | 'md' | Padding + font-size preset. |
radius | 'none' | 'light' | 'sm' | 'md' | 'lg' | 'pill' | — | Corner-radius preset. Omit to inherit the global default via the `--bwo-radius-current` CSS variable. |
loading | boolean | — | Show a spinner and disable the button. Width stays stable so the layout doesn’t shift. |
leftIcon | ReactNode | — | Icon (or any node) placed before the label. |
rightIcon | ReactNode | — | Icon (or any node) placed after the label. |
iconBadge | boolean | — | Wrap `leftIcon` / `rightIcon` in a circular badge — matches the boogie.ro CTA style. |
disabled | boolean | — | Standard. Dims the button and disables interactions. |
All other props are forwarded to the underlying <button>.
ButtonGroup props
| Prop | Type | Default | Description |
|---|---|---|---|
attached | boolean | — | Remove the gap between children so they share edges (seamless segmented control). |
radius | 'none' | 'light' | 'sm' | 'md' | 'lg' | 'pill' | — | Cascade a radius preset to every Button inside via `data-radius` + the `--bwo-radius-current` CSS variable. |
All other props are forwarded to the underlying <div role="group">.