AdminKitDocs
Components

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

ClassEffect
btn-groupJoins the buttons in a row
btn-group-verticalJoins them in a column
btn-group-sm, btn-group-lgSizes all the buttons of the group at once
btn-toolbarLays out several groups in a row; space them with me-2
btn-checkHides 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:

src/components/range-switch.tsx
"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>

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 an aria-label. A toolbar has role="toolbar".
  • Every radio or checkbox input has a unique id, and its label refers to it with htmlFor. The arrow keys move between the radios of one name.
  • Toggle buttons without inputs set aria-pressed from state, as on the Buttons page.

Last updated on

On this page