# Mailroom — Email Client & Inbox UI for React, shadcn/ui and Tailwind

> A complete email client for React and shadcn/ui: three resizable panes, folders and labels, threaded conversations, a rich-text compose window with recipient chips and attachments, an inline reply, snooze, multi-select with bulk actions, undo toasts, keyboard shortcuts, phone…

- Kind: Component (Dashboards)
- Price: comes with the Pro plan
- License: UpFork Standard License. Use it in unlimited personal and commercial projects. Don't resell or redistribute it on its own.
- Developer: UpFork
- Tags: dashboard, sidebar, form, list, email, inbox, email-client, shadcn, tiptap, rich-text-editor, react
- npm packages: @tiptap/extension-image, @tiptap/extensions, @tiptap/react, @tiptap/starter-kit, cmdk, lucide-react, motion, react-resizable-panels, sonner, zustand
- shadcn/ui components: alert-dialog, avatar, button, calendar, checkbox, command, context-menu, dialog, dropdown-menu, hover-card, input, kbd, popover, resizable, scroll-area, separator, sheet, skeleton, sonner, switch, tooltip
- Published: 2026-10-15
- Updated: 2026-10-15
- Page: https://upfork.dev/products/email-client-inbox-react-shadcn

## How to get it

Install it into a React + Tailwind project with the shadcn CLI:

```bash
npx shadcn@latest add @upfork/email-client-inbox-react-shadcn
```

It installs with the member's personal CLI token (UPFORK_TOKEN), made on their UpFork account page. It comes with UpFork's Pro plan and the plans after it.

## About

A premium mail client you drop into a dashboard: folders and labels on the left, the conversation list in the
middle, the thread on the right, in three resizable panes that remember their sizes. Search highlights what it finds,
rows animate out when you archive them, every action can be undone, and the whole thing runs from the keyboard. The
Compose window floats bottom-right like Gmail's, with recipient chips, rich text, attachments and autosave. Tablets
get two panes, phones one screen at a time with swipes and a full-screen composer. It uses only shadcn/ui's theme
tokens, so it looks finished in light and dark and takes on your theme.

Built from the brief in `prompt/component/mail-upgrade-prompt.md` (a redesign of an admin template's Mail page),
written from scratch.

Next.js 16 (App Router), React 19, TypeScript strict, Tailwind CSS 4, shadcn/ui (Radix), `react-resizable-panels`,
`sonner`, TipTap 3, `motion`, `zustand`, `cmdk`, `react-day-picker`, lucide icons, Geist and Geist Mono.

## Run it

```bash
pnpm install
pnpm dev                    # http://localhost:3000
pnpm build && pnpm start    # production
```

The demo page is the mail client filling the window, with a light / system / dark switch. Press `?` for the
shortcuts. The address bar follows the view (`?folder=sent`, `?label=social`, `?id=code-review`), so views can be
linked and Back works.

## Use it

```tsx
import { Mail } from "@/components/mail"

export default function MailPage() {
  return (
    <div className="h-[calc(100dvh-12rem)]">
      <Mail urlState />
    </div>
  )
}
```

`<Mail>` fills its parent, so give the parent a height. It picks its layout from its own width, not the window's,
so it works inside any dashboard shell: three panes from 900 px, two from 640 px, one below.

| Prop | Default | What it does |
| --- | --- | --- |
| `data` | the demo mailbox | Your mailbox: `{ me, people, labels, threads }` (see `types.ts`) |
| `defaultFolder` | `"inbox"` | The folder to open first (the address bar wins when `urlState` is on) |
| `defaultThreadId` | — | The conversation open at first, by thread id |
| `urlState` | `false` | Mirror `?folder`, `?label` and `?id` in the address bar |
| `shortcuts` | `true` | Gmail-style single-key shortcuts |
| `toaster` | `true` | Mount a sonner `<Toaster />`; turn off if your layout already has one |
| `loadingDelay` | `320` | How long the list shows its skeleton on first load and folder changes (`0` = never) |
| `storageKey` | `"mail"` | Where pane sizes are kept in localStorage |
| `onSend(draft)` | — | Called when a message is sent, to hand it to your backend |
| `onReply(thread, mode, html)` | — | Called when a reply or forward is sent |
| `className` | — | Classes for the root |

All state lives in one zustand store made per `<Mail>` (`store.ts`): the actions (`change` with Undo snapshots,
`send`, `reply`, `saveDraft`, `addLabel`, `wakeSnoozed`…) are pure functions over the threads, and every count is
derived from the data, never stored. Swap `createMailData()` for your own data loader and call your API from
`onSend` / `onReply`, or wrap `store.actions` to persist changes.

## What's in it

- **Folders** Inbox, Snoozed, Drafts, Sent, Spam, Trash, Archive with live counts, and **labels** in the five label
  colours (a collapsible section, **New label** with a name and a colour). The pane collapses to an icon rail with
  tooltips when dragged narrow.
- **List** with All mail / Unread (a fixed badge), debounced search over sender, address, subject and body with the
  matches highlighted, four sort orders in a radio menu, sticky date groups, an unread dot, hover quick actions
  (Archive, Trash, Read/Unread, Star), a checkbox on the avatar, Shift+click ranges, a bulk bar, a right-click menu,
  attachment and thread-count chips, label dots with names on hover, rows that animate out, a loading skeleton, and a
  separate empty state for an empty folder, no results, nothing unread and nothing selected.
- **Toolbar** fitted to the folder (Inbox, Snoozed, Drafts, Sent, Spam, Trash, Archive each get their own actions),
  tooltips with shortcuts, Manage labels with search, Snooze with presets and a date picker, previous / next with
  "3 of 10", and a ⋮ menu with Mark as unread, Star, Archive, Spam, Trash, Print and Copy link.
- **Thread** with label pills, an avatar stack, the full date on hover, a sender card (photo, Copy email, New
  message), earlier messages folded into one pill, foldable message cards, quoted text behind `•••`, attachment cards
  with a download button, and your own messages on a quieter surface.
- **Reply box** that opens from a one-line pill into Reply / Reply all / Forward with formatting, attachments, Mute
  this thread, a "Saved" draft indicator and ⌘↵ / Ctrl+↵.
- **Compose** docked bottom-right (expanded, minimized, full screen), recipient chips with contact suggestions and
  red invalid addresses, Cc and Bcc, TipTap rich text (bold, italic, underline, lists, links, images), attachments by
  picker or drag and drop, autosave into Drafts, Discard with confirmation, Send with Undo. Opening a draft opens it
  here, filled in.
- **Undo** on every move, archive, delete, spam, label, snooze and send, through sonner toasts.
- **Keyboard**: J / K, Enter / O, Esc, E, #, !, S, U, L, H, X, R, A, F, C, /, G I, G S, G D and ? for the cheat
  sheet; none of them fire while you type.
- **Phones**: one screen at a time, the thread slides in with Back, swipe left to archive and right to mark read, long
  press to select, a Compose button that opens a full-screen sheet, 40 px tap targets and 16 px text in fields so iOS
  doesn't zoom.

## Accessibility

Real buttons with names, a roving tab stop in the list (↑ ↓ Home End), focus rings from `--ring`, labelled switches
and fields, live announcements for counts, `inert` behind the full-screen composer, and reduced motion respected
everywhere. axe-core (WCAG 2.2 AA) reports no issues on the main screens in either theme. One detail: shadcn's
`muted-foreground` on `muted` is 4.3 : 1, just under AA, so muted text on muted or accent surfaces uses the
foreground at 60 % instead (`TEXT_ON_MUTED` in `parts.tsx`): same palette, 5 : 1.

## Checks

```bash
pnpm test          # Vitest: store, Undo snapshots, send/draft/reply, snooze, search, grouping, dates, parsing
pnpm lint          # ESLint (Next's config, React Compiler rules)
pnpm typecheck
node scripts/e2e.mjs [url]          # 108 click-through checks in Chrome (the brief's checklist, phone and tablet)
node scripts/a11y.mjs [url]         # axe-core on 7 screens × light and dark
pnpm shots [url] [scene…]           # screenshots in .review/ at 1440, 1024, 768 and 390 px, light and dark
```

## Files

```
components/mail/
  mail.tsx              <Mail>: store, layouts by width, dialogs, toaster
  store.ts              the zustand store and its pure thread operations (Undo snapshots)
  selectors.ts          counts, views, search, sorting, date groups, highlights
  data.ts               the demo mailbox (dates relative to now)
  mail-sidebar.tsx      folders, labels, New label, the icon rail
  mail-list.tsx         header, tabs, search, sort, bulk bar, groups, skeleton, empty states
  mail-list-item.tsx    a row: avatar/checkbox, quick actions, chips, swipe, long press
  mail-toolbar.tsx      the folder-aware toolbar
  thread-view.tsx       the reading pane, earlier messages, draft preview, no-selection state
  message-card.tsx      a message, its body (paragraphs, lists, quotes) and attachments
  inline-reply.tsx      the reply box
  compose-window.tsx    the Compose window
  rich-text.tsx         TipTap and its toolbar (its own chunk, preloaded while idle; editor.tsx loads it)
  recipient-input.tsx   address chips with suggestions
  label-picker.tsx      Manage labels
  snooze-menu.tsx       Snooze presets (+ snooze-calendar.tsx, loaded when the date picker opens)
  sender-card.tsx       the hover card (a tap-to-open card on touch screens)
  shortcuts.tsx         the keyboard shortcuts and the cheat sheet
  use-thread-actions.ts every action with its toast and Undo
  url-state.ts          the address bar sync
components/ui/          the shadcn/ui components it uses, unchanged
public/images/avatars/  the photos (CREDITS.md), public/files/ the demo attachments
scripts/                fetch-avatars, attachments, e2e, a11y, screenshots
```

## Re-skinning

Everything is drawn from shadcn's tokens (`background`, `foreground`, `muted`, `accent`, `border`, `ring`,
`primary`, `destructive`, `sidebar`) plus the five label hues in `parts.tsx` (`LABEL_STYLES`). Change your theme and
the mail follows; there are no hard-coded colours in the components. Times and counts use `font-mono` (Geist Mono in
a default shadcn project).

## Departures from the brief

- Avatars use shadcn's `Avatar` (a plain `<img>` with explicit width and height, lazy, with the initials fallback
  delayed so it never flashes) instead of `next/image`, so the component installs in any React project.
- Organisation senders were renamed so the product carries no third-party trademarks: "Amazon Deals" → **Cartly
  Deals**, "LinkedIn" → **ProCircle**, and the spam sender is the **Inheritance Claims Office** as the brief asked.
- Label views list every conversation with the label outside Spam and Trash (as Gmail does), so Updates counts 5
  and Promotions 2 (the archived Server Maintenance Notice and June newsletter carry those labels).
- A **Snoozed** folder was added so snoozed conversations have somewhere to wait; they come back to the Inbox unread.
- The star is drawn in the foreground colour, not yellow, because the palette is frozen.
- Responsive breakpoints follow the component's own width (900 / 640 px), so it behaves the same inside a dashboard.
- Closing Compose keeps what you wrote as a draft (as mail clients do); Discard deletes it after asking.

## Credits

Photos: Unsplash, listed in `public/images/avatars/CREDITS.md`. The names, addresses and messages are invented.
Icons: lucide. UI primitives: shadcn/ui (MIT).
