Skip to main content

Mail

App Shells

Three-zone mail shell: the collapsible icon rail, a searchable message list, and a reading pane sharing one floating inset surface. A variation on the dashboard shell, not a separate idea; below md the two content zones become a drill-down.

npx shadcn@latest add https://elabs-ai.com/r/sidebar-04.json

Skipping the @source line is the most common first failure: Tailwind does not scan node_modules, so every component renders unstyled. The one-time wiring

Prompt for your coding agent

Paste it into Claude Code, Cursor, Codex or any other coding agent. Text in [brackets] is yours to fill in.

Add the brand-ui registry block "Mail" (sidebar-04) to this project. A block is copied into the app and becomes my code.

1. Read https://elabs-ai.com/llms.txt first: it maps every package and the rules. If you can use MCP servers, connect https://elabs-ai.com/mcp and use its search, docs and tokens tools. If not, run the same lookups in the terminal: `npx -y @elabs-ai/components-cli search "<what you need>"` and `npx -y @elabs-ai/components-cli docs <Component>`.
2. Find it and what it is built from: `npx -y @elabs-ai/components-cli search sidebar-04`.
3. Make sure the base is in place: `pnpm add @elabs-ai/components-tokens @elabs-ai/components-ui` (or `npm install @elabs-ai/components-tokens @elabs-ai/components-ui`), plus the packages the block imports. If this is the first brand-ui package in the project, wire it once: import `@elabs-ai/components-tokens/styles.css` plus the `themes/light.css` and `themes/dark.css` stylesheets in the CSS entry, add one Tailwind `@source` line per installed @elabs-ai package (Tailwind does not scan node_modules, so without it everything renders unstyled), and wrap the app root in `ThemeProvider` from `@elabs-ai/components-tokens`.
4. Copy the block in with `npx shadcn@latest add https://elabs-ai.com/r/sidebar-04.json`; if that url does not answer, copy its files from https://github.com/mreimitz/elabs-components/tree/main/registry/blocks/sidebar-04 instead. Then adapt it here: [the screen or file, and the real data it should show]
5. Rules: use only @elabs-ai/components-* components and the semantic tokens — no raw colours, no raw font sizes, no arbitrary Tailwind values. Look up each component's real props with the docs tool before you use it; never guess a prop.
6. Done means: the typecheck passes, `npx -y @elabs-ai/components-cli audit src --strict` reports nothing, and you have opened the app and looked at every screen you touched in the light and the dark theme.

Dependencies

  • @elabs-ai/components-data
  • @elabs-ai/components-icons
  • @elabs-ai/components-tokens
  • @elabs-ai/components-ui
  • lucide-react

Examples

10 live examples

Frame

The frame with an empty content slot — what the shell contributes on its own, before a single message exists.

Loading

Waiting on the first page. Both zones show a layout-shaped skeleton — the same boxes the real rows and the real message will occupy — so nothing jumps when the data lands, and the region announces itself once rather than per box.

Empty

An empty mailbox. The panel says what will appear here rather than inventing an action — there is nothing for the reader to do about an empty inbox.

Filtered Empty

A search that matches nothing is a different kind of empty from an empty mailbox, so it gets a different glyph, different words, and — the part that matters — a way OUT that really works.

Filtered Empty Recovered

The way out, taken — the second half of the no-results round trip. Split from `FilteredEmpty` so each story's FINAL PAINT matches its own name. The claim here is the one that matters about an escape hatch: it is not decoration. Pressing it clears the query AND brings every row back.

Collapsed

The collapsed rail: icons only, but nothing becomes unreachable. Compose keeps a name once its word folds away, and each mailbox keeps its unread count — the visible badge is hidden in this state, so the count lives in the link's accessible name instead.

Overflowing Content

Narrow Drill Down

The phone branch — the half of this shell that a 1200px test runner has never rendered. Below `md` the two content zones stop being columns and become a drill-down: the list owns the screen until a message is opened, then hands it over and the top bar grows a back control. The viewport really changes here. `md:` and `max-md:` are VIEWPORT media queries, so constraining a wrapper's width would prove nothing — the story would render the desktop branch under a mobile name, and (because a hidden element measures zero) it would pass. Measured inside this play function: `window.innerWidth` is 414 and `matchMedia("(min-width: 48rem)")` is false, against 1200 / true everywhere else in this file. The global is per-story and restores afterwards, so no sibling story inherits it.

Just Above The Breakpoint

Just above the `md` boundary — the width band this file could not see. Every other story here renders at 1200px or, for the drill-down, at 414px: both far from 768px and on opposite sides of it. So sliding the shell's layout breakpoint one step, `md:` to `lg:`, silently switched every reader between 768px and 1023px to the phone layout — two zones collapsed to one — and the file stayed green. 800px sits inside that band. The claim asserted here is the same one the 1200px stories make: with a message open, all three zones are on screen at once. That is what makes it a breakpoint lock rather than a second desktop story — it is the identical claim, measured on the other side of a line nothing else in the file crosses.