Next.js Bootstrap 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.
A button lets people do something on a page, or go to another one. AdminKit for Next.js has no Button component. You write Bootstrap's .btn classes in JSX, on a <button> or on a next/link link. Buttons are in the free and PRO versions.
Where a button's code goes depends on what it does. In the App Router, pages are server components: they render on the server, with no state and no event handlers. A plain button or a link renders fine there, so a page can hold it with no "use client". A button that reacts to a click, such as a loading or toggle button, needs a handler in the browser. It goes in a client component, a file that starts with "use client".
Example
<button type="button" className="btn btn-primary me-2 mb-2">Primary</button>
<button type="button" className="btn btn-secondary me-2 mb-2">Secondary</button>
<button type="button" className="btn btn-success me-2 mb-2">Success</button>
<button type="button" className="btn btn-danger me-2 mb-2">Danger</button>
<button type="button" className="btn btn-warning me-2 mb-2">Warning</button>
<button type="button" className="btn btn-info me-2 mb-2">Info</button>
<button type="button" className="btn btn-light me-2 mb-2">Light</button>
<button type="button" className="btn btn-dark me-2 mb-2">Dark</button>
<button type="button" className="btn btn-link me-2 mb-2">Link</button>JSX drops the line breaks between inline elements, so the buttons above are spaced with me-2. The buttons demo page, src/app/(dashboard)/ui-buttons/page.tsx, renders its rows from a list of colors instead. It puts a {" "} after each button to keep the gap, and it stays a server component with its metadata export. In short:
import { Fragment } from "react";
import { pageMetadata } from "@/lib/site";
export const metadata = pageMetadata("/ui-buttons");
const colors = ["primary", "secondary", "success", "danger", "warning", "info"];
export default function Page() {
return (
<div className="mb-3">
{colors.map((color) => (
<Fragment key={color}>
<button type="button" className={`btn btn-${color}`}>
{color}
</button>{" "}
</Fragment>
))}
</div>
);
}Classes
| Class | Effect |
|---|---|
btn | The base class, on every button |
btn-primary, btn-secondary, btn-success, btn-danger, btn-warning, btn-info, btn-light, btn-dark | A filled theme color |
btn-outline-<color> | Border and text in the color, filled on hover |
btn-link | A button styled as a link |
btn-sm, btn-lg | Smaller or larger |
btn-facebook, btn-twitter, btn-google, btn-github, ... | Brand colors, from $social-colors in src/scss/1-variables/_app.scss |
active | The pressed look of a toggle button |
The button styles come from Bootstrap and src/scss/3-components/_buttons.scss. In AdminKit, .btn-warning has dark text, since white text does not reach a 4.5:1 contrast on it. For the same reason, the brand colors are darkened where white text falls short of 4.5:1.
Outline buttons and sizes
Outline buttons carry less weight than filled ones, so they fit a secondary action. btn-sm and btn-lg make a button smaller or larger.
<button type="button" className="btn btn-outline-primary me-2 mb-2">Primary</button>
<button type="button" className="btn btn-outline-secondary me-2 mb-2">Secondary</button>
<button type="button" className="btn btn-outline-success me-2 mb-2">Success</button>
<button type="button" className="btn btn-primary btn-sm me-2">Small</button>
<button type="button" className="btn btn-primary me-2">Medium</button>
<button type="button" className="btn btn-primary btn-lg">Large</button>Loading state
Show a loading state when a click starts a request, so the user knows to wait.
A button with an onClick needs a client component, because a server component cannot pass a function to the browser. AdminKit for Next.js is also a static export (output: "export"): the build writes plain HTML files, with no Next.js server behind them. So there are no Server Actions, and the button calls your API with fetch:
The spinner in this button is only available in AdminKit PRO.
"use client";
import { useState } from "react";
export function SaveButton() {
const [saving, setSaving] = useState(false);
async function save() {
setSaving(true);
try {
await fetch("/api/settings", { method: "POST" });
} finally {
setSaving(false);
}
}
return (
<button type="button" className="btn btn-primary" disabled={saving} onClick={save}>
{saving && <span className="spinner-border spinner-border-sm me-2" aria-hidden="true" />}
{saving ? "Saving..." : "Save changes"}
</button>
);
}The page imports SaveButton and stays a server component. The other spinner styles are on the Spinners page.
Toggle buttons
A toggle button turns something on or off and shows which it is. It holds its pressed state, so it is a client component too. cx from @/lib/cx adds active only when the state is true:
"use client";
import { useState } from "react";
import { cx } from "@/lib/cx";
export function PinButton() {
const [pinned, setPinned] = useState(false);
return (
<button
type="button"
className={cx("btn btn-outline-primary", pinned && "active")}
aria-pressed={pinned}
onClick={() => setPinned(!pinned)}
>
{pinned ? "Pinned" : "Pin to dashboard"}
</button>
);
}Icons and links
Icons from lucide-react work in server components, so adding one does not change where a button lives. Inside a .btn, the .lucide svg is 14px. A button with only an icon needs an aria-label to give it a name:
import { Download, Trash } from "lucide-react";
<button type="button" className="btn btn-primary me-2">
<Download /> Export
</button>
<button type="button" className="btn btn-danger" aria-label="Delete">
<Trash />
</button>When a button opens another page, make it a next/link Link with the .btn classes. Link prefetches the page, and it needs no client component:
import Link from "next/link";
<Link href="/pages-projects" className="btn btn-primary">
View projects
</Link>Rounded and square buttons
These classes are only available in AdminKit PRO.
Two classes change the corners to fit the design around the button. btn-pill gives a button round ends. btn-square takes its border radius away.
<button type="button" className="btn btn-pill btn-primary me-2">Rounded</button>
<button type="button" className="btn btn-square btn-primary">Square</button>Accessibility
- Set
typeon every<button>. A button without it submits the form it is in. - Icon-only buttons need an
aria-label, as in the buttons demo. - Toggle buttons set
aria-pressedfrom their state. - A loading button keeps a visible text, such as "Saving...", and its spinner has
aria-hidden="true". - Prefer
<button disabled>to a disabled link, which needsaria-disabled="true"andtabIndex={-1}as well.
Related
- React Bootstrap buttons: the same markup in AdminKit for React (Vite)
- Bootstrap 5 buttons: the buttons of the HTML version
- Button group: joined buttons and toolbars
- Buttons demo
Last updated on
Badge
Next.js Bootstrap badge markup that renders in server components: theme and light colors, pills, status badges from data and counters on buttons.
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.