Empty State
v0.3.0A 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.
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";45export 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}1516/**17 * Generic centered placeholder for empty lists/grids/results: icon, a18 * description, and an optional action. Modeled on AntD's `Empty` — reusable19 * 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 ...rest27}: EmptyStateProps) => {28 const [rootClass, getChildClass] = getClassName({29 className,30 rootClass: "BHT__EmptyState",31 styles,32 });3334 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);1415 &__icon {16 font-size: 2.5rem;17 line-height: 1;18 opacity: 0.4;19 }2021 &__description {22 margin: 0;23 }2425 &__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";23export const BasicEmptyState = () => (4 <EmptyState description="No blocks yet." />5);67export const EmptyStateWithIcon = () => (8 <EmptyState icon="🧩" description="No blocks yet. Add one to get started." />9);1011export 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";34describe("EmptyState", () => {5 it("renders the description", () => {6 render(<EmptyState description="No blocks yet." />);7 expect(screen.getByText("No blocks yet.")).toBeTruthy();8 });910 it("applies additional className", () => {11 const { container } = render(12 <EmptyState className="custom" description="Empty" />,13 );14 expect(container.firstElementChild?.className).toContain("custom");15 });1617 it("renders no icon element when icon is omitted", () => {18 const { container } = render(<EmptyState description="Empty" />);19 expect(container.querySelector('[class*="icon"]')).toBeNull();20 });2122 it("renders the icon when provided", () => {23 render(<EmptyState icon="🧩" description="Empty" />);24 expect(screen.getByText("🧩")).toBeTruthy();25 });2627 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});