Behive Tech Registry

Empty State

v0.3.0

A generic centered placeholder for empty lists, grids, or search results: an optional icon, a description, and an optional action. Modeled on AntD's `Empty` — reusable anywhere something has nothing to show yet.

pnpm add @behivetech/molecules.empty-state

registry/behivetech/molecules/empty-state · depends on @behivetech/get-class-name

empty-state.tsx
1import { type HTMLAttributes, type ReactNode } from "react";
2import { getClassName } from "@behivetech/get-class-name";
3import styles from "./empty-state.module.scss";
4
5export interface EmptyStateProps extends HTMLAttributes<HTMLDivElement> {
6 /** Additional class names to merge with the component root element */
7 className?: string;
8 /** Icon or illustration shown above the description */
9 icon?: ReactNode;
10 /** Message explaining the empty state */
11 description: ReactNode;
12 /** Action content rendered below the description (typically a Button) */
13 children?: ReactNode;
14}
15
16/**
17 * Generic centered placeholder for empty lists/grids/results: icon, a
18 * description, and an optional action. Modeled on AntD's `Empty` — reusable
19 * anywhere something has nothing to show yet, not tied to any one feature.
20 */
21export const EmptyState = ({
22 className,
23 icon,
24 description,
25 children,
26 ...rest
27}: EmptyStateProps) => {
28 const [rootClass, getChildClass] = getClassName({
29 className,
30 rootClass: "BHT__EmptyState",
31 styles,
32 });
33
34 return (
35 <div {...rest} className={rootClass}>
36 {icon !== undefined && (
37 <div className={getChildClass("icon")}>{icon}</div>
38 )}
39 <p className={getChildClass("description")}>{description}</p>
40 {children !== undefined && (
41 <div className={getChildClass("action")}>{children}</div>
42 )}
43 </div>
44 );
45};
empty-state.module.scss
1.BHT__EmptyState {
2 display: flex;
3 flex-direction: column;
4 align-items: center;
5 justify-content: center;
6 gap: var(--spacing-4);
7 text-align: center;
8 color: var(--md-sys-color-on-surface-variant, var(--color-text-muted));
9 font-family: var(--md-sys-typescale-body-medium-font, var(--font-body));
10 font-size: var(--md-sys-typescale-body-medium-size, 0.875rem);
11 font-weight: var(--md-sys-typescale-body-medium-weight, 400);
12 line-height: var(--md-sys-typescale-body-medium-line-height, 1.25rem);
13 letter-spacing: var(--md-sys-typescale-body-medium-tracking, 0.0179em);
14
15 &__icon {
16 font-size: 2.5rem;
17 line-height: 1;
18 opacity: 0.4;
19 }
20
21 &__description {
22 margin: 0;
23 }
24
25 &__action {
26 display: inline-flex;
27 }
28}
index.ts
1export { EmptyState } from "./empty-state.js";
2export type { EmptyStateProps } from "./empty-state.js";
empty-state.composition.tsx
1import { EmptyState } from "./empty-state.js";
2
3export const BasicEmptyState = () => (
4 <EmptyState description="No blocks yet." />
5);
6
7export const EmptyStateWithIcon = () => (
8 <EmptyState icon="🧩" description="No blocks yet. Add one to get started." />
9);
10
11export const EmptyStateWithAction = () => (
12 <EmptyState icon="🧩" description="No blocks yet. Add one to get started.">
13 <button onClick={() => {}}>+ Add Block</button>
14 </EmptyState>
15);
empty-state.spec.tsx
1import { render, screen } from "@testing-library/react";
2import { EmptyState } from "./empty-state.js";
3
4describe("EmptyState", () => {
5 it("renders the description", () => {
6 render(<EmptyState description="No blocks yet." />);
7 expect(screen.getByText("No blocks yet.")).toBeTruthy();
8 });
9
10 it("applies additional className", () => {
11 const { container } = render(
12 <EmptyState className="custom" description="Empty" />,
13 );
14 expect(container.firstElementChild?.className).toContain("custom");
15 });
16
17 it("renders no icon element when icon is omitted", () => {
18 const { container } = render(<EmptyState description="Empty" />);
19 expect(container.querySelector('[class*="icon"]')).toBeNull();
20 });
21
22 it("renders the icon when provided", () => {
23 render(<EmptyState icon="🧩" description="Empty" />);
24 expect(screen.getByText("🧩")).toBeTruthy();
25 });
26
27 it("renders action children below the description", () => {
28 render(
29 <EmptyState description="Empty">
30 <button>+ Add Block</button>
31 </EmptyState>,
32 );
33 expect(screen.getByRole("button", { name: "+ Add Block" })).toBeTruthy();
34 });
35});