Next.js Bootstrap Button Group
A Next.js Bootstrap button group for the App Router: groups and toolbars in server components, and a radio group with state in a client component.
A button group joins related buttons into one control, so the user sees at a glance that they belong together. In AdminKit for Next.js you write Bootstrap's .btn-group markup around your buttons, in JSX. There is no component to import. Button groups are in the free and PRO versions.
Most groups have no state, and in the App Router that decides where they go. Pages are server components: they render on the server and cannot hold state or handle clicks. A group, a toolbar or a vertical group without state renders in a server component with no "use client". A segmented control that keeps a selected value, such as a radio group, goes in a client component, a file that starts with "use client".
Example
<div className="btn-group" role="group" aria-label="Text alignment">
<button type="button" className="btn btn-secondary">Left</button>
<button type="button" className="btn btn-secondary">Middle</button>
<button type="button" className="btn btn-secondary">Right</button>
</div>Classes
| Class | Effect |
|---|---|
btn-group | Joins the buttons in a row |
btn-group-vertical | Joins them in a column |
btn-group-sm, btn-group-lg | Sizes all the buttons of the group at once |
btn-toolbar | Lays out several groups in a row; space them with me-2 |
btn-check | Hides a checkbox or radio input; its label.btn is the visible button |
Radio group in a client component
Reach for a radio group when exactly one option can be on, such as a date range. It holds the selected value in state and handles onChange, so the file starts with "use client". Each input has an id, and its label points at it with htmlFor:
"use client";
import { Fragment, useState } from "react";
const ranges = ["Day", "Week", "Month"];
export function RangeSwitch() {
const [range, setRange] = useState("Week");
return (
<div className="btn-group" role="group" aria-label="Date range">
{ranges.map((item) => {
const id = `range-${item.toLowerCase()}`;
return (
<Fragment key={item}>
<input
type="radio"
className="btn-check"
name="range"
id={id}
checked={range === item}
onChange={() => setRange(item)}
/>
<label className="btn btn-outline-primary" htmlFor={id}>
{item}
</label>
</Fragment>
);
})}
</div>
);
}The initial value, "Week", is in the static HTML of the page. So the right option is checked before the JavaScript loads. If the value comes from the browser, such as from localStorage, read it in an effect after the first render. That avoids a hydration mismatch: hydration is the browser taking over the HTML that Next.js built, and a mismatch means the browser's first render differs from it.
Toolbar
A toolbar lines up several groups in one row. The buttons demo renders its toolbar from arrays, right in the server component page:
<div className="btn-toolbar" role="toolbar" aria-label="Pages">
<div className="btn-group me-2" role="group" aria-label="First group">
{["1", "2", "3", "4"].map((label) => (
<button key={label} type="button" className="btn btn-secondary">
{label}
</button>
))}
</div>
<div className="btn-group" role="group" aria-label="Second group">
{["5", "6", "7"].map((label) => (
<button key={label} type="button" className="btn btn-secondary">
{label}
</button>
))}
</div>
</div>Sizes and vertical groups
btn-group-sm and btn-group-lg size every button of a group at once. btn-group-vertical stacks the buttons in a column instead of a row.
<div className="btn-group btn-group-sm me-2" role="group" aria-label="Small group">
<button type="button" className="btn btn-secondary">Left</button>
<button type="button" className="btn btn-secondary">Right</button>
</div>
<div className="btn-group-vertical" role="group" aria-label="Vertical group">
<button type="button" className="btn btn-primary">Top</button>
<button type="button" className="btn btn-primary">Middle</button>
<button type="button" className="btn btn-primary">Bottom</button>
</div>Dropdown in a group
A dropdown at the end of a group holds the options that do not fit as buttons. Give Dropdown the btn-group class to make a dropdown button, as on the buttons demo. Dropdown is a client component. A server component page can still render it and pass it static children. Its props are on the Dropdowns page:
import { Dropdown, DropdownMenu, DropdownToggle } from "@/components/bootstrap/dropdown";
<Dropdown className="btn-group">
<DropdownToggle as="button" type="button" className="btn btn-primary dropdown-toggle">
Export
</DropdownToggle>
<DropdownMenu>
<a className="dropdown-item" href="#">CSV</a>
<a className="dropdown-item" href="#">PDF</a>
</DropdownMenu>
</Dropdown>Accessibility
- A group has
role="group"and anaria-label. A toolbar hasrole="toolbar". - Every radio or checkbox input has a unique
id, and itslabelrefers to it withhtmlFor. The arrow keys move between the radios of onename. - Toggle buttons without inputs set
aria-pressedfrom state, as on the Buttons page.
Related
- React Bootstrap button group: the same markup in AdminKit for React (Vite)
- Bootstrap 5 button group: the button group of the HTML version
- Buttons: colors, sizes and states of a single button
- Buttons demo
Last updated on
Buttons
Next.js Bootstrap button markup for the App Router: static buttons in server components, plus loading and toggle buttons in a small client component.
Card
Next.js Bootstrap card markup for server components: header and title, images with asset(), cards mapped from data, next/link cards and CardActions.