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/emptyRun 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'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
| Prop | Type | Default | Description |
|---|---|---|---|
kind | "empty" | "error" | "start" | Default "empty" | Nothing matched, it could not load, or nothing has been made yet. |
title | ReactNode | Default — | What is empty, in words. |
description | ReactNode | Default — | Why, and what to try. |
actions | ReactNode | Default — | Buttons. The first is the way out: clear, retry or create. |
count | ReactNode | Default 0, ! or + | The figure in the count cell. |
code | ReactNode | Default by kind | The word under the figure: RESULTS, ERROR or NEW. |
live | boolean | Default false | Announce 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. |
headingLevel | 2 – 6 | Default 3 | The title's heading level. |
The title is a heading, so place it where the outline expects one.