Skip to content
  1. Keyline UI
  2. Button group
Keyline UIComponents
  • Get started
    • Installation
    • Theming
    • Modes
    • Configuration
    • Composition
  • Actions
    • Button
    • Button group
    • Split button
    • Toggle
    • Toggle group
    • Toolbar
    • Menu
    • Menubar
    • Context menu
  • Inputs
    • Field and form
    • Checkbox
    • Checkbox group
    • Radio group
    • Switch
    • Slider
    • Segmented
    • Select
    • Native select
    • Combobox
    • Multiselect
    • Number field
    • OTP field
    • Time field
    • Date picker and calendar
    • Date range
  • Data
    • Table
    • Chart
    • Stat and meter
    • Status and badge
    • Progress
    • Skeleton
    • Empty
    • Avatar
    • Kbd
    • Marker
  • Navigation
    • Tabs
    • Breadcrumb
    • Pagination
    • Nav
    • Navigation menu
    • Command menu
  • Overlays
    • Dialog
    • Alert dialog
    • Drawer
    • Sheet
    • Popover
    • Hover card
    • Tooltip
    • Toast
    • Notice
  • Layout
    • App shell
    • Sidebar
    • Panel
    • Resizable
    • Scroll area
    • Accordion
    • Collapsible
    • Carousel
    • Inspector
    • Item
    • Direction
  • Conversation
    • Message
    • Bubble
    • Message scroller
    • Attachment
    • Questionnaire
  • Blocks
    • Shipments

Button group

A few related buttons in one frame, joined on shared seams. Each button stays its own Tab stop. For a long row of commands that should be one stop, use Toolbar.

Loading the live example…

"use client"

import { useState } from "react"
import { Button } from "@/components/keyline/ui/button"
import { ButtonGroup, ButtonGroupText } from "@/components/keyline/ui/button-group"

const ORDERS = [
  { id: "DM-4102", client: "Scranton Business Park", paper: "40 cases, 20 lb copy", rep: "Dwight Schrute" },
  { id: "DM-4087", client: "Lackawanna County", paper: "12 cases, cardstock", rep: "Jim Halpert" },
  { id: "DM-4120", client: "Wilkes-Barre Library", paper: "6 cases, recycled bond", rep: "Phyllis Vance" },
  { id: "DM-4131", client: "Blue Cross of Pennsylvania", paper: "25 cases, letterhead", rep: "Stanley Hudson" },
  { id: "DM-4136", client: "Harper Collins, Scranton", paper: "8 cases, legal pads", rep: "Andy Bernard" },
]

export function OrderReview() {
  const [i, setI] = useState(0)
  const order = ORDERS[i]

  return (
    <div className="grid gap-4">
      <ButtonGroup aria-label="Today’s orders" className="justify-self-start">
        <Button layout="cell" disabled={i === 0} onClick={() => setI(i - 1)}>
          Previous
        </Button>
        <ButtonGroupText aria-live="polite">
          {i + 1} of {ORDERS.length}
        </ButtonGroupText>
        <Button layout="cell" disabled={i === ORDERS.length - 1} onClick={() => setI(i + 1)}>
          Next
        </Button>
      </ButtonGroup>
      <dl className="m-0 grid grid-cols-[auto_1fr] gap-x-6 gap-y-1 text-[14px]">
        <dt className="text-kl-ink-3">Order</dt>
        <dd className="m-0 font-kl-mono">{order.id}</dd>
        <dt className="text-kl-ink-3">Client</dt>
        <dd className="m-0">{order.client}</dd>
        <dt className="text-kl-ink-3">Paper</dt>
        <dd className="m-0">{order.paper}</dd>
        <dt className="text-kl-ink-3">Sales rep</dt>
        <dd className="m-0">{order.rep}</dd>
      </dl>
    </div>
  )
}

Installation

npx shadcn@latest add @keyline-ui/button-group

Run it where your components.json is. The first time, map the @keyline-ui registry: one command, in Installation.

Usage

Children are cells: a Button with layout="cell", a ButtonGroupText, or a menu trigger rendering a cell button.

import { Button } from "@/components/keyline/ui/button"
import { ButtonGroup, ButtonGroupText } from "@/components/keyline/ui/button-group"
<ButtonGroup aria-label="Today’s orders">
  <Button layout="cell">Previous</Button>
  <ButtonGroupText>Order 1 of 5</ButtonGroupText>
  <Button layout="cell">Next</Button>
</ButtonGroup>

Examples

Text and separator

ButtonGroupText holds a value between steppers. ButtonGroupSeparator draws a heavier seam where one run of cells ends and the next begins.

Loading the live example…

"use client"

import { useState } from "react"
import { Button } from "@/components/keyline/ui/button"
import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText } from "@/components/keyline/ui/button-group"

export function ReportZoom() {
  const [zoom, setZoom] = useState(100)

  return (
    <div className="grid gap-4">
      <ButtonGroup aria-label="Zoom the quarterly sales report" className="justify-self-start">
        <Button layout="cell" size="icon" aria-label="Zoom out" disabled={zoom <= 50} onClick={() => setZoom(zoom - 25)}>
          −
        </Button>
        <ButtonGroupText className="min-w-[7ch] justify-center tabular-nums">
          <bdi>{zoom}%</bdi>
        </ButtonGroupText>
        <Button layout="cell" size="icon" aria-label="Zoom in" disabled={zoom >= 200} onClick={() => setZoom(zoom + 25)}>
          +
        </Button>
        <ButtonGroupSeparator />
        <Button layout="cell" disabled={zoom === 100} onClick={() => setZoom(100)}>
          Actual size
        </Button>
      </ButtonGroup>
      <p role="status" className="m-0 text-[14px] text-kl-ink-2">
        Q3 sales report, Scranton branch · shown at {zoom}%
      </p>
    </div>
  )
}

With a menu

A MenuTrigger that renders a cell button joins the group; its menu docks to that cell.

Loading the live example…

"use client"

import { ChevronDown } from "lucide-react"
import { useState } from "react"
import { Button } from "@/components/keyline/ui/button"
import { ButtonGroup } from "@/components/keyline/ui/button-group"
import { Menu, MenuContent, MenuItem, MenuTrigger } from "@/components/keyline/ui/menu"

export function TrackingLink() {
  const [sent, setSent] = useState<string | null>(null)

  return (
    <div className="grid gap-4">
      <ButtonGroup aria-label="Tracking link for DM-4087" className="justify-self-start">
        <Button layout="cell" onClick={() => setSent("Link copied: dundermifflin.com/track/DM-4087")}>
          Copy tracking link
        </Button>
        <Menu>
          <MenuTrigger render={<Button layout="cell" aria-label="Send the tracking link" />}>
            Send
            <ChevronDown size={16} aria-hidden="true" />
          </MenuTrigger>
          <MenuContent align="end">
            <MenuItem onClick={() => setSent("Emailed to the Lackawanna County clerk.")}>Email the client</MenuItem>
            <MenuItem onClick={() => setSent("Texted to Jim Halpert.")}>Text the sales rep</MenuItem>
            <MenuItem onClick={() => setSent("Printed at reception. Pam has it.")}>Print for reception</MenuItem>
          </MenuContent>
        </Menu>
      </ButtonGroup>
      <p role="status" className="m-0 min-h-5 text-[14px] text-kl-ink-2">
        {sent ?? "Order DM-4087 left the warehouse at 10:15."}
      </p>
    </div>
  )
}

Props

PropTypeDefaultDescription
orientation"horizontal" | "vertical"Default "horizontal"Vertical stacks the cells on horizontal seams.
aria-labelstringDefault —The group is role="group"; name it for what the buttons act on.

Parts: ButtonGroup, ButtonGroupText, ButtonGroupSeparator. Other div props pass through.

Sheet 8 of 73PreviousButtonNextSplit button
On this page