Button
Triggers an action or event when activated.
Overview
The Button component provides a versatile and customizable button system with multiple variants, appearances, sizes, and shapes. It supports polymorphic rendering with the render prop (Base UI pattern), as well as special features like pending states and URL state management.
Use Button.Wrapper to visually attach adjacent buttons for split-button layouts while keeping each inner action as a real Button.
Usage
import { Button } from "@tilt-legal/cubitt-components/button";<Button>Click me</Button>Using Cubitt's URL-state hooks, you can sync button clicks with the URL by providing paramName and paramSetValue:
// Clicking sets ?action=save in the URL
<Button paramName="action" paramSetValue="save">
Save Draft
</Button>
// Advanced options:
<Button
paramName="action"
paramSetValue="publish"
paramDebounce={300} // Debounce URL updates
>
Publish
</Button>Use the render prop to render the button as a different element while preserving button styling:
import { Button } from "@tilt-legal/cubitt-components/button";
import { Link } from "@tanstack/react-router";
<Button render={<Link href="/home" />}>Go Home</Button>;Examples
Secondary
import { Button } from "@tilt-legal/cubitt-components/button";
export default function Component() {
return <Button variant="secondary">Secondary</Button>;
}Frost
import { Button } from "@tilt-legal/cubitt-components/button";
export default function Component() {
return <Button variant="frost">Frost</Button>;
}Destructive
import { Button } from "@tilt-legal/cubitt-components/button";
export default function Component() {
return <Button variant="destructive">Destructive</Button>;
}Outline
import { Button } from "@tilt-legal/cubitt-components/button";
export default function Component() {
return <Button variant="outline">Outline</Button>;
}Ghost
import { Button } from "@tilt-legal/cubitt-components/button";
export default function Component() {
return <Button variant="ghost">Ghost</Button>;
}With Icon
import { Button } from "@tilt-legal/cubitt-components/button";
import { Archive, UserPen } from "@tilt-legal/cubitt-icons/ui/outline";
export default function Component() {
return (
<Button variant="secondary">
<UserPen />
With Icon
</Button>
);
}Icon Only
import { Button } from "@tilt-legal/cubitt-components/button";
import { UserPen } from "@tilt-legal/cubitt-icons/ui/outline";
export default function Component() {
return (
<Button aria-label="Edit profile" variant="secondary" mode="icon">
<UserPen />
</Button>
);
}Tooltip
Use tooltip to attach compact context to icon-only or disabled actions without wrapping the button manually.
import { Button } from "@tilt-legal/cubitt-components/button";
import { UserPen } from "@tilt-legal/cubitt-icons/ui/outline";
export default function Component() {
return (
<div className="flex items-center gap-4">
<Button
aria-label="Edit profile"
mode="icon"
tooltip="Edit profile"
variant="secondary"
>
<UserPen />
</Button>
<Button
aria-label="Archive"
disabled
mode="icon"
tooltip={{
content: "Archiving is unavailable",
side: "bottom",
sideOffset: 6,
}}
variant="secondary"
>
<Archive />
</Button>
</div>
);
}Icon Label Transition
Keep a plain text label mounted and toggle mode="icon" when a button needs to collapse to its icon-only form. Cubitt handles the visual label transition while preserving the button's accessible name.
"use client";
import { Button } from "@tilt-legal/cubitt-components/button";
import { Bell, UserPen } from "@tilt-legal/cubitt-icons/ui/outline";
import { useState } from "react";
export default function Component() {
const [secondaryCollapsed, setSecondaryCollapsed] = useState(true);
const [variantCollapsed, setVariantCollapsed] = useState(true);
return (
<div className="flex flex-col items-center gap-3">
<Button
aria-pressed={!secondaryCollapsed}
mode={secondaryCollapsed ? "icon" : undefined}
onClick={() => setSecondaryCollapsed((current) => !current)}
variant="secondary"
>
<UserPen />
Edit profile
</Button>
<Button
aria-pressed={!variantCollapsed}
mode={variantCollapsed ? "icon" : undefined}
onClick={() => setVariantCollapsed((current) => !current)}
variant={variantCollapsed ? "ghost" : "outline"}
>
<Bell />
Notifications
</Button>
</div>
);
}Pending
Shows a loading spinner while an action is in progress.
"use client";
import { Button } from "@tilt-legal/cubitt-components/button";
import { useEffect, useState } from "react";
export default function Component() {
const [isPending, setIsPending] = useState(false);
useEffect(() => {
if (!isPending) {
return;
}
const timeout = window.setTimeout(() => {
setIsPending(false);
}, 2000);
return () => window.clearTimeout(timeout);
}, [isPending]);
return (
<Button
variant="secondary"
onClick={() => setIsPending(true)}
pending={isPending}
>
Submit
</Button>
);
}With Badge
Combine buttons with badges for notifications and counters.
import { Button } from "@tilt-legal/cubitt-components/button";
import { Bell, Inbox, CircleCheck } from "@tilt-legal/cubitt-icons/ui/outline";
export default function Component() {
return (
<div className="flex flex-col items-center gap-10">
<Button variant="outline" mode="icon" className="relative">
<Bell />
<span className="border-2 border-background rounded-full size-3 bg-primary absolute -top-1 -inset-e-1 animate-bounce" />
</Button>
<Button variant="outline" mode="icon" className="relative">
<Inbox />
<Badge
variant="primary"
shape="circle"
size="sm"
className="absolute top-0 inset-s-full -translate-y-1/2 -translate-x-1/2 rtl:translate-x-1/2"
>
5
</Badge>
</Button>
<Button variant="outline" className="relative">
<Inbox />
Messages
<Badge
variant="destructive"
shape="circle"
size="sm"
className="absolute top-0 inset-s-full -translate-y-1/2 -translate-x-1/2 rtl:translate-x-1/2"
>
5
</Badge>
</Button>
<Button variant="outline">
<CircleCheck />
Notifications
<Badge variant="brand" size="sm">
10+
</Badge>
</Button>
</div>
);
}Sizes
import { Button } from "@tilt-legal/cubitt-components/button";
import { UserPen } from "@tilt-legal/cubitt-icons/ui/outline";
export default function Component() {
return (
<div className="flex flex-col items-center justify-center gap-6">
<div className="flex items-center gap-4">
<Button variant="outline" size="sm">
<UserPen />
Small
</Button>
<Button variant="outline">
<UserPen />
Medium
</Button>
<Button variant="outline" size="lg">
<UserPen />
Large
</Button>
</div>
<div className="flex items-center gap-4">
<Button variant="outline" size="sm" mode="icon">
<UserPen />
</Button>
<Button variant="outline" mode="icon">
<UserPen />
</Button>
<Button variant="outline" size="lg" mode="icon">
<UserPen />
</Button>
</div>
</div>
);
}Link
Style buttons as links using variant="link".
import { Button } from "@tilt-legal/cubitt-components/button";
import { Link } from "@tanstack/react-router";
export default function Component() {
return (
<div className="flex flex-col items-center gap-6">
<Button variant="link" size="sm" render={<Link href="#" />}>
Link Button
</Button>
<Button variant="link" render={<Link href="#" />}>
Link Button
</Button>
<Button variant="link" size="lg" render={<Link href="#" />}>
Link Button
</Button>
</div>
);
}Split Button
Use Button.Wrapper when you want a primary action and an adjacent secondary trigger to read as a single control.
"use client";
import { Button } from "@tilt-legal/cubitt-components/button";
import {
Menu,
MenuContent,
MenuItem,
MenuSeparator,
MenuTrigger,
} from "@tilt-legal/cubitt-components/menu";
import { useEffect, useState } from "react";
export default function Component() {
const PENDING_RESET_DELAY_MS = 2000;
const variants = [
{ label: "Default", moreLabel: "More primary actions", variant: "default" },
{
label: "Secondary",
moreLabel: "More secondary actions",
variant: "secondary",
},
{
label: "Destructive",
moreLabel: "More destructive actions",
variant: "destructive",
},
{ label: "Frost", moreLabel: "More frost actions", variant: "frost" },
{ label: "Outline", moreLabel: "More outline actions", variant: "outline" },
] as const;
function SplitButtonItem({
label,
moreLabel,
variant,
}: (typeof variants)[number]) {
const [isPending, setIsPending] = useState(false);
const actionTooltip =
variant === "outline" ? "Run outline action" : undefined;
const menuTooltip =
variant === "outline"
? {
content: "More outline actions",
side: "bottom" as const,
sideOffset: 6,
}
: undefined;
useEffect(() => {
if (!isPending) {
return;
}
const timeout = window.setTimeout(() => {
setIsPending(false);
}, PENDING_RESET_DELAY_MS);
return () => window.clearTimeout(timeout);
}, [isPending]);
return (
<Menu>
<Button.Wrapper>
<Button
onClick={() => setIsPending(true)}
pending={isPending}
tooltip={actionTooltip}
variant={variant}
>
{label}
</Button>
<MenuTrigger
render={
<Button
aria-label={moreLabel}
mode="icon"
tooltip={menuTooltip}
variant={variant}
/>
}
>
<Button.Arrow className="me-0 ms-0" />
</MenuTrigger>
</Button.Wrapper>
<MenuContent align="end">
<MenuItem>Duplicate</MenuItem>
<MenuItem>Move</MenuItem>
<MenuSeparator />
<MenuItem variant="destructive">Archive</MenuItem>
</MenuContent>
</Menu>
);
}
return (
<div className="flex flex-wrap items-center gap-4">
{variants.map((item) => (
<SplitButtonItem key={item.variant} {...item} />
))}
</div>
);
}Disabled
import { Button } from "@tilt-legal/cubitt-components/button";
export default function Component() {
return (
<div className="flex items-center gap-4">
<Button disabled>Primary</Button>
<Button variant="secondary" disabled>
Secondary
</Button>
<Button variant="outline" disabled>
Outline
</Button>
</div>
);
}URL State
Sync button clicks with URL parameters. Click a button to see the URL update with the corresponding action.
import { Button } from "@tilt-legal/cubitt-components/button";
export default function Component() {
return (
<div className="flex items-center gap-4">
<Button paramName="demo-action" paramSetValue="save" variant="secondary">
Save Draft
</Button>
<Button paramName="demo-action" paramSetValue="publish">
Publish
</Button>
<Button
paramName="demo-action"
paramSetValue="archive"
variant="destructive"
>
Archive
</Button>
</div>
);
}API Reference
Button
The root component for creating buttons with multiple variants, appearances, sizes, and shapes. Supports polymorphic rendering via the render prop and URL state management.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "secondary" | "mono" | "inverse" | "destructive" | "frost" | "outline" | "ghost" | "link" | "default" | Visual style variant. |
size | "sm" | "md" | "lg" | "md" | Button size. |
mode | "icon" | "input" | — | Layout mode. Use icon for square icon buttons and input for form trigger styling. |
unstyled | boolean | false | Render as a completely unstyled button. |
pending | boolean | false | Show a loading spinner. Leading icons swap immediately; other layouts keep the animated inline indicator. |
tooltip | ButtonTooltip | — | Optional tooltip content or configuration. Disabled tooltip buttons use aria-disabled so the tooltip can still open. |
selected | boolean | false | Mark the button as selected for toggle or popup-trigger states. |
disabled | boolean | false | Disable the button. |
reduceMotion | boolean | false | Disable Button-owned animations. |
render | ReactElement | function | — | Render as another element while preserving Button behavior. |
className | string | — | Additional classes for the button. |
ButtonTooltip
The tooltip prop accepts a ReactNode or an object with these fields.
| Property | Type | Default | Description |
|---|---|---|---|
content | ReactNode | — | Tooltip content. Required for object form. |
delay | number | 500 | Hover/focus delay in milliseconds. |
side | "top" | "right" | "bottom" | "left" | "inline-start" | "inline-end" | "top" | Preferred tooltip side. |
sideOffset | number | 8 | Distance from the button in pixels. |
align | "start" | "center" | "end" | "center" | Alignment along the selected side. |
alignOffset | number | 0 | Offset along the alignment axis. |
className | string | — | Additional classes for the tooltip content. |
URL State Props
When provided with a paramName, the button will set URL parameters when clicked.
| Prop | Type | Default | Description |
|---|---|---|---|
paramName | string | — | URL parameter name. Enables URL state management. |
paramSetValue | string | — | Value to set in the URL when the button is clicked. |
paramThrottle | number | — | Throttle URL updates in milliseconds. |
paramDebounce | number | — | Debounce URL updates in milliseconds. |