Next.js Bootstrap Placeholders
Next.js Bootstrap placeholders in AdminKit PRO: loading cards in the static HTML, swapped for data fetched in a client component, with sizes and animation.
This component is only available in AdminKit PRO. Learn more.
While a card waits for its data, placeholders fill it with gray blocks in the shape of its text and buttons. The page has its final layout right away, and nothing jumps when the content arrives. Placeholders are Bootstrap's .placeholder classes in JSX, so there is no component to import. They are part of the PRO version of AdminKit for Next.js.
The work splits in two. The placeholder markup has no state, so its file needs no "use client", and a server component page can render it. Swapping it for the real data needs state, so that part goes in a client component, a file that starts with "use client".
For charts, maps and plugins, AdminKit uses the Skeleton component instead.
Example
The placeholders demo puts a card next to its loading version, with the same size and layout:
<div className="row">
<div className="col-6">
<div className="card">
<div className="card-body">
<h2 className="card-title">Website redesign</h2>
<p className="card-text">
New landing pages and a lighter checkout, due at the end of the quarter.
</p>
<a href="#" className="btn btn-primary">Open project</a>
</div>
</div>
</div>
<div className="col-6">
<div className="card" aria-hidden="true">
<div className="card-body">
<h2 className="card-title placeholder-glow">
<span className="placeholder col-6" />
</h2>
<p className="card-text placeholder-glow">
<span className="placeholder col-7" />{" "}
<span className="placeholder col-4" />{" "}
<span className="placeholder col-4" />{" "}
<span className="placeholder col-6" />
</p>
<a
href="#"
tabIndex={-1}
className="btn btn-primary disabled placeholder col-6"
aria-disabled="true"
/>
</div>
</div>
</div>
</div>{" "} between the spans keeps the spaces that HTML has between them, so they wrap like words. Empty elements close themselves in JSX: <span className="placeholder col-6" />.
Classes
| Class | Effect |
|---|---|
placeholder | A block in the current text color, at reduced opacity |
col-<n>, w-25 to w-100 | The width; or style={{ width: "25%" }} |
placeholder-lg, placeholder-sm, placeholder-xs | The height; otherwise it follows the parent's font size |
bg-<color> | A color other than the text color |
placeholder-glow, placeholder-wave | On the parent: a pulsing or a sweeping animation |
A .btn.placeholder keeps the button height. Add disabled and tabIndex={-1} to keep it from being clicked or focused.
Swapped for data from the browser
A static export has no server at request time. Data that changes after the build is therefore fetched in the browser. Put the two halves in two files. The placeholder file needs no "use client". The client component that fetches the data imports it, renders it first and then shows the data:
export function ProjectCardPlaceholder() {
return (
<div className="card" aria-hidden="true">
<div className="card-body">
<h2 className="card-title placeholder-glow">
<span className="placeholder col-6" />
</h2>
<p className="card-text placeholder-glow">
<span className="placeholder col-7" /> <span className="placeholder col-4" />
</p>
</div>
</div>
);
}"use client";
import { useEffect, useState } from "react";
import { ProjectCardPlaceholder } from "@/components/project-card-placeholder";
type Project = { name: string; summary: string };
export function ProjectCard({ id }: { id: string }) {
const [project, setProject] = useState<Project | null>(null);
useEffect(() => {
fetch(`/api/projects/${id}`)
.then((response) => response.json())
.then(setProject);
}, [id]);
return (
<div aria-busy={project === null}>
{project === null ? (
<ProjectCardPlaceholder />
) : (
<div className="card">
<div className="card-body">
<h2 className="card-title">{project.name}</h2>
<p className="card-text">{project.summary}</p>
</div>
</div>
)}
</div>
);
}The first render shows the placeholder, and that is what the static HTML of the page contains. So the page has its final layout before any JavaScript runs. When the browser takes over the HTML (hydration), it renders the same placeholder, so the two match.
For a list or a table, render a fixed number of placeholder rows with Array.from({ length: 3 }, (_, index) => ...), keyed by the index.
Sizes, colors and animation
A placeholder takes its height from a size class, its color from a bg- class and its animation from the parent:
<>
<p className="placeholder-glow mb-2"><span className="placeholder col-12 placeholder-lg" /></p>
<p className="placeholder-glow mb-2"><span className="placeholder col-12" /></p>
<p className="placeholder-glow mb-2"><span className="placeholder col-12 placeholder-sm bg-primary" /></p>
<p className="placeholder-wave mb-0"><span className="placeholder col-12 placeholder-xs bg-success" /></p>
</>Placeholders or Skeleton
- Placeholders copy the layout of markup you write yourself: cards, lists, table rows, profile headers.
- Skeleton (
@/components/ui/skeleton) stands in for content a library draws in the browser, such as a chart or a map. The plugin wrappers show it until they have drawn.
Accessibility
aria-hidden="true"hides the placeholder version from screen readers. Its blocks have no text to read.aria-busyon the region tells assistive technology that its content is still loading.- Placeholder buttons get
tabIndex={-1}andaria-disabled="true", so the keyboard skips them.
Related
- React Bootstrap placeholders: the same markup in AdminKit for React (Vite)
- Bootstrap 5 placeholders: the placeholders of the HTML version
- Placeholders demo: a card next to its loading version, in every theme color
Last updated on
Offcanvas
A Next.js offcanvas client component for the App Router: Bootstrap side panels in the static HTML, a backdrop added after hydration, four placements.
PopoversPro
A Next.js Bootstrap popover for App Router client components: a title and body that open on click beside an element and render only in the browser.