Pattern: app launch
Who uses it: anyone opening the suite, every session —
the first screen after sign-in. What "done" looks like:
the right app is one tap away, whether it's a favourite or three deep in
a category. A suite entry screen — the icon-grid launcher (the "Bento UI"
/ tablet-launcher archetype from the
device-coverage table — same
.bo-widget-grid container-query mechanism as a dashboard, at a
smaller tile size, not a new grid component). Tiles are plain
links composed into .bo-widget cards with real
CSS icons (themable currentColor
glyphs) where a glyph is genuinely right, an initials chip where none is, and
inline <svg> where a tenant brings their own; the category row is the
ordinary tabs pattern; a "folder" tile is just a
widget whose body holds a cluster of small tiles. Zero new CSS.
Not for returning a user to a specific record they already know —
a bookmark, a notification, or a saved link should land directly on
the object page or
the list report, never route back
through the launcher grid to get there.
Favourites
All applications — category tabs
A "folder" tile — grouped apps
A folder is a single link to a category page (or a
dialog listing its apps) whose face previews the members as badges — not a
hover-expanding stack. One click, one navigation; the preview is
aria-hidden so the link's accessible name is just "Finance apps".
A searchable launcher for a catalogue too big to scan
The grids above are the answer when someone can see their app. Past roughly two screens of tiles they cannot, and scrolling a category tree to find “Receivables” is slower than typing it. This is the same catalogue behind a dialog with a search box — composed from what already ships, with no new component and no new fullscreen modifier.
Why this one is allowed to fill the
viewport. A dialog is normally kept to a measure — 32rem, because
prose stops being readable past it. A launcher is not prose: it is a grid
the reader scans, and a narrow column turns one glance into a scroll. The
sizing here is page-local, a few lines against this
dialog’s own id, not a new --fullscreen modifier that every
future dialog could reach for.
Which of the three to reach for. The tile grids above win when the catalogue fits on a screen or two — recognition beats recall, and no typing is faster than typing. This launcher wins when it does not fit and the reader knows roughly what the thing is called. The command bar wins when they know exactly, and want to jump without leaving the keyboard — it searches actions as well as apps, and it is a different keyboard model. Not for a list of records; that is a list report with a filter.
What is real here and what is not.
Opening, the search, Escape, Close and the focus moves are real, and
seven cases in check:claims drive them with real key
and pointer events — including the one that used to be
broken: with a query typed, type="search" carries a native
Escape-to-clear that consumed the key, so closing took two presses. One
press closes it now, and the build fails if that regresses. The tiles are href="#", like every other
tile on this page — they go nowhere. The dialog needs
JavaScript: initDialogs() opens it, so without JS the
trigger does nothing and the catalogue above remains the way in. That is why
the grids stay on this page rather than being replaced by the launcher.
Filtering, the result count and what the tiles link to are yours.
Markup
<!-- A tile is a link styled as a card. The grid is the SAME .bo-widget-grid
a dashboard uses — --bo-widget-min sets the tile size; the grid's own
container query reflows it on tablet/narrow widths for free. -->
<div class="bo-widget-grid" style="--bo-widget-min: 9rem">
<a href="/purchasing" class="bo-widget"
style="text-align: center; padding-block: var(--bo-space-4)">
<!-- No color on the mark: the reset's own a { color: accent } already
gives the tile its accent, and the label below opts OUT of it. -->
<span class="bo-icon bo-icon--cart" aria-hidden="true"
style="font-size: 2rem; margin-inline: auto"></span>
<span style="color: var(--bo-color-text-primary)">Purchasing</span>
</a>
…
</div>
<!-- Category row = the ordinary tabs pattern; each panel holds its own grid. --> Anatomy
- App shell — header + sidebar + main; the shell is the pattern.
- Entry tiles — a
.bo-gridof cards, each a task the user starts here. - Counts / badges — "12 awaiting approval" on a tile: the reason this screen exists rather than a static menu.
- Recent items — a short list back into records the user touched last.
One glyph per app, never reused in a launcher. A tile grid is scanned by shape before it is read, so two apps wearing the same mark cost the user the whole benefit of the grid. If no distinct glyph exists, use the initials chip rather than an approximate one — an approximate mark is worse than letters, because it is confidently wrong. This page practises that: Payables and Receivables are AP and AR rather than two near-identical document outlines, which is exactly the bug this section now rules out.
The icon set is not the limit. A launcher is product content —
your app list is yours, and no framework can ship the right glyphs for
it. Any inline <svg> drops into the same slot and inherits
currentColor, so it themes with everything else for free.
Currencies above is one: the international currency sign, hand-drawn in six
lines of SVG, with nothing added to the framework.
<a href="/currencies" class="bo-widget">
<svg viewBox="0 0 24 24" width="32" height="32" fill="none"
stroke="currentColor" stroke-width="2" aria-hidden="true">
…your mark…
</svg>
<span>Currencies</span>
</a> Data contract
| Request | Returns / on failure |
|---|---|
| GET /home | the tile grid; counts may be computed asynchronously |
| GET /home/counts | a fragment swapped into the badges once available — tiles must render WITHOUT it. On failure (5xx/timeout), the badges simply never appear; that is a silent degrade, not an error banner — a tile grid failing loudly over a missing count would be worse than the count just being absent |
| GET /recent | the recent-items list; empty for a new user |
States
| State | What ships |
|---|---|
| Loading | tiles render immediately — nothing here depends on data, only on which apps this user may open |
| Per-tile counts (if your deployment adds them) | not part of AppTile's three marks (icon / initials / svg) — a count badge is a fourth kind this component does not ship; add it as a sibling element inside the tile, and drop it silently rather than showing a stale or zero count |
| Empty (new tenant) | a first-run state naming the one setup action, not an empty grid |
| No permission | omit tiles the user cannot open — a disabled tile teaches nothing |
| Error | inline alert above the grid with retry; keep whatever tiles did load |
| No JS | the category tabs are plain <button role="tab"> elements with no native switching behavior — without initTabs()'s click handler, the second panel (marked hidden in the markup) never un-hides, so only the Finance tiles are ever reachable; the Logistics apps behind the second tab are effectively invisible |
Components used
complexity 1 of 4