← All components

Empty

The first screen of a place: what will live here, and the one button that starts it.

No projects yet

A project holds the files, notes and people for one piece of work. Start one and invite who you need.

The idea

Empty states teach, for the reader who can fill the place — so they say what it will hold and hand over the one action that begins it, never a shrug. Content only an owner would have is the opposite case: when it is empty it should not render at all, because showing a visitor an empty state advertises a gap they did not need to see. It is not a box; a dashed border round an absence only points at the absence.

Composes Button.

Install

terminal1 line
npm i mihirmodi-ui motion

The tokens, the theme script and the routing provider are a one-time job — see Setup. Or skip the package entirely and take the source at the foot of this page.

Usage

Empty — usage11 lines
import { Button, Empty } from "mihirmodi-ui";

// For the reader who can fill this place. Owner-only content that is
// empty for a visitor should not render at all.
<Empty
  mark={<FolderIcon />}
  title="No projects yet"
  action={<Button variant="primary" onClick={create}>New project</Button>}
>
  A project holds the files, notes and people for one piece of work.
</Empty>

Props

PropNotes
markReactNodeA small glyph on a hairline tile.
titleReactNodeWhat is not here yet, plainly.
childrenReactNodeOne sentence on what this place holds and how it gets there.
actionReactNodeUsually one primary Button.
secondaryReactNodeA quieter way out — a link to the docs.

Source

Exactly what the package ships — this is read out of the source at build time, not a copy. Take it and adapt it if you would rather not add a dependency.

packages/ui/src/components/layout/Empty.tsx55 lines
import type { ReactNode } from "react";

export type EmptyProps = {
  /** A small glyph that names the kind of thing missing — a folder, an inbox. */
  mark?: ReactNode;
  /** What is not here yet, as a plain statement: "No projects yet". */
  title: ReactNode;
  /** One sentence on what this place will hold and how it gets there. */
  children?: ReactNode;
  /** The way out — usually one primary `Button` that makes the first one. */
  action?: ReactNode;
  /** A quieter second way out, under the action — a link to the docs. */
  secondary?: ReactNode;
  className?: string;
};

/**
 * What a place shows before anything is in it.
 *
 * ── Empty states teach; gated content just doesn't render ──────────────────
 *
 * An empty state is for a reader who can fill the place: the owner of a new
 * workspace, someone who has not saved anything yet. For them it is the most
 * useful screen in the product — it says what will live here and hands them
 * the one button that starts it, so it is never a shrug like "Nothing here".
 * For everyone else it is noise. Content that only an owner would have —
 * a "your drafts" section on a public profile, an admin panel's empty log —
 * should not render at all when it is empty. Showing a visitor an empty state
 * tells them about a feature they cannot use and a gap they did not need to
 * see. Gate it; do not explain it.
 *
 * It is not a box. A dashed border round an empty area draws attention to
 * the absence; the mark, two lines and a button are enough, centred in the
 * space the content will take. The mark sits on a small hairline tile so it
 * reads as an object rather than a stray icon, and the title separates from
 * the sentence on weight, not size.
 */
export function Empty({ mark, title, children, action, secondary, className = "" }: EmptyProps) {
  return (
    <div className={`flex flex-col items-center px-6 py-10 text-center ${className}`}>
      {mark && (
        <span
          aria-hidden
          className="mb-4 flex h-10 w-10 items-center justify-center rounded-[12px] border-[0.5px] border-gray-alpha-400 bg-background-100 text-muted shadow-[0_1px_1px_rgba(0,0,0,0.02),0_4px_8px_-2px_rgba(0,0,0,0.04)]"
        >
          {mark}
        </span>
      )}
      <p className="text-[14px] font-medium leading-[1.4] text-gray-1000">{title}</p>
      {children && <p className="mt-1 max-w-[320px] text-[13px] leading-[1.55] text-muted">{children}</p>}
      {action && <div className="mt-5 flex items-center gap-2">{action}</div>}
      {secondary && <div className="mt-3 text-[12px] text-faint">{secondary}</div>}
    </div>
  );
}