89 / 122 · Thread
Empty state
The first screen: a greeting, three ways in, and the composer front and center.
What are we building?
Installation
npx shadcn@latest add "@assistant-ui/elements-empty-state"
Usage
import {
EmptyState,
EmptyStateComposer,
EmptyStateGreeting,
EmptyStateSuggestion,
EmptyStateSuggestions,
} from "@/components/elements/empty-state";
<EmptyState>
<EmptyStateGreeting>What are we building?</EmptyStateGreeting>
<EmptyStateSuggestions>
{items.map((item, i) => (
<EmptyStateSuggestion key={item} index={i} onClick={() => send(item)}>
{item}
</EmptyStateSuggestion>
))}
</EmptyStateSuggestions>
<EmptyStateComposer placeholder="Ask anything" onSend={send} />
</EmptyState>Props
EmptyState
childrenReact.ReactNodeA centered column. Compose the greeting, the suggestions, and your own composer, or leave any of them out.
EmptyStateSuggestion
indexnumberPosition in the row. Staggers the entrance so the pills arrive in order.
...propsReact.ComponentProps<"button">Spread onto the button, so onClick and the rest are yours.
EmptyStateComposer
placeholder*stringResting text in the input bar.
onSend() => voidCalled from the send control. Omit it and the control renders disabled.
Source
empty-state.tsx"use client";
import type { ComponentProps } from "react";
import { ArrowUpIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { inkButton, paper } from "./surfaces";
export function EmptyState({ className, ...props }: ComponentProps<"div">) {
return (
<div
data-slot="empty-state"
className={cn(
"flex w-full max-w-md flex-col items-center gap-7",
className,
)}
{...props}
/>
);
}
export function EmptyStateGreeting({
className,
...props
}: ComponentProps<"h2">) {
return (
<h2
data-slot="empty-state-greeting"
className={cn(
"fade-in slide-in-from-bottom-1 animate-in fill-mode-both text-center text-2xl font-medium tracking-tight duration-500 motion-reduce:animate-none",
className,
)}
{...props}
/>
);
}
export function EmptyStateSuggestions({
className,
...props
}: ComponentProps<"div">) {
return (
<div
data-slot="empty-state-suggestions"
className={cn("flex flex-wrap justify-center gap-2", className)}
{...props}
/>
);
}
export function EmptyStateSuggestion({
index = 0,
className,
style,
...props
}: ComponentProps<"button"> & { index?: number }) {
return (
<button
type="button"
data-slot="empty-state-suggestion"
style={{ animationDelay: `${120 + index * 70}ms`, ...style }}
className={cn(
paper,
"fade-in slide-in-from-bottom-2 animate-in fill-mode-both focus-visible:ring-foreground/20 rounded-full px-4 py-2 text-[13px] transition-transform duration-500 outline-none hover:-translate-y-px focus-visible:ring-2 active:scale-[0.96] motion-reduce:animate-none",
className,
)}
{...props}
/>
);
}
export function EmptyStateComposer({
placeholder,
onSend,
className,
style,
...props
}: Omit<ComponentProps<"div">, "children" | "placeholder"> & {
placeholder: string;
onSend?: () => void;
}) {
return (
<div
data-slot="empty-state-composer"
style={{ animationDelay: "360ms", ...style }}
className={cn(
paper,
"fade-in slide-in-from-bottom-2 animate-in fill-mode-both flex h-13 w-full items-center justify-between rounded-full py-2 ps-5 pe-2.5 duration-500 motion-reduce:animate-none",
className,
)}
{...props}
>
<span className="text-foreground/35 text-[15px]">{placeholder}</span>
<button
type="button"
aria-label="Send"
onClick={onSend}
disabled={!onSend}
className={cn(
inkButton,
"flex size-8 items-center justify-center rounded-full disabled:pointer-events-none disabled:opacity-30",
)}
>
<ArrowUpIcon className="size-4" />
</button>
</div>
);
}