Empty State

Centered placeholder for empty content areas — no data, no results, no uploads.

Quick Start

<%= kiso(:empty_state) do %>
  <%= kiso(:empty_state, :header) do %>
    <%= kiso(:empty_state, :media, variant: :icon) do %>
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/><path d="M2 10h20"/></svg>
    <% end %>
    <%= kiso(:empty_state, :title) { "No Projects Yet" } %>
    <%= kiso(:empty_state, :description) { "Get started by creating your first project." } %>
  <% end %>
<% end %>

Locals

The root empty_state has no variant axis — it renders a centered flex container.

Local Type Default
css_classes: String ""
**component_options Hash {}

Sub-parts

Part Usage Purpose
:header kiso(:empty_state, :header) Groups media, title, and description
:media kiso(:empty_state, :media) Icon or image container
:title kiso(:empty_state, :title) Heading text
:description kiso(:empty_state, :description) Muted description text
:content kiso(:empty_state, :content) Action area (buttons, links)

All sub-parts accept css_classes: and **component_options.

Anatomy

Empty State
├── Header
│   ├── Media (icon or image)
│   ├── Title
│   └── Description
└── Content (actions)

All sub-parts are optional. Use any combination.

Usage

Media Variants

The :media sub-part has a variant: local to control how icons are displayed.

<%= kiso(:empty_state, :media) do %>
  <svg>...</svg>
<% end %>

<%= kiso(:empty_state, :media, variant: :icon) do %>
  <svg>...</svg>
<% end %>
Variant Appearance
default Transparent background, bare icon
icon Muted background, rounded container, auto-sized SVG

With Actions

Use the :content sub-part for buttons and links below the header.

<%= kiso(:empty_state) do %>
  <%= kiso(:empty_state, :header) do %>
    <%= kiso(:empty_state, :media, variant: :icon) do %>
      <svg>...</svg>
    <% end %>
    <%= kiso(:empty_state, :title) { "No Projects Yet" } %>
    <%= kiso(:empty_state, :description) { "Get started by creating your first project." } %>
  <% end %>
  <%= kiso(:empty_state, :content) do %>
    <div class="flex gap-2">
      <%= kiso(:button) { "Create Project" } %>
      <%= kiso(:button, variant: :outline) { "Import Project" } %>
    </div>
  <% end %>
<% end %>

With Dashed Border

The base includes border-dashed (style only). Add border via css_classes: to show the dashed outline — useful for drop zones and upload areas.

<%= kiso(:empty_state, css_classes: "border border-dashed") do %>
  <%= kiso(:empty_state, :header) do %>
    <%= kiso(:empty_state, :title) { "Upload Files" } %>
    <%= kiso(:empty_state, :description) { "Drag and drop files here." } %>
  <% end %>
<% end %>

Inside a Card

Empty states work well nested inside Card content areas. Override flex-1 with flex-none when the empty state shouldn’t expand to fill the container.

<%= kiso(:card) do %>
  <%= kiso(:card, :header) do %>
    <%= kiso(:card, :title) { "Recent Activity" } %>
  <% end %>
  <%= kiso(:card, :content) do %>
    <%= kiso(:empty_state, css_classes: "flex-none") do %>
      <%= kiso(:empty_state, :header) do %>
        <%= kiso(:empty_state, :title) { "No Activity" } %>
        <%= kiso(:empty_state, :description) { "Activity will show up here." } %>
      <% end %>
    <% end %>
  <% end %>
<% end %>

Theme

Kiso::Themes::EmptyState = ClassVariants.build(
  base: "flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12"
)

EmptyStateHeader      = ClassVariants.build(base: "flex max-w-sm flex-col items-center gap-2 text-center")
EmptyStateMedia       = ClassVariants.build(
  base: "flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
  variants: { variant: { default: "bg-transparent", icon: "bg-muted text-foreground size-10 rounded-lg ..." } },
  defaults: { variant: :default }
)
EmptyStateTitle       = ClassVariants.build(base: "text-lg font-medium tracking-tight")
EmptyStateDescription = ClassVariants.build(base: "text-muted-foreground text-sm/relaxed ...")
EmptyStateContent     = ClassVariants.build(base: "flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance")

Accessibility

Empty State renders as a <div> with data-component="empty_state". Sub-parts use data-empty-state-part attributes for identity.

For semantic meaning, use component_options to set a role:

<%= kiso(:empty_state, role: :status, "aria-label": "No results") do %>
  ...
<% end %>