Skip to content
AI Interfaces block

Assistant Starter Screen

The blank thread, answered: grouped starter prompts as real lists of buttons, each group labelled, so the highest-leverage screen in an AI product is not a centred logo.

138 lineslucide-reactAdded 13 Aug 2026Updated 2 Sept 2026
  • empty state
  • starters
  • suggestions
  • onboarding
  • chat

What's included

  • components/chat-empty-state.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add chat-empty-state

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

What can I look into?

Connected to your warehouse, docs and support inbox. Ask in plain language — I will show the query I ran.

Analyse

Find

Draft

Explain

Answers cite their sources. Nothing is written back without your approval.

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/chat-empty-state.tsx
/**
 * <ChatEmptyState> — the first screen of an assistant, before any turn.
 *
 * The blank thread is the highest-leverage screen in an AI product and the
 * one most often left as a centred logo. A visitor who does not know what
 * the thing can be asked will ask nothing, so this screen's whole job is to
 * answer "what do I type" with examples specific enough to be worth
 * clicking.
 *
 * Structure, not decoration:
 *
 *  - Suggestions are a real `<ul>` of buttons, so a screen reader hears "list
 *    of 4" and can skip it. A grid of divs with click handlers is the usual
 *    shape and is unreachable by keyboard.
 *  - Each starter is grouped under a heading tied to the list by
 *    `aria-labelledby`, because "Explore" alone tells a reader nothing about
 *    what the four items under it are.
 *  - The capability strip is `aria-hidden` where it repeats the headings —
 *    reassurance for the eye, noise for a reader.
 *
 * Server component: no state, no effects, nothing to hydrate.
 */

import * as React from 'react'
import { BarChart3, FileSearch, Lightbulb, Sparkles, Table2, Wand2 } from 'lucide-react'

export interface StarterGroup {
  id: string
  label: string
  icon: 'analyse' | 'find' | 'draft' | 'explain'
  prompts: string[]
}

export interface ChatEmptyStateProps {
  greeting?: string
  subheading?: string
  groups?: StarterGroup[]
  footnote?: string
  className?: string
}

const GROUP_ICON = {
  analyse: BarChart3,
  find: FileSearch,
  draft: Wand2,
  explain: Lightbulb,
} as const

const DEFAULT_GROUPS: StarterGroup[] = [
  {
    id: 'analyse',
    label: 'Analyse',
    icon: 'analyse',
    prompts: ['Break down Q3 revenue by region', 'Which cohort churned hardest, and why?'],
  },
  {
    id: 'find',
    label: 'Find',
    icon: 'find',
    prompts: ['Show every account with an open escalation', 'Pull the last three pricing decks'],
  },
  {
    id: 'draft',
    label: 'Draft',
    icon: 'draft',
    prompts: ['Write the weekly update from these metrics', 'Draft a save offer for at-risk teams'],
  },
  {
    id: 'explain',
    label: 'Explain',
    icon: 'explain',
    prompts: ['Walk me through how ARR is calculated here', 'What changed in the forecast model?'],
  },
]

export function ChatEmptyState({
  greeting = 'What can I look into?',
  subheading = 'Connected to your warehouse, docs and support inbox. Ask in plain language — I will show the query I ran.',
  groups = DEFAULT_GROUPS,
  footnote = 'Answers cite their sources. Nothing is written back without your approval.',
  className = '',
}: ChatEmptyStateProps) {
  return (
    <section className={`mx-auto w-full max-w-3xl px-6 py-16 text-center ${className}`}>
      <span className="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-primary/10 text-primary">
        <Sparkles aria-hidden className="h-5 w-5" />
      </span>

      <h2 className="mt-5 text-balance text-2xl font-bold tracking-tight sm:text-3xl">
        {greeting}
      </h2>
      <p className="mx-auto mt-3 max-w-lg text-pretty text-sm leading-relaxed text-muted-foreground">
        {subheading}
      </p>

      <div className="mt-10 grid gap-4 text-start sm:grid-cols-2">
        {groups.map((group) => {
          const Icon = GROUP_ICON[group.icon]
          const headingId = `starters-${group.id}`

          return (
            <div
              key={group.id}
              className="rounded-2xl border border-border/60 bg-card/60 p-4 transition-colors hover:border-border"
            >
              <h3
                id={headingId}
                className="mb-3 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-muted-foreground"
              >
                <Icon aria-hidden className="h-3.5 w-3.5 text-primary" />
                {group.label}
              </h3>

              <ul aria-labelledby={headingId} className="space-y-1.5">
                {group.prompts.map((prompt) => (
                  <li key={prompt}>
                    <button
                      type="button"
                      className="w-full rounded-xl px-3 py-2 text-start text-sm leading-snug text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
                    >
                      {prompt}
                    </button>
                  </li>
                ))}
              </ul>
            </div>
          )
        })}
      </div>

      <p className="mx-auto mt-8 inline-flex max-w-md items-center gap-2 rounded-full border border-border/60 bg-card/60 px-3.5 py-1.5 text-xs text-muted-foreground">
        <Table2 aria-hidden className="h-3.5 w-3.5 shrink-0" />
        {footnote}
      </p>
    </section>
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Install: npm i lucide-react
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/chat-empty-state.tsx and import it where you need the section:

import { ChatEmptyState } from '@/components/chat-empty-state'

Customize

2 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
greetingstring'What can I look into?'
subheadingstring—
groupsStarterGroup[]DEFAULT_GROUPS
footnotestring'Answers cite their sources. Nothing is writt…
classNamestring''

Not using React?

The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.

chat-empty-state.html
<!--
  Assistant Starter Screen — markup from the Hoverlab catalog.

  This is the block rendered once to HTML and wrapped as a component
  file. It is not a port of the React source: the Tailwind classes carry
  the design, which is the part that took the work, and they are the same
  in every framework.

  This block has no interactive behaviour, so nothing is missing.
-->
<section class="mx-auto w-full max-w-3xl px-6 py-16 text-center ">
  <span class="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-primary/10 text-primary">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkles h-5 w-5" aria-hidden="true">
      <path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"></path>
      <path d="M20 3v4"></path>
      <path d="M22 5h-4"></path>
      <path d="M4 17v2"></path>
      <path d="M5 18H3"></path>
    </svg>
  </span>
  <h2 class="mt-5 text-balance text-2xl font-bold tracking-tight sm:text-3xl">What can I look into?</h2>
  <p class="mx-auto mt-3 max-w-lg text-pretty text-sm leading-relaxed text-muted-foreground">Connected to your warehouse, docs and support inbox. Ask in plain language — I will show the query I ran.</p>
  <div class="mt-10 grid gap-4 text-start sm:grid-cols-2">
    <div class="rounded-2xl border border-border/60 bg-card/60 p-4 transition-colors hover:border-border">
      <h3 id="starters-analyse" class="mb-3 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chart-column h-3.5 w-3.5 text-primary" aria-hidden="true"><path d="M3 3v16a2 2 0 0 0 2 2h16"></path><path d="M18 17V9"></path><path d="M13 17V5"></path><path d="M8 17v-3"></path></svg>Analyse</h3>
      <ul aria-labelledby="starters-analyse" class="space-y-1.5">
        <li>
          <button type="button" class="w-full rounded-xl px-3 py-2 text-start text-sm leading-snug text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">Break down Q3 revenue by region</button>
        </li>
        <li>
          <button type="button" class="w-full rounded-xl px-3 py-2 text-start text-sm leading-snug text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">Which cohort churned hardest, and why?</button>
        </li>
      </ul>
    </div>
    <div class="rounded-2xl border border-border/60 bg-card/60 p-4 transition-colors hover:border-border">
      <h3 id="starters-find" class="mb-3 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-search h-3.5 w-3.5 text-primary" aria-hidden="true"><path d="M14 2v4a2 2 0 0 0 2 2h4"></path><path d="M4.268 21a2 2 0 0 0 1.727 1H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v3"></path><path d="m9 18-1.5-1.5"></path><circle cx="5" cy="14" r="3"></circle></svg>Find</h3>
      <ul aria-labelledby="starters-find" class="space-y-1.5">
        <li>
          <button type="button" class="w-full rounded-xl px-3 py-2 text-start text-sm leading-snug text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">Show every account with an open escalation</button>
        </li>
        <li>
          <button type="button" class="w-full rounded-xl px-3 py-2 text-start text-sm leading-snug text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">Pull the last three pricing decks</button>
        </li>
      </ul>
    </div>
    <div class="rounded-2xl border border-border/60 bg-card/60 p-4 transition-colors hover:border-border">
      <h3 id="starters-draft" class="mb-3 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wand-sparkles h-3.5 w-3.5 text-primary" aria-hidden="true"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72"></path><path d="m14 7 3 3"></path><path d="M5 6v4"></path><path d="M19 14v4"></path><path d="M10 2v2"></path><path d="M7 8H3"></path><path d="M21 16h-4"></path><path d="M11 3H9"></path></svg>Draft</h3>
      <ul aria-labelledby="starters-draft" class="space-y-1.5">
        <li>
          <button type="button" class="w-full rounded-xl px-3 py-2 text-start text-sm leading-snug text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">Write the weekly update from these metrics</button>
        </li>
        <li>
          <button type="button" class="w-full rounded-xl px-3 py-2 text-start text-sm leading-snug text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">Draft a save offer for at-risk teams</button>
        </li>
      </ul>
    </div>
    <div class="rounded-2xl border border-border/60 bg-card/60 p-4 transition-colors hover:border-border">
      <h3 id="starters-explain" class="mb-3 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-lightbulb h-3.5 w-3.5 text-primary" aria-hidden="true"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"></path><path d="M9 18h6"></path><path d="M10 22h4"></path></svg>Explain</h3>
      <ul aria-labelledby="starters-explain" class="space-y-1.5">
        <li>
          <button type="button" class="w-full rounded-xl px-3 py-2 text-start text-sm leading-snug text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">Walk me through how ARR is calculated here</button>
        </li>
        <li>
          <button type="button" class="w-full rounded-xl px-3 py-2 text-start text-sm leading-snug text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">What changed in the forecast model?</button>
        </li>
      </ul>
    </div>
  </div>
  <p class="mx-auto mt-8 inline-flex max-w-md items-center gap-2 rounded-full border border-border/60 bg-card/60 px-3.5 py-1.5 text-xs text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-table2 lucide-table-2 h-3.5 w-3.5 shrink-0" aria-hidden="true"><path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"></path></svg>Answers cite their sources. Nothing is written back without your approval.</p>
</section>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

Used in these pages

Want the whole screen instead of this one section? Open a page and copy it entire.

7 more blocks in Agent Chat

All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.

Browse Agent Chat

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More Agent Chat blocks

View category
Open the full page for this block

Assistant Thread with Composer

A scrolling conversation with per-turn actions, announced as a polite log restricted to additions — so a streaming reply is not re-read on every token — and auto-scroll that only pins when the user is already at the bottom.

Agent Chat253 lines1 dep
Open the full page for this block

Composer with @ Sources and / Commands

The trigger menus done properly: @ and / open only at a token boundary, the query is derived from the caret so the menu closes itself, and focus never leaves the textarea — the combobox pattern, not a floating div.

Agent Chat434 lines1 dep
Open the full page for this block

Streaming Answer with Citations

Token-by-token text with linked citations, follow-up chips and a caret that stops the moment the stream does. The status is announced once; the prose is deliberately outside every live region.

Agent Chat296 lines1 dep
Open the full page for this block

Message Version Switcher

The answers a regenerate throws away — siblings rather than replacements, an edited question that branches in place, and a plain sentence about what happens to the replies below.

Agent Chat272 lines1 dep
Open the full page for this block

Chat Beside a Canvas

The two-pane assistant layout, with the mismatch nobody else states: the canvas names the version it is showing and says so when that is not the one the conversation is about. Versions link back to the turn that made them.

Agent Chat321 lines1 dep
Open the full page for this block

Model Picker with Trade-offs

Model selection with the three numbers the choice actually turns on — capability, latency and cost per message — and locked models explained rather than greyed out.

Agent Chat267 lines1 dep
Open the full page for this block

Prompt Attachment Tray

Files staged against a prompt with a running context-token budget, unreadable files flagged before send, removal allowed mid-upload, and a send button that names its one blocker.

Agent Chat269 lines1 dep