AdminKitDocs
Components

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:

src/app/(dashboard)/ui-buttons/page.tsx
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

ClassEffect
btnThe base class, on every button
btn-primary, btn-secondary, btn-success, btn-danger, btn-warning, btn-info, btn-light, btn-darkA filled theme color
btn-outline-<color>Border and text in the color, filled on hover
btn-linkA button styled as a link
btn-sm, btn-lgSmaller or larger
btn-facebook, btn-twitter, btn-google, btn-github, ...Brand colors, from $social-colors in src/scss/1-variables/_app.scss
activeThe 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:

Pro

The spinner in this button is only available in AdminKit PRO.

src/components/save-button.tsx
"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:

src/components/pin-button.tsx
"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 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

Pro

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 type on 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-pressed from 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 needs aria-disabled="true" and tabIndex={-1} as well.

Last updated on

On this page