Next.js Bootstrap Tables
Next.js Bootstrap table markup rendered on the server: rows from an array with map and key, striped and hover tables, row colors, tables in cards.
Tables show rows of data that people scan and compare, such as clients or orders. In AdminKit for Next.js, a table is Bootstrap's .table markup in JSX: you map an array to rows, and there is no table component to import. Tables are part of the free and PRO versions.
A table needs no "use client". Your page is a server component, so it renders the table on the server, and the rows end up in the static HTML of the page. No JavaScript has to run before people can read them.
For sorting, search and paging, AdminKit PRO has the Data table plugin.
Example
import { pageMetadata } from "@/lib/site";
export const metadata = pageMetadata("/tables-bootstrap");
const people = [
{ id: 1, name: "Vanessa Tucker", phone: "864-348-0485", birthday: "June 21, 1961" },
{ id: 2, name: "William Harris", phone: "914-939-2458", birthday: "May 15, 1948" },
{ id: 3, name: "Sharon Lessman", phone: "704-993-5435", birthday: "September 14, 1965" },
];
export default function Page() {
return (
<table className="table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Phone number</th>
<th scope="col">Date of birth</th>
</tr>
</thead>
<tbody>
{people.map((person) => (
<tr key={person.id}>
<td>{person.name}</td>
<td>{person.phone}</td>
<td>{person.birthday}</td>
</tr>
))}
</tbody>
</table>
);
}Give each row a key from the data. Always write <thead> and <tbody>. A <tr> directly in a <table> is invalid HTML, and the browser corrects it when it reads the page. When React then takes over that HTML in the browser (hydration), it finds markup it did not render, and Next.js reports a hydration error.
Classes
| Class | Effect |
|---|---|
table | The base class |
table-striped | Zebra stripes on the body rows |
table-hover | A hover color on body rows |
table-bordered, table-borderless | Borders on every side, or none |
table-sm | Cells with half the padding |
table-<color> | A row or cell in a theme color, such as table-primary |
table-responsive | On a wrapper: horizontal scrolling for a wide table |
table-action | AdminKit: muted icon links with 18px icons |
AdminKit centers body cells vertically, and it adapts tables to cards. A .table placed directly in a .card, or in a .table-responsive in a card, loses its outer borders and bottom margin. Its first and last cells line up with the card padding. These rules and the .table-action style are in src/scss/3-components/_tables.scss.
In a card, with actions
Dashboard tables usually sit in a card and end with a column of actions. On the tables demo, each table follows the .card-header of its card, and a .table-action cell holds the edit and delete links. The lucide-react icons render on the server too:
import { Pen, Trash } from "lucide-react";
<div className="card">
<div className="card-header">
<h2 className="card-title mb-0">Clients</h2>
</div>
<table className="table table-striped table-hover">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Phone number</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{clients.map((client) => (
<tr key={client.id} className={client.vip ? "table-success" : undefined}>
<td>{client.name}</td>
<td>{client.phone}</td>
<td className="table-action">
<a href="#" aria-label={`Edit ${client.name}`}>
<Pen className="align-middle" />
</a>{" "}
<a href="#" aria-label={`Delete ${client.name}`}>
<Trash className="align-middle" />
</a>
</td>
</tr>
))}
</tbody>
</table>
</div>Here the row color comes from the data. For clients that are not VIPs, className={client.vip ? "table-success" : undefined} leaves the attribute out.
Rows fetched in the browser
Rows you know at build time belong in the server component. Rows from your API are different. In the static export they arrive after the page loads, so a client component fetches them and holds them in state. While there are none, render one row that spans all the columns:
"use client";
import { useEffect, useState } from "react";
type Order = { id: string; number: string; customer: string; total: string };
export function OrdersTable() {
const [orders, setOrders] = useState<Order[] | null>(null);
useEffect(() => {
fetch("/api/orders")
.then((response) => response.json())
.then(setOrders);
}, []);
return (
<table className="table">
<thead>
<tr>
<th scope="col">Order</th>
<th scope="col">Customer</th>
<th scope="col" className="text-end">Total</th>
</tr>
</thead>
<tbody>
{orders === null || orders.length === 0 ? (
<tr>
<td colSpan={3} className="text-center text-body-secondary">
{orders === null ? "Loading orders..." : "No orders yet"}
</td>
</tr>
) : (
orders.map((order) => (
<tr key={order.id}>
<td>{order.number}</td>
<td>{order.customer}</td>
<td className="text-end">{order.total}</td>
</tr>
))
)}
</tbody>
</table>
);
}In JSX, colspan is written colSpan. In AdminKit PRO, rows of placeholders can replace the loading text.
Responsive tables
When a table is wider than the screen, put it in a .table-responsive wrapper, so it scrolls sideways on its own. The demo makes the wrapper focusable and names it after the card title:
<div className="card">
<div className="card-header">
<h2 className="card-title mb-0" id="usage-title">Usage by region</h2>
</div>
<div className="table-responsive" role="region" tabIndex={0} aria-labelledby="usage-title">
<table className="table mb-0">{/* ... */}</table>
</div>
</div>The demo hides the date of birth column on small screens with d-none d-md-table-cell on its <th> and <td> cells.
Accessibility
- Column headings go in
<th scope="col">, row headings in<th scope="row">. - Name icon-only links after their row, such as "Edit Vanessa Tucker".
- A scrolling
.table-responsivehasrole="region",tabIndex={0}and a name, so keyboard users can reach and scroll it. - A colored row needs a word too, such as "Overdue"; the color alone is not enough.
Related
- React Bootstrap tables: the same markup in AdminKit for React (Vite)
- Bootstrap 5 tables: the tables of the HTML version
- Data table: sorting, search and paging (PRO)
- Tables demo
Last updated on
SpinnersPro
Next.js Bootstrap spinner markup in AdminKit PRO: border and grow spinners in server components, client-side loading states and where loading.tsx fits.
ToastsPro
Next.js toast notifications for the App Router: a ToastProvider in the root layout and a useToast() hook for client components, built on Bootstrap's toast.