Support Chat Widget
The corner launcher, with real availability instead of a stock reassurance, article deflection before the form, no self-opening, and a way to make it go away.
The list half of a two-pane inbox, with unread and selected as visibly different states and bulk selection as a mode rather than a hover checkbox.
components/inbox-thread-list.tsxnpx hoverlab add inbox-thread-list
Or over MCP, from your editor's agent — no account needed.
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.
Start a page with this section — add more, order them, and leave with the page source.
Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.
'use client'
/**
* <InboxThreadList> — the list half of a two-pane inbox.
*
* A thread list is a table pretending to be a list, and the pretence is what
* makes it hard. Six things compete for one row: who, subject, preview, when,
* unread, and whatever the product's own state is — assignee, SLA, channel.
* Every inbox that reads badly has lost that fight in the same way, by giving
* each of the six an equal share of the row.
*
* THE HIERARCHY THIS PICKS, AND WHY
*
* line 1 sender, then time — the two things scanned first, at the edges
* line 2 subject, in the reading weight
* line 3 preview, one line, clipped
* markers unread dot at the start edge, state chips at the end
*
* Unread is a dot and a font weight, not a background colour: a highlighted
* row is how a list of forty unread messages becomes an unreadable block, and
* the dot survives being read at a glance in a way a shade of grey does not.
*
* SELECTION AND UNREAD ARE DIFFERENT THINGS AND MUST LOOK DIFFERENT
*
* The row you are reading is selected; the rows you have not read are unread;
* a selected unread row is both, briefly. Products that use one visual
* treatment for both make it impossible to tell what you are looking at, so
* here selection is a filled surface plus a start-edge rule and unread is the
* dot and the weight.
*
* BULK SELECTION IS A SEPARATE MODE. A checkbox that appears on hover in a row
* that is also a button is a click-target conflict, so bulk mode is entered
* deliberately and the rows become checkboxes while it is on.
*
* ACCESSIBILITY: the list is a `<ul>` of buttons, not a `role="grid"`; the
* selected row carries `aria-current="true"`; unread is announced through a
* visually hidden word rather than by the dot; the filter row is a real
* tablist so arrow keys move between filters.
*/
import * as React from 'react'
import { CheckSquare, Circle, Mail, Paperclip, Star } from 'lucide-react'
export type ThreadChannel = 'email' | 'chat' | 'form'
export interface InboxThread {
id: string
sender: string
initials: string
subject: string
preview: string
at: string
unread?: boolean
starred?: boolean
attachments?: number
channel: ThreadChannel
/** Free-form state chip — assignee, SLA, queue. */
chip?: string
}
export interface InboxThreadListProps {
threads?: InboxThread[]
className?: string
}
const DEFAULT_THREADS: InboxThread[] = [
{
id: 't1',
sender: 'Priya Raman',
initials: 'PR',
subject: 'Split delivery for ORD-88410',
preview: 'Chairs are on the 07:00 run Monday. I moved the desks to Wednesday.',
at: '09:14',
unread: true,
channel: 'chat',
chip: 'You',
},
{
id: 't2',
sender: 'accounts@contoso.example',
initials: 'CO',
subject: 'Remittance advice — 12 September',
preview: 'Payment of £18,900.00 has been sent covering invoice INV-2026-0841.',
at: '08:02',
unread: true,
attachments: 2,
channel: 'email',
},
{
id: 't3',
sender: 'Marte Haugen',
initials: 'MH',
subject: 'Cracked base plate — replacement or swap?',
preview: 'Happy to fit the plate myself if you can post one. The allen key is in the box.',
at: 'Yesterday',
starred: true,
channel: 'form',
chip: 'Sam O.',
},
{
id: 't4',
sender: 'Fabrikam Parts',
initials: 'FP',
subject: 'Lead time change: brass lamp shades',
preview: 'October stock has slipped to the 12th. Everything else is unchanged.',
at: 'Yesterday',
channel: 'email',
},
{
id: 't5',
sender: 'Kofi Ojo',
initials: 'KO',
subject: 'Refund not received',
preview: 'It has been eleven days. My bank says nothing has been presented.',
at: 'Wed',
channel: 'form',
chip: 'Breaching',
},
]
const CHANNEL_LABEL: Record<ThreadChannel, string> = {
email: 'Email',
chat: 'Live chat',
form: 'Web form',
}
export function InboxThreadList({
threads = DEFAULT_THREADS,
className = '',
}: InboxThreadListProps) {
const uid = React.useId()
const [filter, setFilter] = React.useState<'all' | 'unread' | 'starred'>('all')
const [selectedId, setSelectedId] = React.useState(threads[0]?.id ?? '')
const [bulk, setBulk] = React.useState(false)
const [checked, setChecked] = React.useState<string[]>([])
const visible = threads.filter((thread) =>
filter === 'unread' ? thread.unread : filter === 'starred' ? thread.starred : true,
)
return (
<section className={`bg-background px-4 py-10 sm:px-6 ${className}`}>
<div className="mx-auto max-w-md overflow-hidden rounded-2xl border border-border bg-card">
<header className="flex items-center justify-between gap-3 border-b border-border p-3">
<h2 className="text-sm font-semibold">Inbox</h2>
{/* A separate mode, not a checkbox fighting the row for clicks. */}
<button
type="button"
onClick={() => {
setBulk((v) => !v)
setChecked([])
}}
aria-pressed={bulk}
className={`inline-flex items-center gap-1.5 rounded-lg border px-2 py-1 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
bulk ? 'border-primary bg-primary/10 text-primary' : 'border-border hover:bg-muted'
}`}
>
<CheckSquare aria-hidden className="h-3.5 w-3.5" />
Select
</button>
</header>
<div role="tablist" aria-label="Filter threads" className="flex gap-1 border-b border-border p-2">
{(['all', 'unread', 'starred'] as const).map((value) => (
<button
key={value}
type="button"
role="tab"
id={`${uid}-tab-${value}`}
aria-selected={filter === value}
aria-controls={`${uid}-list`}
tabIndex={filter === value ? 0 : -1}
onClick={() => setFilter(value)}
className={`rounded-lg px-2.5 py-1 text-xs font-medium capitalize transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
filter === value ? 'bg-muted text-foreground' : 'text-muted-foreground'
}`}
>
{value}
</button>
))}
</div>
{bulk && checked.length > 0 ? (
<p role="status" className="border-b border-border bg-primary/5 px-3 py-2 text-xs">
<span className="font-semibold">{checked.length} selected.</span>{' '}
<button type="button" className="underline underline-offset-2">
Mark read
</button>{' '}
·{' '}
<button type="button" className="underline underline-offset-2">
Assign
</button>{' '}
·{' '}
<button type="button" className="underline underline-offset-2">
Archive
</button>
</p>
) : null}
<ul
id={`${uid}-list`}
role="tabpanel"
aria-labelledby={`${uid}-tab-${filter}`}
className="divide-y divide-border/70"
>
{visible.map((thread) => {
const selected = !bulk && selectedId === thread.id
const isChecked = checked.includes(thread.id)
return (
<li key={thread.id}>
<button
type="button"
onClick={() =>
bulk
? setChecked((c) =>
c.includes(thread.id)
? c.filter((id) => id !== thread.id)
: [...c, thread.id],
)
: setSelectedId(thread.id)
}
aria-current={selected ? 'true' : undefined}
aria-pressed={bulk ? isChecked : undefined}
// Selection is a surface and a start-edge rule; unread is a
// dot and a weight. Two states, two treatments.
className={`flex w-full gap-3 border-s-2 p-3 text-start transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring ${
selected
? 'border-s-primary bg-primary/5'
: 'border-s-transparent hover:bg-muted/50'
}`}
>
{bulk ? (
<span
aria-hidden
className={`mt-1 grid h-4 w-4 shrink-0 place-items-center rounded border ${
isChecked ? 'border-primary bg-primary text-primary-foreground' : 'border-border'
}`}
>
{isChecked ? <CheckSquare className="h-3 w-3" /> : null}
</span>
) : (
<span
aria-hidden
className="grid h-8 w-8 shrink-0 place-items-center rounded-full bg-muted text-[10px] font-bold text-muted-foreground"
>
{thread.initials}
</span>
)}
<span className="min-w-0 flex-1">
<span className="flex items-baseline justify-between gap-2">
<span
className={`truncate text-sm ${
thread.unread ? 'font-bold' : 'font-medium'
}`}
>
{thread.sender}
</span>
<span className="shrink-0 text-[11px] text-muted-foreground">
{thread.at}
</span>
</span>
<span
className={`mt-0.5 flex items-center gap-1.5 truncate text-sm ${
thread.unread ? 'font-semibold' : ''
}`}
>
{thread.unread ? (
<Circle
aria-hidden
className="h-2 w-2 shrink-0 fill-current text-primary"
/>
) : null}
<span className="min-w-0 truncate">{thread.subject}</span>
</span>
<span className="mt-0.5 block truncate text-xs text-muted-foreground">
{thread.preview}
</span>
<span className="mt-1.5 flex flex-wrap items-center gap-1.5">
<span className="inline-flex items-center gap-1 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
<Mail aria-hidden className="h-2.5 w-2.5" />
{CHANNEL_LABEL[thread.channel]}
</span>
{thread.attachments ? (
<span className="inline-flex items-center gap-1 text-[10px] text-muted-foreground">
<Paperclip aria-hidden className="h-2.5 w-2.5" />
{thread.attachments}
</span>
) : null}
{thread.starred ? (
<Star
aria-hidden
className="h-3 w-3 fill-current text-amber-500"
/>
) : null}
{thread.chip ? (
<span
className={`rounded px-1.5 py-0.5 text-[10px] font-semibold ${
thread.chip === 'Breaching'
? 'bg-destructive/10 text-destructive'
: 'bg-primary/10 text-primary'
}`}
>
{thread.chip}
</span>
) : null}
</span>
{/* Announced, rather than left to the dot. */}
<span className="sr-only">
{thread.unread ? 'Unread. ' : ''}
{thread.starred ? 'Starred. ' : ''}
</span>
</span>
</button>
</li>
)
})}
</ul>
{visible.length === 0 ? (
<p className="p-6 text-center text-sm text-muted-foreground">
Nothing matches that filter.
</p>
) : null}
</div>
</section>
)
}
bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.Drop it at components/inbox-thread-list.tsx and import it where you need the section:
import { InboxThreadList } from '@/components/inbox-thread-list'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.
| Prop | Type | Default |
|---|---|---|
threads | InboxThread[] | DEFAULT_THREADS |
className | string | '' |
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.
This block is interactive. The markup below is its initial state with the event handlers stripped — you will need to re-wire the behaviour in your framework.
<!--
Inbox Thread List — 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 is interactive in React and the handlers are NOT here.
Buttons, toggles and menus render in their initial state and do
nothing until you wire them up.
-->
<section class="bg-background px-4 py-10 sm:px-6 ">
<div class="mx-auto max-w-md overflow-hidden rounded-2xl border border-border bg-card">
<header class="flex items-center justify-between gap-3 border-b border-border p-3">
<h2 class="text-sm font-semibold">Inbox</h2>
<button type="button" aria-pressed="false" class="inline-flex items-center gap-1.5 rounded-lg border px-2 py-1 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring border-border hover:bg-muted"><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-square-check-big h-3.5 w-3.5" aria-hidden="true"><path d="M21 10.656V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h12.344"></path><path d="m9 11 3 3L22 4"></path></svg>Select</button>
</header>
<div role="tablist" aria-label="Filter threads" class="flex gap-1 border-b border-border p-2">
<button type="button" role="tab" id="_R_0_-tab-all" aria-selected="true" aria-controls="_R_0_-list" tabindex="0" class="rounded-lg px-2.5 py-1 text-xs font-medium capitalize transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring bg-muted text-foreground">all</button>
<button type="button" role="tab" id="_R_0_-tab-unread" aria-selected="false" aria-controls="_R_0_-list" tabindex="-1" class="rounded-lg px-2.5 py-1 text-xs font-medium capitalize transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring text-muted-foreground">unread</button>
<button type="button" role="tab" id="_R_0_-tab-starred" aria-selected="false" aria-controls="_R_0_-list" tabindex="-1" class="rounded-lg px-2.5 py-1 text-xs font-medium capitalize transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring text-muted-foreground">starred</button>
</div>
<ul id="_R_0_-list" role="tabpanel" aria-labelledby="_R_0_-tab-all" class="divide-y divide-border/70">
<li>
<button type="button" aria-current="true" class="flex w-full gap-3 border-s-2 p-3 text-start transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring border-s-primary bg-primary/5">
<span aria-hidden="true" class="grid h-8 w-8 shrink-0 place-items-center rounded-full bg-muted text-[10px] font-bold text-muted-foreground">PR</span>
<span class="min-w-0 flex-1">
<span class="flex items-baseline justify-between gap-2">
<span class="truncate text-sm font-bold">Priya Raman</span>
<span class="shrink-0 text-[11px] text-muted-foreground">09:14</span>
</span>
<span class="mt-0.5 flex items-center gap-1.5 truncate text-sm font-semibold">
<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-circle h-2 w-2 shrink-0 fill-current text-primary" aria-hidden="true">
<circle cx="12" cy="12" r="10"></circle>
</svg>
<span class="min-w-0 truncate">Split delivery for ORD-88410</span>
</span>
<span class="mt-0.5 block truncate text-xs text-muted-foreground">Chairs are on the 07:00 run Monday. I moved the desks to Wednesday.</span>
<span class="mt-1.5 flex flex-wrap items-center gap-1.5">
<span class="inline-flex items-center gap-1 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium 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-mail h-2.5 w-2.5" aria-hidden="true"><path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7"></path><rect x="2" y="4" width="20" height="16" rx="2"></rect></svg>Live chat</span>
<span class="rounded px-1.5 py-0.5 text-[10px] font-semibold bg-primary/10 text-primary">You</span>
</span>
<span class="sr-only">Unread.</span>
</span>
</button>
</li>
<li>
<button type="button" class="flex w-full gap-3 border-s-2 p-3 text-start transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring border-s-transparent hover:bg-muted/50">
<span aria-hidden="true" class="grid h-8 w-8 shrink-0 place-items-center rounded-full bg-muted text-[10px] font-bold text-muted-foreground">CO</span>
<span class="min-w-0 flex-1">
<span class="flex items-baseline justify-between gap-2">
<span class="truncate text-sm font-bold">accounts@contoso.example</span>
<span class="shrink-0 text-[11px] text-muted-foreground">08:02</span>
</span>
<span class="mt-0.5 flex items-center gap-1.5 truncate text-sm font-semibold">
<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-circle h-2 w-2 shrink-0 fill-current text-primary" aria-hidden="true">
<circle cx="12" cy="12" r="10"></circle>
</svg>
<span class="min-w-0 truncate">Remittance advice — 12 September</span>
</span>
<span class="mt-0.5 block truncate text-xs text-muted-foreground">Payment of £18,900.00 has been sent covering invoice INV-2026-0841.</span>
<span class="mt-1.5 flex flex-wrap items-center gap-1.5">
<span class="inline-flex items-center gap-1 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium 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-mail h-2.5 w-2.5" aria-hidden="true"><path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7"></path><rect x="2" y="4" width="20" height="16" rx="2"></rect></svg>Email</span>
<span class="inline-flex items-center gap-1 text-[10px] 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-paperclip h-2.5 w-2.5" aria-hidden="true"><path d="m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551"></path></svg>2</span>
</span>
<span class="sr-only">Unread.</span>
</span>
</button>
</li>
<li>
<button type="button" class="flex w-full gap-3 border-s-2 p-3 text-start transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring border-s-transparent hover:bg-muted/50">
<span aria-hidden="true" class="grid h-8 w-8 shrink-0 place-items-center rounded-full bg-muted text-[10px] font-bold text-muted-foreground">MH</span>
<span class="min-w-0 flex-1">
<span class="flex items-baseline justify-between gap-2">
<span class="truncate text-sm font-medium">Marte Haugen</span>
<span class="shrink-0 text-[11px] text-muted-foreground">Yesterday</span>
</span>
<span class="mt-0.5 flex items-center gap-1.5 truncate text-sm ">
<span class="min-w-0 truncate">Cracked base plate — replacement or swap?</span>
</span>
<span class="mt-0.5 block truncate text-xs text-muted-foreground">Happy to fit the plate myself if you can post one. The allen key is in the box.</span>
<span class="mt-1.5 flex flex-wrap items-center gap-1.5">
<span class="inline-flex items-center gap-1 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium 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-mail h-2.5 w-2.5" aria-hidden="true"><path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7"></path><rect x="2" y="4" width="20" height="16" rx="2"></rect></svg>Web form</span>
<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-star h-3 w-3 fill-current text-amber-500" aria-hidden="true">
<path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
</svg>
<span class="rounded px-1.5 py-0.5 text-[10px] font-semibold bg-primary/10 text-primary">Sam O.</span>
</span>
<span class="sr-only">Starred.</span>
</span>
</button>
</li>
<li>
<button type="button" class="flex w-full gap-3 border-s-2 p-3 text-start transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring border-s-transparent hover:bg-muted/50">
<span aria-hidden="true" class="grid h-8 w-8 shrink-0 place-items-center rounded-full bg-muted text-[10px] font-bold text-muted-foreground">FP</span>
<span class="min-w-0 flex-1">
<span class="flex items-baseline justify-between gap-2">
<span class="truncate text-sm font-medium">Fabrikam Parts</span>
<span class="shrink-0 text-[11px] text-muted-foreground">Yesterday</span>
</span>
<span class="mt-0.5 flex items-center gap-1.5 truncate text-sm ">
<span class="min-w-0 truncate">Lead time change: brass lamp shades</span>
</span>
<span class="mt-0.5 block truncate text-xs text-muted-foreground">October stock has slipped to the 12th. Everything else is unchanged.</span>
<span class="mt-1.5 flex flex-wrap items-center gap-1.5">
<span class="inline-flex items-center gap-1 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium 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-mail h-2.5 w-2.5" aria-hidden="true"><path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7"></path><rect x="2" y="4" width="20" height="16" rx="2"></rect></svg>Email</span>
</span>
<span class="sr-only"></span>
</span>
</button>
</li>
<li>
<button type="button" class="flex w-full gap-3 border-s-2 p-3 text-start transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring border-s-transparent hover:bg-muted/50">
<span aria-hidden="true" class="grid h-8 w-8 shrink-0 place-items-center rounded-full bg-muted text-[10px] font-bold text-muted-foreground">KO</span>
<span class="min-w-0 flex-1">
<span class="flex items-baseline justify-between gap-2">
<span class="truncate text-sm font-medium">Kofi Ojo</span>
<span class="shrink-0 text-[11px] text-muted-foreground">Wed</span>
</span>
<span class="mt-0.5 flex items-center gap-1.5 truncate text-sm ">
<span class="min-w-0 truncate">Refund not received</span>
</span>
<span class="mt-0.5 block truncate text-xs text-muted-foreground">It has been eleven days. My bank says nothing has been presented.</span>
<span class="mt-1.5 flex flex-wrap items-center gap-1.5">
<span class="inline-flex items-center gap-1 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium 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-mail h-2.5 w-2.5" aria-hidden="true"><path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7"></path><rect x="2" y="4" width="20" height="16" rx="2"></rect></svg>Web form</span>
<span class="rounded px-1.5 py-0.5 text-[10px] font-semibold bg-destructive/10 text-destructive">Breaching</span>
</span>
<span class="sr-only"></span>
</span>
</button>
</li>
</ul>
</div>
</section>
What each framework gets across the whole catalog — effects convert properly; this rung is markup.
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.
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 CommunicationCopying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.
The corner launcher, with real availability instead of a stock reassurance, article deflection before the form, no self-opening, and a way to make it go away.
Bubbles with the parts that get skipped: per-message delivery state, grouping by author and time, day separators, and a retry on the failed bubble itself.
Five kinds of thing that is not text - image, document, link, location and an upload still in flight - each answering the question its own kind raises.
The reading pane with the composer always present, quoted history collapsed behind a count, and an internal-note mode whose send button names the recipient.
A voice note you can navigate: a waveform that is a real seek control, a speed button, a transcript, and a played state that survives finishing.
The call grid with the state that makes it usable - active speaker, per-tile connection quality, hide self-view, and the you-are-muted nudge.
The green room, where every picker has a test beside it - an input meter, a test tone, a camera preview - and blocked permission is a state, not an error.
The customer side of a ticket, where a named owner and a deadline replace Status: Open - including the waiting-on-you state that stops the argument about response times.