Toggle group
Independent toggles in one frame, each with its lamp. When the row runs out of room it scrolls inside the frame and shades the edge that hides items.
StandardNarrow
010020030040050060070080090010001100120013001400150016001700180019002000
Loading the live example…
"use client"
import { useState } from "react"
import { ToggleGroup, ToggleGroupItem } from "@/components/keyline/ui/toggle-group"
const ORDERS = [
{ id: "DM-4102", client: "Scranton Business Park", rep: "Dwight" },
{ id: "DM-4087", client: "Lackawanna County", rep: "Jim" },
{ id: "DM-4120", client: "Wilkes-Barre Library", rep: "Phyllis" },
{ id: "DM-4131", client: "Blue Cross of Pennsylvania", rep: "Stanley" },
{ id: "DM-4133", client: "Pennsylvania Power & Light", rep: "Dwight" },
{ id: "DM-4136", client: "Harper Collins, Scranton", rep: "Andy" },
{ id: "DM-4140", client: "Vance Refrigeration", rep: "Phyllis" },
]
const REPS = ["Dwight", "Jim", "Phyllis", "Stanley", "Andy"]
export function OrdersByRep() {
const [reps, setReps] = useState<string[]>(["Dwight", "Phyllis"])
// no rep chosen means no filter: every order shows
const rows = reps.length ? ORDERS.filter((o) => reps.includes(o.rep)) : ORDERS
return (
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)] gap-4">
<ToggleGroup aria-label="Sales rep" value={reps} onValueChange={setReps} className="justify-self-start">
{REPS.map((r) => (
<ToggleGroupItem key={r} value={r} count={ORDERS.filter((o) => o.rep === r).length}>
{r}
</ToggleGroupItem>
))}
</ToggleGroup>
<ul className="m-0 grid list-none gap-1 p-0 text-[14px]" aria-label="Open orders">
{rows.map((o) => (
<li key={o.id} className="truncate">
<span className="font-kl-mono whitespace-nowrap">{o.id}</span> · {o.client}
</li>
))}
</ul>
<p role="status" className="m-0 text-[14px] text-kl-ink-2">
{reps.length ? `${rows.length} open orders for ${reps.join(", ")}.` : `All ${ORDERS.length} open orders.`}
</p>
</div>
)
}Installation
npx shadcn@latest add @keyline-ui/toggle-groupRun it where your components.json is. The first time, map the @keyline-ui registry: one command, in Installation.
Usage
import { ToggleGroup, ToggleGroupItem } from "@/components/keyline/ui/toggle-group"<ToggleGroup aria-label="Sales rep" value={reps} onValueChange={setReps}>
<ToggleGroupItem value="Dwight" count={2}>Dwight</ToggleGroupItem>
<ToggleGroupItem value="Jim" count={1}>Jim</ToggleGroupItem>
</ToggleGroup>Examples
In a panel
With layout="cell" the group sits in a panel header and shares its keylines. Turn both filters on to see what the panel says when nothing matches.
Loading the live example…
"use client"
import { useState } from "react"
import { Panel, PanelBody, PanelHeader } from "@/components/keyline/ui/panel"
import { ToggleGroup, ToggleGroupItem } from "@/components/keyline/ui/toggle-group"
const STOCK = [
{ item: "20 lb copy paper", cases: 4, low: true, onOrder: false },
{ item: "Cardstock, ivory", cases: 31, low: false, onOrder: false },
{ item: "Legal pads, yellow", cases: 2, low: true, onOrder: false },
{ item: "Toner, HP 26A", cases: 9, low: false, onOrder: true },
]
export function SupplyRoom() {
const [show, setShow] = useState<string[]>([])
const rows = STOCK.filter((s) => (!show.includes("low") || s.low) && (!show.includes("order") || s.onOrder))
return (
<Panel className="max-w-[520px]">
<PanelHeader
title="Supply room"
actions={
<ToggleGroup layout="cell" aria-label="Show" value={show} onValueChange={setShow}>
<ToggleGroupItem value="low">Low stock</ToggleGroupItem>
<ToggleGroupItem value="order">On order</ToggleGroupItem>
</ToggleGroup>
}
/>
<PanelBody>
{rows.length ? (
<ul className="m-0 grid list-none gap-1 p-0 text-[14px]" aria-label="Stock">
{rows.map((s) => (
<li key={s.item} className="grid grid-cols-[minmax(0,1fr)_auto] gap-x-4">
<span className="truncate">{s.item}</span>
<span className="tabular-nums text-kl-ink-2">{s.cases} cases</span>
</li>
))}
</ul>
) : (
<p className="m-0 text-[14px] text-kl-ink-2">
Nothing low is on order yet: {STOCK.filter((s) => s.low).length} items need a reorder. Turn off On order to see them.
</p>
)}
</PanelBody>
</Panel>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | string[] | Default — | The pressed items, with onValueChange. Or start with defaultValue. |
multiple | boolean | Default true | Independent toggles. For one choice of many, Segmented reads better than false. |
layout | "control" | "cell" | Default "control" | cell drops the frame, for a group in a panel header. |
count | ReactNode | Default — | On ToggleGroupItem: a count after the label. |
Tab enters the group at one item; arrow keys move between items. Built on Base UI Toggle Group.