Button Group
Attached segmented button group with shared borders (radio semantics) — for period pickers and view switchers. Selected segment gets a surface-2 fill and bold label.
neo-corporate/ui/button-groupSource
"use client";
import { useState, type ReactNode } from "react";
import { cn } from "@/lib/utils";
export interface ButtonGroupOption {
value: string;
label: ReactNode;
/** Accessible label when `label` is an icon. */
ariaLabel?: string;
}
export interface ButtonGroupProps {
options?: ButtonGroupOption[];
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
/** Group label for screen readers. */
label?: string;
size?: "sm" | "md";
className?: string;
}
const DEFAULT_OPTIONS: ButtonGroupOption[] = [
{ value: "7d", label: "7D" },
{ value: "30d", label: "30D" },
{ value: "qtd", label: "QTD" },
{ value: "ytd", label: "YTD" },
{ value: "12m", label: "12M" },
];
/** Attached segmented button group with shared borders (radio semantics) — for period pickers and view switchers. Selected segment gets a surface-2 fill and bold label. */
export function ButtonGroup({ options = DEFAULT_OPTIONS, value, defaultValue, onValueChange, label = "Period", size = "md", className }: ButtonGroupProps) {
const [inner, setInner] = useState(defaultValue ?? options[0]?.value);
const current = value ?? inner;
const select = (v: string) => {
setInner(v);
onValueChange?.(v);
};
return (
<div role="radiogroup" aria-label={label} className={cn("da-stroke inline-flex overflow-hidden rounded-da-md bg-da-surface shadow-da-sm", className)}>
{options.map((o, i) => {
const on = o.value === current;
return (
<button
key={o.value}
type="button"
role="radio"
aria-checked={on}
aria-label={o.ariaLabel}
tabIndex={on ? 0 : -1}
onClick={() => select(o.value)}
onKeyDown={(e) => {
const d = e.key === "ArrowRight" || e.key === "ArrowDown" ? 1 : e.key === "ArrowLeft" || e.key === "ArrowUp" ? -1 : 0;
if (!d) return;
e.preventDefault();
const next = options[(i + d + options.length) % options.length]!;
select(next.value);
(e.currentTarget.parentElement?.children[(i + d + options.length) % options.length] as HTMLElement | undefined)?.focus();
}}
className={cn(
"da-focus da-transition inline-flex items-center justify-center gap-1.5 border-da-border font-medium not-first:border-l focus-visible:relative focus-visible:z-10 [&_svg]:size-4",
size === "sm" ? "h-8 px-2.5 text-xs" : "h-9 px-3.5 text-sm",
on ? "bg-da-surface-2 font-semibold text-da-fg" : "text-da-muted-fg hover:bg-da-surface-2 hover:text-da-fg",
)}
>
{o.label}
</button>
);
})}
</div>
);
}
export default ButtonGroup;
modules/neo-corporate/ui/button-group/index.tsx
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| options | ButtonGroupOption[] | — | Options. |
| value | string | — | Value. |
| defaultValue | string | — | Default Value. |
| onValueChange | (value: string) => void | — | Callback. |
| label | string | — | Group label for screen readers. |
| size | "sm" | "md" | — | Size. |
| className | string | — | Extra classes on the root. |
Other button variants in Neo Corporate
Button
Corporate button: 6px radius, five variants (blue primary, bordered white secondary, ghost, danger, link), three sizes, leading/trailing icons.
Icon Button
Square icon-only button (secondary, ghost, primary, danger) with required aria-label, title tooltip and optional count/dot badge.
Loading Button
Async action button: idle → spinner + “Processing…” (aria-busy, width locked) → green check “Sent” for 2s → idle. Default simulates a 1.4s payment run.
Button in other art directions
Button
Pressable button with 6 variants (primary, secondary, ink, outline, ghost, danger), 4 sizes, icons, loading state and link mode (href). Hard shadow collapses on hover/press.
Copy Button
Code field with an attached yellow copy button that flips to green 'Copied' with a check; optional secret masking for API keys and custom display text for commands.
Segmented Control
Ruled segmented control for mutually exclusive options: mono uppercase segments split by strokes, ink selected segment, yellow hover. Controlled or uncontrolled, typed values.
Split Button
Yellow primary action glued to a chevron trigger that opens a ruled dropdown of secondary actions with descriptions (Radix DropdownMenu). Both halves flip to ink on hover.
Button
Pill button with 5 variants (primary→accent gradient, frosted glass, solid ink, ghost, danger), 4 sizes, icons, loading state and link mode. Lifts on hover with a slight overshoot.
Icon Button
Round frosted icon button with a tooltip; optional toggle mode (aria-pressed) that swaps the icon and fills with the gradient or danger color — ideal for call controls.
Record Button
Record toggle pill: red dot inside a soft halo that morphs into a stop square with a pulsing ring and a running mm:ss timer while recording.
Shine Button
Gradient pill button with a glossy top highlight and a bright light sweep crossing it on hover or keyboard focus (CSS only); renders an anchor when href is set. Two sizes.
Button
Compact button with 6 variants (indigo primary with inner highlight, ink secondary, outline, ghost, danger, link), 4 sizes, icons, keyboard-shortcut hint, loading state and link mode.
Loading Button
Async action button that morphs through pending (spinner), success (green check) and error (red cross) states, then resets; all labels share one grid cell so the width never jumps.
Shortcut Button
Button displaying its keyboard shortcut as keycaps; pressing the shortcut anywhere on the page clicks it (ignored while typing in a field). Primary, secondary and ghost styles.
Toggle Group
Single-choice segmented toolbar on a muted track with a raised active segment (Radix ToggleGroup): text, icon-only or mixed options, two sizes, controlled or uncontrolled.
Arrow Link
Text link with an ↗ that nudges up-right on hover and an ink underline that wipes in from the left; optional mono index prefix; scales from inline to display size.
Button
Square monochrome button: ink primary, outlined (inverts on hover), ghost and underline (signal-colored underline) variants; three sizes and an optional trailing glyph.
Icon Button
Square icon-only button with a hairline border (darkens on hover), ghost or solid ink variant; required label for aria-label and title.
Text Toggle
Typographic toggle: options are plain mono words separated by slashes; the active one is ink with a signal underline, others muted — radio semantics with arrow keys.
Arrow Button
Link-button with a round arrow badge at its end: on hover the badge grows and the arrow turns from ↗ to →, a signature call-to-action for the DA.
Button
Soft pill button: sage primary, sage-tint soft, terracotta clay, outlined ink and ghost variants; three sizes, icons, gentle hover.
Icon Button
Round icon-only button in soft sage, sage, cream (with shadow) or ghost; required label used for aria-label and title.
Segmented
Pill segmented control on a sand track: the selected pill is cream with a soft shadow and slides with a sliding indicator; radio semantics with arrow keys.
Button
Friendly pill button: six variants (periwinkle primary, soft lavender, outline, ghost, danger, ink), three sizes, left/right icons, loading spinner and link mode, with a tiny bounce on hover.
Fab
Floating action button: a big round periwinkle “+” that rotates into “×” and fans out labelled pastel speed-dial actions with a staggered bounce; closes on Escape or outside click.
Icon Button
Round pastel icon button in six tones (neutral, peach, mint, lavender, butter, primary) and three sizes, with optional red counter bubble and a springy press.
Toggle Chips
Row of pill toggle chips with emoji; selected chips fill periwinkle and show a popping check. Multiple or single selection, controlled or not.