# Split Button A compound button that combines a primary action button with a dropdown menu toggle. The primary button fires an action, while the secondary caret button opens a menu of alternative actions. ```tsx demo import { SplitButton } from "@tenstorrent/vesper/split-button"; export default function SplitButtonDemo() { return ( {} }, { text: "Save and publish", onSelect: () => {} }, ]} > Save ); } ``` > [!IMPORTANT] > > Keep the content of the action button short (1-3 words) and descriptive of the action being taken, the same way you would for [a `Button`](./button.mdx). The menu items beside it should be variations on that action, not unrelated ones. ## Options | Prop | Type | Description | Default | | --------------------- | ---------------------------------------- | --------------------------------------------------------------------------- | --------------- | | `children` | `ReactNode` | The content of the primary action button. | — | | `menuItems` | `MenuItemProps[]` | The list of menu items rendered in the dropdown. **Required.** | — | | `onClick` | `MouseEventHandler` | Callback fired when the primary action button is clicked. | — | | `size` | `"sm" \| "md" \| "lg"` | The size of both the action button and the menu toggle. | `"md"` | | `variant` | `"subtle" \| "contrast"` | The visual style variant applied to both buttons. | `"subtle"` | | `disabled` | `boolean` | When `true`, disables both buttons. | `false` | | `menuButtonAriaLabel` | `string` | An accessible `aria-label` for the menu toggle button. | `"Toggle menu"` | | `menuWidth` | `number` | The width of the dropdown menu in pixels. Scales with the base rem size. | `200` | | `menuSide` | `"top" \| "bottom" \| "left" \| "right"` | The preferred side of the split button to render the dropdown menu against. | `"bottom"` | | `menuSideOffset` | `number` | The distance in pixels from the split button to the dropdown menu. | `8` | | `menuAlign` | `"start" \| "center" \| "end"` | The alignment of the dropdown menu relative to the split button. | `"start"` | | `menuAlignOffset` | `number` | An offset in pixels from the aligned edge of the split button. | `0` | | `menuOpen` | `boolean` | Controls the open state of the dropdown menu (controlled mode). | — | | `defaultMenuOpen` | `boolean` | Whether the dropdown menu is open by default (uncontrolled mode). | `false` | | `onMenuOpenChange` | `(open: boolean) => void` | Callback fired when the dropdown menu's open state changes. | — | All other props are forwarded to the underlying `
` element that wraps the two buttons. Use a `SplitButton` when one action is the obvious default, but closely related variations of it should stay within reach. When there is no default action to promote, reach for [the `Menu` component](./menu.mdx) on its own; when there are no alternatives to offer, reach for [the `Button` component](./button.mdx). ## Examples ### Basic usage Render a `SplitButton` by giving it some children to label the action button, an `onClick` handler for that action, and the `menuItems` that make up its dropdown: ```tsx demo import { useState } from "react"; import { SplitButton } from "@tenstorrent/vesper/split-button"; import { Typography } from "@tenstorrent/vesper/typography"; export default function BasicSplitButton() { const [lastAction, setLastAction] = useState(null); return (
setLastAction("Save")} menuItems={[ { text: "Save as draft", onSelect: () => setLastAction("Draft") }, { text: "Save and publish", onSelect: () => setLastAction("Publish"), }, ]} > Save Last action: {lastAction ?? "none"}
); } ``` Clicking the action button fires `onClick` without ever opening the menu, and clicking the caret button opens the menu without firing `onClick`. ### Menu items `menuItems` takes the same items as [the `Menu` component](./menu.mdx), so each one can render an `icon`, a `description`, and one of the `"default"`, `"selected"`, `"danger"`, `"locked"`, or `"disabled"` styles: ```tsx demo import { Copy, Download, Trash } from "@tenstorrent/vesper/icons"; import { SplitButton } from "@tenstorrent/vesper/split-button"; export default function SplitButtonMenuItems() { return ( {}} menuItems={[ { text: "Export as CSV", icon: , description: "Includes every visible column", onSelect: () => {}, }, { text: "Copy to clipboard", icon: , onSelect: () => {}, }, { text: "Delete export", icon: , style: "danger", onSelect: () => {}, }, ]} > Export ); } ``` ### Variants A `SplitButton` can be rendered in one of two variants, which is applied to both of its buttons at once: | Variant | When to use | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | `"contrast"` | Use when the action needs strong visual contrast against its surrounding content, such as the main action of a toolbar. | | `"subtle"` | Default variant. Use when the action should sit alongside surrounding content without dominating it, such as a row-level action in a table. | ```tsx demo import { SplitButton } from "@tenstorrent/vesper/split-button"; const MENU_ITEMS = [ { text: "Save as draft", onSelect: () => {} }, { text: "Save and publish", onSelect: () => {} }, ]; export default function SplitButtonVariants() { return (
Contrast Subtle
); } ``` ### Different sizes A `SplitButton` can be rendered in one of three sizes; `"sm"`, `"md"`, or `"lg"`. The default size is `"md"`. ```tsx demo import { SplitButton } from "@tenstorrent/vesper/split-button"; const MENU_ITEMS = [ { text: "Save as draft", onSelect: () => {} }, { text: "Save and publish", onSelect: () => {} }, ]; export default function SplitButtonSizes() { return (
Small Medium Large
); } ``` ### Setting the menu width The dropdown is `200px` wide by default. Pass `menuWidth` when that is too narrow for its items, such as when they carry descriptions or unusually long labels. Like the menu offsets, this value scales with the base rem size: ```tsx demo import { SplitButton } from "@tenstorrent/vesper/split-button"; const MENU_ITEMS = [ { text: "Export as CSV", onSelect: () => {} }, { text: "Export as JSON", onSelect: () => {} }, ]; export default function SplitButtonMenuWidth() { return ( 320px width menu ); } ``` ### Positioning the menu The dropdown is positioned against the split button as a whole, not just the caret button that opens it, so a menu aligned to `"end"` lines up with the right edge of the action button. The `menuSide`, `menuSideOffset`, `menuAlign`, and `menuAlignOffset` props map onto [the `Menu` component](./menu.mdx)'s `side`, `sideOffset`, `align`, and `alignOffset` props: ```tsx demo import { SplitButton } from "@tenstorrent/vesper/split-button"; const MENU_ITEMS = [ { text: "Save as draft", onSelect: () => {} }, { text: "Save and publish", onSelect: () => {} }, ]; export default function SplitButtonMenuPlacement() { return (
Aligned to start Aligned to end Above the button
); } ``` ### Controlling the menu's open state By default, a `SplitButton` tracks its own menu state. Pass `defaultMenuOpen` to render with the menu already open, or use `menuOpen` and `onMenuOpenChange` together to own the state yourself: ```tsx demo import { useState } from "react"; import { SplitButton } from "@tenstorrent/vesper/split-button"; import { Typography } from "@tenstorrent/vesper/typography"; export default function ControlledSplitButtonMenu() { const [menuOpen, setMenuOpen] = useState(false); return (
{}} menuItems={[ { text: "Save as draft", onSelect: () => {} }, { text: "Save and publish", onSelect: () => {} }, ]} > Save Menu open state: {String(menuOpen)}
); } ``` ### Labelling the menu toggle The caret button has no visible text, so it is labelled `"Toggle menu"` for assistive technology by default. Pass `menuButtonAriaLabel` to override the default ARIA label: ```tsx demo import { SplitButton } from "@tenstorrent/vesper/split-button"; export default function LabelledSplitButton() { return ( {}} menuItems={[ { text: "Export as CSV", onSelect: () => {} }, { text: "Export as JSON", onSelect: () => {} }, ]} > Export ); } ``` ### Disabling a split button Pass `disabled` to disable both the action button and the caret button at the same time. The action button stops firing `onClick`, and the caret button no longer opens its menu: ```tsx demo import { SplitButton } from "@tenstorrent/vesper/split-button"; export default function DisabledSplitButton() { return ( {}} menuItems={[ { text: "Save as draft", onSelect: () => {} }, { text: "Save and publish", onSelect: () => {} }, ]} > Save ); } ``` > [!NOTE] > > There is no way to disable only one half of a `SplitButton`. When the alternatives are unavailable but the main action is not (or the other way around), render the individual items as `"disabled"` or `"locked"` [menu items](./menu.mdx) instead, so the reason stays visible to the user.