# 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](/nextjs/) 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

```html
<button type="button" class="btn btn-primary me-2 mb-2">Primary</button>
<button type="button" class="btn btn-secondary me-2 mb-2">Secondary</button>
<button type="button" class="btn btn-success me-2 mb-2">Success</button>
<button type="button" class="btn btn-danger me-2 mb-2">Danger</button>
<button type="button" class="btn btn-warning me-2 mb-2">Warning</button>
<button type="button" class="btn btn-info me-2 mb-2">Info</button>
<button type="button" class="btn btn-light me-2 mb-2">Light</button>
<button type="button" class="btn btn-dark me-2 mb-2">Dark</button>
<button type="button" class="btn btn-link me-2 mb-2">Link</button>
```
```tsx code
<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:

```tsx title="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

| 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.

```html
<button type="button" class="btn btn-outline-primary me-2 mb-2">Primary</button>
<button type="button" class="btn btn-outline-secondary me-2 mb-2">Secondary</button>
<button type="button" class="btn btn-outline-success me-2 mb-2">Success</button>
<br>
<button type="button" class="btn btn-primary btn-sm me-2">Small</button>
<button type="button" class="btn btn-primary me-2">Medium</button>
<button type="button" class="btn btn-primary btn-lg">Large</button>
```
```tsx code
<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**.

```html
<button type="button" class="btn btn-primary me-2">Save changes</button>
<button type="button" class="btn btn-primary" disabled>
  <span class="spinner-border spinner-border-sm me-2" aria-hidden="true"></span>
  Saving...
</button>
```
```tsx code title="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](/nextjs/docs/components/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:

```html
<button type="button" class="btn btn-outline-primary me-2" data-bs-toggle="button" aria-pressed="false">Pin to dashboard</button>
<button type="button" class="btn btn-outline-primary active" data-bs-toggle="button" aria-pressed="true">Pinned</button>
```
```tsx code title="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 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:

```tsx
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:

```tsx
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.

```html
<button type="button" class="btn btn-pill btn-primary me-2">Rounded</button>
<button type="button" class="btn btn-square btn-primary">Square</button>
```
```tsx code
<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.

## Related

- [React Bootstrap buttons](/react/docs/components/buttons/): the same markup in AdminKit for React (Vite)
- [Bootstrap 5 buttons](/bootstrap/docs/components/buttons/): the buttons of the HTML version
- [Button group](/nextjs/docs/components/button-group/): joined buttons and toolbars
- [Buttons demo](https://nextjs-demo.adminkit.io/ui-buttons)

---

Canonical HTML page: https://adminkit.io/nextjs/docs/components/buttons/

Site overview for agents: https://adminkit.io/llms.txt (expanded: https://adminkit.io/llms-full.txt) · Documentation: https://adminkit.io/bootstrap/docs/ · Questions: support@adminkit.io
