Responsive web app
One web app for desktop, tablet and phone browsers. Developers build it once with AppShell; the shell and its components rearrange themselves by the width they're given, so there is no phone variant to build or keep in sync. Feature VI-017 · PRODUCT. (The native mobile app is a separate surface with its own React Native layout.)
Three layouts
| Width of the shell | Layout | Vault | Actions | Chat dock | Give feedback | Peeks, Go deeper, Sources |
|---|---|---|---|---|---|---|
| ≥ 1024 | wide |
Docked left, open | Toolbar top-right, with labels | Bottom-center, clear of the Vault | Floating bottom-right | Card; side panel on the right |
| 720 – 1023 | medium |
Drawer over the map, closed (☰ top-left) | Toolbar, icons only (counts as badges) | Bottom-center | Floating bottom-right | Card; sheet below 900px |
| < 720 | compact |
Bottom sheet (☰ in the top bar) | Top bar: ☰ · title · Edit · Sources · ⋯ (Export, Help) | Full width; hidden while a sheet or the editing bar is up | In the Vault footer | Bottom sheets |
The layout follows the shell's own width (a ResizeObserver), not the screen — a split-screen window, an embedded frame or the docs preview all get the right layout. Touch behaviour is separate and follows the pointer in use (VisualMap interaction="auto"): an iPad in landscape gets the wide layout with tap-to-peek.
Building it
import { AppShell, Vault, VisualMap, ChatTurn, Composer, exportItems } from '@visualli/design-system/components';
<AppShell
title={doc.title}
vault={<Vault entries={entries} activeId={id} onOpen={open} onNew={create} onExport={exportAs} user={user} onComfort={showComfort} />}
actions={[
{ id: 'edit', label: 'Edit', icon: 'pencil', pressed: editing, pinned: true, onClick: toggleEdit },
{ id: 'sources', label: 'Sources', icon: 'paperclip', count: sources.length, pressed: showSources, pinned: true, onClick: toggleSources },
{ id: 'export', label: 'Export', icon: 'download', menu: exportItems(exportCurrent) },
{ id: 'help', label: 'Help', icon: 'help-circle', quiet: true, onClick: openHelp },
]}
map={<VisualMap doc={doc} editing={editing} onEditingChange={setEditing} aside={sourcesAside} … />}
dock={<>{turns.map((t) => <ChatTurn key={t.id} {...t} />)}<Composer value={draft} onChange={setDraft} onSend={send} /></>}
onFeedback={() => window.Quackback?.('open')}
>
{/* dialogs: Comfort & appearance … */}
</AppShell>
What AppShell does for you:
- Vault: wires Hide vault, closes the drawer or sheet after Open and New Visualli, closes on Escape or a tap on the scrim, and moves Give feedback into the Vault footer on phones.
- Actions: one list.
pinnedactions (at most two) stay in the phone top bar; the rest, andmenuitems under their label, go into ⋯.quietactions (Help) are icon-only after a divider.countshows as a pill on wide and a badge elsewhere. - Map: sets
insets(room for the Vault, top bar and dock) and fills the shell; on phones the zoom controls sit under the top bar, opposite the depth trail. - Room for panels: while a side panel is docked, the toolbar, zoom and dock slide left of it and Give feedback hides; while a sheet or the phone editing bar is up, the dock steps aside.
- Phone browsers:
height: 100dvh(the composer never hides under the address bar), safe-area padding for the notch and home bar, and a 16px composer so iOS doesn't zoom on focus.
On the page
Add these to the web app's index.html so it behaves like an app on phones:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#f6f3ee" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#0f1220" media="(prefers-color-scheme: dark)">
viewport-fit=cover turns on the safe-area insets AppShell already uses. Don't set maximum-scale or user-scalable=no — people who zoom the page need to.
Rules
- Every feature stays reachable at every width; nothing is desktop-only. If an action doesn't fit, it moves (⋯, Vault footer), it doesn't disappear.
- Touch targets are at least 44px on phones (the top-bar buttons are 44px).
- Force a layout only to test it:
<AppShell layout="compact">. In the docs, the Web app page's Desktop / Tablet / Phone toggle renders the prototype at real widths. - Breakpoints live in
SHELL_BREAKPOINTS({ medium: 720, wide: 1024 });shellLayoutFor(width)gives the layout for a width.
