{
  "contract": "tessli.public-library-item.v1",
  "canonicalPath": "/library/empty-state",
  "representations": {
    "json": "/library/empty-state/item.json",
    "markdown": "/library/empty-state/item.md"
  },
  "preview": "/preview/empty-state",
  "item": {
    "slug": "empty-state",
    "title": "Empty state that says why and what next",
    "kind": "component",
    "category": "empty-states",
    "summary": "Replaces a list, table or feed that has nothing to show, and tells people why it is empty and the one thing to do next.",
    "status": "community",
    "install": "npx shadcn@latest add online-scope-lab/tessli/empty-state",
    "registry": {
      "type": "registry:component",
      "registryDependencies": [],
      "dependencies": []
    },
    "notes": {
      "id": "empty-state",
      "kind": "component",
      "category": "empty-states",
      "title": "Empty state that says why and what next",
      "summary": "Replaces a list, table or feed that has nothing to show, and tells people why it is empty and the one thing to do next.",
      "use_when": [
        "A region that normally holds content is empty on first use, after a search or filter, or because loading failed.",
        "There is one clear next action, such as creating, importing, clearing a filter or retrying."
      ],
      "avoid_when": [
        "The whole screen is empty because the user has no access; show a permission message with who to ask instead.",
        "Content is still loading; show a skeleton or progress state, not an empty state.",
        "The empty region is minor (a sidebar list, a small widget); a single muted sentence is enough."
      ],
      "rules": [
        "H-02",
        "S-02",
        "C-02",
        "W-02",
        "ST-01",
        "ST-02",
        "ST-04",
        "A-01"
      ],
      "ai_default": "A large centred illustration or icon, the headline \"Nothing here yet\", a vague line such as \"Get started by creating your first item\", and the same generic message for first use, no search results and failed loads.",
      "why_this_is_better": [
        "The title names what is empty in the user's words (\"No notes in Field research yet\"), so it reads correctly out of context and to screen readers.",
        "First use, no results and error are different states with different copy and actions, because they have different causes.",
        "No results echoes the query and says what the search covered, so people can tell a typo from missing content.",
        "The error state says what failed and what is safe (\"Nothing was deleted\") before offering a retry, and is labelled in text, not only by colour.",
        "It sits where the content would be, left-aligned with the page, instead of a centred illustration that pushes the action below the fold.",
        "One primary action, at most one secondary, both at least 44px tall."
      ],
      "failures": [
        "Adding an illustration that is larger than the message and pushes the action off small screens.",
        "Repeating a primary action that the toolbar above already offers, so two buttons compete for the same job.",
        "Using one generic \"No data\" message for every cause.",
        "Turning the error state into a red banner with no explanation of what is safe or what to do."
      ],
      "states": [
        "first-use",
        "no-results",
        "error"
      ],
      "responsive": {
        "desktop": "Bordered region aligned with the surrounding content; actions sit side by side under the text, which keeps a readable measure.",
        "mobile": "Same order; padding tightens and actions become full-width buttons stacked in priority order."
      },
      "a11y": [
        "A real heading (level set by headingLevel to match the page outline) names the region through aria-labelledby.",
        "The error variant adds a text label above the title, so the state is not conveyed by colour alone.",
        "Actions are native buttons or links at least 44px tall with a visible focus ring.",
        "If the state appears after an action (a search, a failed save), announce it from your own live region; the component does not announce on its own."
      ],
      "status": "community",
      "reviewed_by": [],
      "reviewed_at": null,
      "contributors": [],
      "provenance": "original",
      "license": "MIT"
    },
    "rules": [
      {
        "id": "H-02",
        "statement": "One h1 per page; nest h2–h4 in order. Level follows structure, size follows style.",
        "path": "/rules/H-02"
      },
      {
        "id": "S-02",
        "statement": "Align a section's heading, text and actions on one left edge; centre only short blocks.",
        "path": "/rules/S-02"
      },
      {
        "id": "C-02",
        "statement": "Pair every colour-coded meaning (error, selected, status) with text or shape.",
        "path": "/rules/C-02"
      },
      {
        "id": "W-02",
        "statement": "Label actions with what happens next, not \"Get started\", \"Submit\" or \"Learn more\".",
        "path": "/rules/W-02"
      },
      {
        "id": "ST-01",
        "statement": "Design empty, loading and error states for every region that loads or lists data.",
        "path": "/rules/ST-01"
      },
      {
        "id": "ST-02",
        "statement": "Give first use, no results and failure their own message and action.",
        "path": "/rules/ST-02"
      },
      {
        "id": "ST-04",
        "statement": "Say what failed, what is still safe, and one way forward (retry, edit or contact).",
        "path": "/rules/ST-04"
      },
      {
        "id": "A-01",
        "statement": "Make every interactive target at least 44×44 CSS px; inline text links excepted.",
        "path": "/rules/A-01"
      }
    ],
    "explanation": "An empty state is the first screen many people see, and the screen they see when something goes wrong. The common default treats all of those moments the same: an illustration and \"Nothing here yet\". This item treats them as three different situations.\n\n**First use** explains what will appear and offers the action that fills it. **No results** repeats the query and the search scope, then offers a wider search or a reset. **Error** says what failed and what is safe, then offers a retry. Loading is out of scope: pair this item with a skeleton or progress state to cover all of ST-01.\n\nThe component is deliberately plain: a heading, a sentence or two, and actions. Put it where the content would have been, at the same alignment, so the page does not jump when content arrives.",
    "source": {
      "path": "web/registry/items/empty-state/empty-state.tsx",
      "code": "import * as React from \"react\";\n\nimport { cn } from \"@/registry/shadcn/lib/utils\";\n\ntype HeadingLevel = 2 | 3 | 4;\n\nexport type EmptyStateProps = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n  /** Says what is empty, in the user's words: \"No notes in Field research yet\". */\n  title: React.ReactNode;\n  /** One or two sentences: why it is empty and what will appear here. */\n  description?: React.ReactNode;\n  /**\n   * `neutral` for first use and no results; `error` when something failed.\n   * Error adds a text label, so the state never relies on colour alone.\n   */\n  variant?: \"neutral\" | \"error\";\n  /** Text shown before the title in the error variant. */\n  errorLabel?: string;\n  /** Match the surrounding outline; the empty state replaces a region's content. */\n  headingLevel?: HeadingLevel;\n  /** Actions: one primary, at most one secondary. */\n  actions?: React.ReactNode;\n};\n\nexport function EmptyState({\n  title,\n  description,\n  variant = \"neutral\",\n  errorLabel = \"Error\",\n  headingLevel = 2,\n  actions,\n  children,\n  className,\n  ...props\n}: EmptyStateProps) {\n  const headingId = React.useId();\n  const Heading = `h${headingLevel}` as const;\n  const isError = variant === \"error\";\n\n  return (\n    <section\n      aria-labelledby={headingId}\n      data-slot=\"empty-state\"\n      data-variant={variant}\n      className={cn(\n        \"rounded-lg border bg-card px-5 py-6 text-card-foreground sm:px-8 sm:py-8\",\n        isError ? \"border-destructive/50\" : \"border-dashed border-border\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"max-w-prose\">\n        <Heading\n          id={headingId}\n          className=\"text-lg leading-snug font-semibold tracking-tight text-balance\"\n        >\n          {isError ? (\n            <span className=\"mb-1 block text-sm font-semibold text-destructive\">\n              {errorLabel}\n            </span>\n          ) : null}\n          {title}\n        </Heading>\n        {description ? (\n          <div className=\"mt-2 text-sm leading-relaxed text-pretty text-muted-foreground\">\n            {description}\n          </div>\n        ) : null}\n        {children ? <div className=\"mt-4 text-sm\">{children}</div> : null}\n      </div>\n      {actions ? (\n        <div className=\"mt-6 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center\">\n          {actions}\n        </div>\n      ) : null}\n    </section>\n  );\n}\n"
    }
  },
  "boundaries": [
    "Community items are merged and pass automated checks (schema, build, accessibility, overflow and console errors in three themes at five widths); only items with status reviewed have been checked by a maintainer.",
    "Each rule id links to its page at /rules/<id>; every rule is a draft until a maintainer signs it.",
    "Tessli does not claim that using an item improves AI-generated UI; that claim waits for a published evaluation."
  ]
}
