Skip to content
  1. Keyline UI
  2. Empty
Keyline UIComponents
  • Get started
    • Installation
    • Theming
    • Modes
    • Configuration
    • Composition
  • Actions
    • Button
    • Button group
    • Split button
    • Toggle
    • Toggle group
    • Toolbar
    • Menu
    • Menubar
    • Context menu
  • Inputs
    • Field and form
    • Checkbox
    • Checkbox group
    • Radio group
    • Switch
    • Slider
    • Segmented
    • Select
    • Native select
    • Combobox
    • Multiselect
    • Number field
    • OTP field
    • Time field
    • Date picker and calendar
    • Date range
  • Data
    • Table
    • Chart
    • Stat and meter
    • Status and badge
    • Progress
    • Skeleton
    • Empty
    • Avatar
    • Kbd
    • Marker
  • Navigation
    • Tabs
    • Breadcrumb
    • Pagination
    • Nav
    • Navigation menu
    • Command menu
  • Overlays
    • Dialog
    • Alert dialog
    • Drawer
    • Sheet
    • Popover
    • Hover card
    • Tooltip
    • Toast
    • Notice
  • Layout
    • App shell
    • Sidebar
    • Panel
    • Resizable
    • Scroll area
    • Accordion
    • Collapsible
    • Carousel
    • Inspector
    • Item
    • Direction
  • Conversation
    • Message
    • Bubble
    • Message scroller
    • Attachment
    • Questionnaire
  • Blocks
    • Shipments

Empty

A place with nothing in it, said plainly: the count, what happened and the way out. No illustration; the number is the picture. Below 520px of its own width the actions move under the message.

Loading the live example…

"use client"

import { useState } from "react"
import { Button } from "@/components/keyline/ui/button"
import { Empty } from "@/components/keyline/ui/empty"
import { Field, FieldFrame, FieldInput, FieldLabel } from "@/components/keyline/ui/field"

const CLIENTS = ["Blue Cross of Pennsylvania", "Harper Collins", "Lackawanna County", "Scranton Business Park", "Scranton White Pages", "Vance Refrigeration"]

export function ClientSearch() {
  const [q, setQ] = useState("Stamford")
  const hits = CLIENTS.filter((c) => c.toLowerCase().includes(q.trim().toLowerCase()))

  return (
    <div className="grid max-w-[560px] gap-4">
      <Field>
        <FieldLabel>Search clients</FieldLabel>
        <FieldFrame>
          <FieldInput type="search" value={q} onChange={(e) => setQ(e.target.value)} spellCheck={false} />
        </FieldFrame>
      </Field>
      {hits.length ? (
        <ul className="m-0 grid list-none gap-2 p-0 text-[14px]" aria-label="Clients">
          {hits.map((c) => (
            <li key={c}>{c}</li>
          ))}
        </ul>
      ) : (
        <Empty
          live
          title={`No clients match “${q.trim()}”`}
          description="Stamford's clients moved to Scranton under their own names. Try the company, not the branch."
          actions={<Button onClick={() => setQ("")}>Clear search</Button>}
        />
      )}
    </div>
  )
}

Installation

npx shadcn@latest add @keyline-ui/empty

Run it where your components.json is. The first time, map the @keyline-ui registry: one command, in Installation.

Usage

import { Empty } from "@/components/keyline/ui/empty"
<Empty
  live
  title="No clients match “Stamford”"
  description="Try the company, not the branch."
  actions={<Button onClick={clear}>Clear search</Button>}
/>

Examples

First use and errors

kind="error" turns the count cell red and announces itself with live; Retry fails once here, then works. kind="start" is for a place that has nothing yet, with the action that makes the first thing.

Loading the live example…

"use client"

import { useEffect, useRef, useState } from "react"
import { Button } from "@/components/keyline/ui/button"
import { Empty } from "@/components/keyline/ui/empty"

type State = "loading" | "error" | "retrying" | "ready"

export function CallLog() {
  const [state, setState] = useState<State>("loading")
  const [calls, setCalls] = useState<string[]>([])
  const failed = useRef(false)
  const timer = useRef<ReturnType<typeof setTimeout>>(undefined)

  const load = () => {
    setState(failed.current ? "retrying" : "loading")
    // the first load fails, as the call log server does every Monday morning; the retry works
    timer.current = setTimeout(() => {
      setState(failed.current ? "ready" : "error")
      failed.current = true
    }, 900)
  }
  useEffect(() => {
    load()
    return () => clearTimeout(timer.current)
  }, [])

  if (state === "loading")
    return (
      <p role="status" className="m-0 text-[14px] text-kl-ink-2">
        Loading Phyllis&apos;s calls from this week…
      </p>
    )

  if (state !== "ready")
    return (
      <Empty
        live
        kind="error"
        title="The call log didn't load"
        description="The server in the annex didn't answer. Nothing was lost; try again."
        actions={
          <Button signal={state === "retrying" ? "pending" : "idle"} onClick={load}>
            Retry
          </Button>
        }
      />
    )

  if (!calls.length)
    return (
      <Empty
        live
        kind="start"
        title="No calls logged this week"
        description="Phyllis logs each sales call here, with who she spoke to and what they ordered."
        actions={<Button variant="primary" onClick={() => setCalls(["Called Lackawanna County. 120 cases of copy paper, delivered Thursday."])}>Log a call</Button>}
      />
    )

  return (
    <div className="grid gap-3">
      <ul className="m-0 grid list-none gap-2 p-0 text-[14px]">
        {calls.map((c, i) => (
          <li key={i}>{c}</li>
        ))}
      </ul>
      <Button className="justify-self-start" onClick={() => setCalls([])}>
        Clear the log
      </Button>
    </div>
  )
}

Props

PropTypeDefaultDescription
kind"empty" | "error" | "start"Default "empty"Nothing matched, it could not load, or nothing has been made yet.
titleReactNodeDefault —What is empty, in words.
descriptionReactNodeDefault —Why, and what to try.
actionsReactNodeDefault —Buttons. The first is the way out: clear, retry or create.
countReactNodeDefault 0, ! or +The figure in the count cell.
codeReactNodeDefault by kindThe word under the figure: RESULTS, ERROR or NEW.
livebooleanDefault falseAnnounce it when it appears. Use when it replaces what someone was looking at.
layout"control" | "cell"Default "control"cell sits inside another frame, such as a panel or table.
headingLevel2 – 6Default 3The title's heading level.

The title is a heading, so place it where the outline expects one.

Sheet 38 of 73PreviousSkeletonNextAvatar
On this page