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.

Clicked 0 times
<Button onClick={() => alert('clicked')}>Click me</Button>

Button props

PropTypeDefaultDescription
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.
loadingbooleanShow a spinner and disable the button. Width stays stable so the layout doesn’t shift.
leftIconReactNodeIcon (or any node) placed before the label.
rightIconReactNodeIcon (or any node) placed after the label.
iconBadgebooleanWrap `leftIcon` / `rightIcon` in a circular badge — matches the boogie.ro CTA style.
disabledbooleanStandard. Dims the button and disables interactions.

All other props are forwarded to the underlying <button>.

ButtonGroup props

PropTypeDefaultDescription
attachedbooleanRemove 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">.