Patterns

Empty state

Yuno's empty-state family has three structurally distinct siblings that share the same intent ('no data / no result yet + optional next step') but do NOT share a common anatomy — so they ship as named siblings under one pattern, NOT as one component with a `variant` prop: EmptyStateInline (shipping — compact card with an icon tile, fits inside a ChartCard / TableCard body), EmptyStateHero (shipping — 592×444 hero for full-page / full-section empty states like Rules, Blocklists, Payments; ported from yuno-emptystates-tables), and EmptyStateIllustration (planned — same compact layout as Inline with an SVG instead of a Phosphor icon). Consumers import a sibling directly (`import { EmptyStateInline, EmptyStateHero }`) or via the namespace (`import { EmptyState }; <EmptyState.Inline />` / `<EmptyState.Hero />`) — matching how the kit already exposes compound patterns.

Updated Aug 20, 2026 by Juan Pablo Turina
Umbrella pattern with 3 siblings — 2 shipping today
EmptyStateInline and EmptyStateHero are available now. EmptyStateIllustration (SVG variant of the compact layout) lands in a follow-up. When it does, it slots into the same `EmptyState` namespace and this same doc page — no breaking changes to existing imports.

1. EmptyStateInline

The compact sibling. Sized to sit inside a ChartCard / TableCard body (or any narrow-ish content area) so the empty state feels like content, not a page hero.

Anatomy

The Inline sibling is a bg-muted rounded-lg container that grows to fill its flex parent (flex-1) and centers its content. Inside, a 40×40 icon tile on bg-background (contrast against the muted surface) holds a Phosphor icon at size-5 weight="light". Below the tile, a title (text-sm foreground semibold) + optional description (text-xs muted) stack with gap-1. An optional action slot renders below the text stack (canonical: outline / sm Button).

No Insight listed yet

You'll see all your insights here. Not sure where to start?

Visit our guides
  1. 1
    Container

    bg-muted rounded-lg with flex-1 min-h-0 so it grows to fill a flex parent, plus items-center + justify-center to anchor its content. Padding: px-6 py-10. Text is centered by default.

  2. 2
    Icon tile

    size-10 rounded-md bg-background centered on the muted surface, holding a Phosphor icon at size-5 weight="light". The bg-background contrast is what makes the tile read as a container instead of a floating icon.

  3. 3
    Title

    text-sm text-foreground font-semibold. One clear line summarizing what's missing ("No Insight listed yet", "No results found").

  4. 4
    Description

    Optional text-xs text-muted-foreground below the title. Use it to hint at what the user could do ("Try clearing filters", "Not sure where to start?") or explain the state. Omit when the title carries the full message.

  5. 5
    Action slot

    Optional CTA below the text stack. Canonical: <Button variant="outline" size="sm"> with a route (asChild + <a>) or a plain onClick. Skip when there is no useful next step — an empty state without an action is still legit.

Recipes

Canonical (title + description + action)

The full shape ported from Insights' NoInsightEmptyBody. Use when the empty state has a real next step the user should take (visit guides, add a first record, connect a provider).

No Insight listed yet

You'll see all your insights here. Not sure where to start?

Visit our guides
<EmptyStateInline
  icon={ChartLine}
  title="No Insight listed yet"
  description="You'll see all your insights here. Not sure where to start?"
  action={
    <Button variant="outline" size="sm" asChild>
      <a href="#">Visit our guides</a>
    </Button>
  }
/>
Without action (informational)

Drop the action slot when the empty state is temporary or self-resolving (a filter returned no results, a search matched nothing). The user's next step is to change the filter above, not to click a button inside the empty state.

No results found

Try clearing filters or expanding the date range.

<EmptyStateInline
  icon={MagnifyingGlass}
  title="No results found"
  description="Try clearing filters or expanding the date range."
/>
Title only (compact)

Drop both description and action for the most compact form. Reserve for empty states that live inside a small container where extra copy would dominate the surface.

No data available

<EmptyStateInline
  icon={ChartPie}
  title="No data available"
/>
Inside a ChartCard body

The canonical composition: EmptyStateInline sits as the direct child of a ChartCard body. ChartCard's Main has flex-1 min-h-0, so EmptyStateInline's flex-1 lets it fill the card and center — the empty card visually matches the rhythm of its neighboring cards in a grid.

Insights

No Insight listed yet

You'll see all your insights here. Not sure where to start?

Visit our guides
<ChartCard title="Insights">
  <EmptyStateInline
    icon={ChartLine}
    title="No Insight listed yet"
    description="You'll see all your insights here. Not sure where to start?"
    action={
      <Button variant="outline" size="sm" asChild>
        <a href="#">Visit our guides</a>
      </Button>
    }
  />
</ChartCard>

2. EmptyStateHero

The full-page / full-section sibling. Ported from yuno-emptystates-tables. Text stack (tagline + big 4xl title + description + primary Button) opposite a 592×444 illustration slot. Responsive: stacks on narrow; flips to row-reverse (illustration LEFT, text RIGHT) at xl+. The illustration slot accepts any ReactNode — an <iframe> pointing at a yuno-emptystates-tables animation, an inline <svg>, a static <img>. The 592×444 default matches the animation assets 1-to-1 so any of the 9 canonical animations drops in.

The recipes below render the Hero at its native production width (1280px — the Yuno dashboard content area) and scale the whole thing DOWN uniformly to fit this doc container. This gives you a faithful preview of how the Hero looks in a real prototype (title on one line, illustration + text balanced) instead of squeezing the component into the doc's narrower width. In your prototype the Hero renders at its natural size and behaves responsively as documented.
Try an animation:
Canonical (tagline + title + description + primary CTA)

The full shape used across Rules, Blocklists, Allowlists, Payments, Installments, Recipients, Subscriptions, Payment links, Chargebacks. The illustration slot renders whatever the caller passes — typically an <iframe> loading the matching animation from yuno-emptystates-tables (light + dark files).

No rules created yet

Create your first Rule

Set up custom velocity rules to control payment outcomes. Identify users, monitor transactions, and decide when to approve, review, or bypass checks — all without code, directly from the Yuno dashboard.

// Canonical shape — pass whatever illustration you want in the slot.
// For the 9 vendored Yuno animations, use AnimationFrame: it swaps
// light/dark with the kit theme and scales the fixed 592x444 scene to
// its container instead of cropping it.
import { EmptyStateHero, AnimationFrame } from "@/components/patterns/empty-state";

<EmptyStateHero
  tagline="No rules created yet"
  title="Create your first Rule"
  description="Set up custom velocity rules to control payment outcomes…"
  action={
    <Button>
      <Plus weight="light" /> Create rule
    </Button>
  }
  illustration={
    <iframe
      src="/empty-state/animations/rules/light.html"
      title="Rules empty state animation"
      className="h-full w-full border-0"
      loading="lazy"
    />
  }
/>
Without tagline

Drop the tagline when the title alone carries the 'no data yet' tone (e.g. "Manage your Chargebacks" — the surrounding section context already implies it). Smaller vertical rhythm, still hero-scale.

Manage your Chargebacks

Track and respond to chargebacks efficiently. Monitor disputes, submit evidence, and manage the entire chargeback lifecycle from a single view in the Yuno dashboard.

<EmptyStateHero
  title="Manage your Chargebacks"
  description="Track and respond to chargebacks efficiently…"
  action={<Button>View chargebacks</Button>}
  illustration={<iframe src="/empty-state/animations/chargebacks/light.html" … />}
/>
Responsive behavior

Container-query responsive (Tailwind 4 @container + @5xl:) — the layout reads the Hero's PARENT width, not the browser viewport. When the container is ≥ 1024px: flips to flex-row-reverse (illustration LEFT, text RIGHT). When the container is < 1024px: stacks flex-col (illustration below text). This matches production behavior AND lets the doc's ScaledPreview render at 1280 native + scale visually while keeping the row-reverse layout intact — a viewport-based `xl:` breakpoint would have broken that trick.

Container-query responsive (Tailwind 4 @container + @5xl:) — the layout reads the Hero's PARENT width, not the browser viewport. When the container is ≥ 1024px: flips to flex-row-reverse (illustration LEFT, text RIGHT). When the container is < 1024px: stacks flex-col (illustration below text). This matches production behavior AND lets the doc's ScaledPreview render at 1280 native + scale visually while keeping the row-reverse layout intact — a viewport-based `xl:` breakpoint would have broken that trick.
// Purely CSS. The pattern flips automatically:
//   - narrow (<xl): flex-col, illustration BELOW text
//   - xl+:         flex-row-reverse, illustration LEFT, text RIGHT
//
// The illustration slot uses width: min(100%, illustrationSize.width) +
// aspect-ratio so it shrinks proportionally at narrow widths without
// cropping the animation inside. Resize the browser to see the flip.

Available animations

The kit vendors the 9 canonical Yuno empty-state animations under public/empty-state/animations/{slug}/{light|dark}.html so prototypes can drop them into the illustration slot immediately — no assets to hunt for. These are meant as high-fidelity placeholders while PMs / designers show the empty-state to stakeholders; a final illustration for the specific feature you're prototyping can swap them out later. Each slug has both a light and a dark variant, and AnimationFrame picks the right one for you: it observes the kit theme and scales the fixed 592x444 scene to its container rather than cropping it. Import it from the pattern, the same place EmptyStateHero comes from.

SlugIframe src (light)Iframe src (dark)
rules/empty-state/animations/rules/light.html/empty-state/animations/rules/dark.html
blocklists/empty-state/animations/blocklists/light.html/empty-state/animations/blocklists/dark.html
allowlists/empty-state/animations/allowlists/light.html/empty-state/animations/allowlists/dark.html
payments/empty-state/animations/payments/light.html/empty-state/animations/payments/dark.html
installments/empty-state/animations/installments/light.html/empty-state/animations/installments/dark.html
recipients/empty-state/animations/recipients/light.html/empty-state/animations/recipients/dark.html
subscriptions/empty-state/animations/subscriptions/light.html/empty-state/animations/subscriptions/dark.html
payment-links/empty-state/animations/payment-links/light.html/empty-state/animations/payment-links/dark.html
chargebacks/empty-state/animations/chargebacks/light.html/empty-state/animations/chargebacks/dark.html

Import

Copy this import line at the top of the file where you compose this molecule.

Direct sibling imports are recommended for readability. The namespace import matches the kit's compound-pattern style (Accordion.Root / Item / Trigger). Both work identically.
// Direct sibling imports (recommended)
import {
  EmptyStateInline,
  EmptyStateHero,
} from "@/components/patterns/empty-state";

// Namespace import (matches the Accordion.Root pattern in the kit)
import { EmptyState } from "@/components/patterns/empty-state";
// then: <EmptyState.Inline … />  or  <EmptyState.Hero … />

Props

Everything else is forwarded to the underlying elements via ...props.

EmptyStateInline

PropTypeDefaultDescription
iconPhosphorIcon (required)Phosphor icon component (from @phosphor-icons/react/dist/ssr). Pass the component itself — the pattern renders it at size-5 with weight="light".
titlestring (required)Primary text. text-sm foreground semibold.
descriptionstringOptional muted subtitle below the title. text-xs muted.
actionReactNodeOptional CTA below the text stack. Canonical: <Button variant="outline" size="sm"> with a route (asChild + <a>) or a plain onClick.
classNamestringExtra classes on the outer container. Use to override the bg-muted / rounded-lg surface if you need a transparent variant sitting on a colored background.

EmptyStateHero

PropTypeDefaultDescription
taglinestringOptional small label above the title (text-sm muted medium). Canonical: "No rules created yet", "No payments yet".
titlestring (required)Big heading. text-4xl bold tracking-tight leading-10.
descriptionstringOptional paragraph below the title. Constrained to max-w-lg (512px) so long copy still reads as a paragraph.
actionReactNodeCTA below the description. Canonical: a shadcn <Button> (default variant, size default = h-9) with the action label ("Create rule", "Add recipient").
illustrationReactNode (required)The illustration to render opposite the text stack. Canonical: <iframe> pointing at one of the vendored animations under /empty-state/animations/{slug}/{light|dark}.html. Also accepts inline <svg> or <img>.
illustrationSize{ width: number; height: number }{ width: 592, height: 444 }Illustration container size. Defaults to the canonical Yuno 592×444 matching the vendored animation assets. Override only for differently-proportioned illustrations.
classNamestringExtra classes on the outer container.

Related

Cross-links to the atoms this molecule composes and sibling patterns.

  • Chart cardCanonical host of the Inline sibling — ChartCard's Main body is designed to absorb EmptyStateInline (flex-1 min-h-0).
  • Table cardSame host relationship for empty-state Tables — drop EmptyStateInline in the body when the table has no rows.
  • ButtonThe canonical action slot content — outline variant, size sm.
  • IconographyThe tile icon uses Phosphor weight="light" (kit canon).