11 — Core Components: Containers & Surfaces
Banking Experience System (BES) · Core Components · v2.0.0-draft
Live example
Remove beneficiary Rajesh Kumar?
You'll need to add them again to send money — and new payees may have a 24-hour holding period.
Code
<div class="demo-row" style="align-items:flex-start">
<div class="bes-card" style="max-width:300px">
<div style="font-weight:600;margin-bottom:4px">Card container</div>
<div style="font-size:13.5px;color:var(--bes-text-secondary)">One concept per card. Dense surfaces prefer flat sections with dividers.</div>
</div>
<button class="bes-btn bes-btn--secondary" data-bes-open="doc-modal">Open confirm dialog</button>
</div>
<div class="bes-scrim" id="doc-modal" role="dialog" aria-modal="true" aria-labelledby="doc-m-t">
<div class="bes-modal">
<h3 id="doc-m-t">Remove beneficiary Rajesh Kumar?</h3>
<p>You'll need to add them again to send money — and new payees may have a 24-hour holding period.</p>
<div class="actions">
<button class="bes-btn bes-btn--secondary" data-bes-close="doc-modal">Keep beneficiary</button>
<button class="bes-btn bes-btn--destructive-solid" data-bes-close="doc-modal">Remove beneficiary</button>
</div>
</div>
</div>Components: Card · List & List Item · Table · Accordion · Modal Dialog · Drawer · Bottom Sheet · Popover · Tooltip · Divider.
Card
Anatomy: container (surface.primary, elevation.1, radius.md) · optional header (title + action) · body · optional footer.
Variants: static (grouping) · interactive (whole card tappable — one action only; internal buttons preclude whole-card tap) · summary (dashboard modules).
States: default · hover (interactive only) · focus · pressed · loading (skeleton body) · error (inline retry) · empty.
Usage: cards group one concept. Anti-decoration rule: no card-inside-card beyond two levels; if everything is a card, nothing is — dense data surfaces (tables, corporate) prefer flat sections with dividers.
Financial: money summary cards state their freshness when not real-time ("As of 14:32"); a card containing a balance respects the masked-balance setting.
A11y: interactive card is a single named control ("Current account, AED 25,000.00 available, view details"); internal structure headed properly.
RTL: header action at inline-end; content mirrors via logical properties.
List & List Item
Anatomy (item): leading slot (icon/avatar/logo) · primary text · secondary text · trailing slot (amount/chevron/status/control) · optional third line (metadata).
Variants: navigation item · selection item (radio/checkbox embedded) · display item · transaction item (specialized, doc 15).
States: default · hover · focus · pressed · selected · disabled · swipe-actions (mobile: max 2 per side, destructive requires confirm — swipe never completes a destructive act alone).
Usage: height per density (64/56/44); dividers border.default inset from leading slot; grouped lists with sticky section headers (dates in transaction lists).
A11y: whole row is the target for navigation items; embedded controls get discrete focus stops; three-line max for readability.
RTL: leading/trailing swap visually; amounts in trailing slot remain LTR runs, end-aligned.
Table
Anatomy: caption/title · column headers (sortable: label + direction arrow) · rows · numeric columns end-aligned tnum · optional row selection checkboxes · sticky header · footer (totals) · toolbar (search, filters, density toggle, export).
Variants: simple · data table (sort/filter/select/bulk) · comparison table (products; first column sticky).
States: default · loading (skeleton rows preserving column widths) · empty (with cause + action) · error (retry) · row hover/selected · sorted column · filtered (active-filter chips above, clearable).
Usage: desktop/SME/corporate density workhorse. On mobile, tables transform — defined per table: transaction tables become Lists; comparison tables become swipeable cards; approval tables become stacked cards with inline actions. A horizontally scrolling raw table on mobile is a last resort and must keep the identifying column sticky.
Financial: amount columns end-aligned with sign and color rules (doc 07); totals row uses type.amount.md; bulk selection surfaces a summary bar ("12 payments selected — total AED 84,300.00") before bulk actions; sort default is newest-first for time data.
A11y: real table semantics (th/scope); sort state announced; row actions reachable by keyboard; selection count live-announced.
RTL: column order mirrors; numeric columns stay end-aligned (visual left in Arabic); sort arrows unmirrored (vertical semantics).
Accordion
Anatomy: header (label + optional metadata + chevron) · panel.
Usage: progressive disclosure of secondary detail (fee breakdowns, FAQ, transaction metadata). Disclosure rule: anything the user must see to decide (total fees, key risks) is outside accordions on review screens — accordions elaborate, they never hide obligations.
States: collapsed · expanded · focus · disabled.
A11y: button header with expanded state; panel labeled by header; multiple-open allowed (no auto-collapse on data surfaces).
RTL: chevron mirrors (points inline-start when open per convention chosen: chevron-down rotates — unmirrored vertical rotation, documented).
Modal Dialog
Anatomy: scrim · surface (elevation.3) · title · body · action row (primary at inline-end, cancel at inline-start) · close (only when closing is safe).
Variants: confirmation (decision) · alert (acknowledge) · task (short embedded form) — anything longer is a screen, not a modal.
States: open · closing-guard (unsaved input → confirm discard).
Usage: modals interrupt; reserve for decisions that must block ("Remove beneficiary Rajesh Kumar? You'll need to add them again to send money."). Money-moving confirmation happens on screens, not modals — a modal confirm for AED 250,000 is too light. Destructive confirms name the object and consequence; buttons echo outcomes ("Remove beneficiary" / "Keep beneficiary").
A11y: focus trapped, initial focus on the least-destructive action, Esc closes (unless closing-guard), focus returns to trigger; title is the accessible name.
RTL: action order mirrors with logical placement; close at inline-end.
Drawer (desktop side panel)
Anatomy: surface sliding from inline-end · header (title + close) · body · optional footer actions.
Usage: contextual detail beside data (transaction detail next to the table, approval detail in corporate queues) — preserves list context. Width 400–560px; push or overlay per surface density.
States: open · closed · stacked (max 2; breadcrumb within).
A11y: focus management as dialog when overlay, as region when push; close returns focus to the row.
RTL: slides from inline-end (visual left in Arabic).
Bottom Sheet (mobile)
Anatomy: grabber · title · content · detents (medium/full) · scrim.
Usage: contextual actions, pickers, short confirmations, in-context explanations ("Why this fee?"). Full flows use screens. Critical warnings (fraud) never use sheets (dismissable-by-swipe is too weak).
States: medium · full · dragging · closing-guard for inputs.
A11y: dialog semantics; swipe-to-close paired with a visible close button; grabber not the only affordance.
Popover
Anatomy: anchored surface (elevation.2) with arrow · content (rich: text, links, small forms) · close on outside-tap/Esc.
Usage: desktop contextual detail (rate breakdown hover-to-detail, calendar legends). On touch, popovers become bottom sheets automatically.
A11y: triggered by click/Enter (never hover-only for content that matters); focusable content reachable.
RTL: anchors and arrows flip logically.
Tooltip
Anatomy: short text label on hover/focus.
Usage: names icon-only controls; ≤ 8 words; never contains information unavailable elsewhere; never on mobile as the sole explanation (use help text or an info popover→sheet).
A11y: appears on focus as well as hover; not the accessible-name mechanism itself (that's aria-label) but supplementary.
Divider
border.default, 1px; section dividers full-bleed, item dividers inset from leading content; vertical dividers only in toolbars/dense headers. Semantic (hr) only when it separates true sections for assistive tech; otherwise decorative and hidden.
All components in this family — live
Every component from this specification, rendered by the coded library. Expand "Code" to copy the markup.
List & data table
| Payee | Amount (AED) | Status |
|---|---|---|
| Gulf Star Trading LLC | 250,000.00 | Awaiting approval |
| DEWA | 843.50 | Completed |
| Etisalat | 399.00 | Completed |
Code
<div class="demo-stack">
<div class="bes-list" style="max-width:460px">
<button class="bes-item" type="button">
<span class="main"><span class="t">Statements</span><br><span class="s">Monthly PDF and CSV</span></span>
<span class="trail" style="color:var(--bes-text-disabled)">›</span>
</button>
<button class="bes-item" type="button">
<span class="main"><span class="t">Limits</span><br><span class="s">Daily transfer AED 50,000</span></span>
<span class="trail" style="color:var(--bes-text-disabled)">›</span>
</button>
</div>
<div class="bes-table-wrap">
<table class="bes-table" data-bes-sortable>
<thead><tr><th data-sort="text">Payee</th><th class="num" data-sort="num">Amount (AED)</th><th>Status</th></tr></thead>
<tbody>
<tr><td>Gulf Star Trading LLC</td><td class="num">250,000.00</td><td><span class="bes-badge bes-badge--warning">Awaiting approval</span></td></tr>
<tr><td>DEWA</td><td class="num">843.50</td><td><span class="bes-badge bes-badge--success">Completed</span></td></tr>
<tr><td>Etisalat</td><td class="num">399.00</td><td><span class="bes-badge bes-badge--success">Completed</span></td></tr>
</tbody>
</table>
</div>
</div>Accordion — elaborates, never hides obligations
Why this fee?
When does the money arrive?
Code
<div class="bes-accordion" style="max-width:460px">
<details><summary>Why this fee?</summary><div class="body">The transfer fee covers processing through the international payment network. Full <a href="#">schedule of charges (PDF)</a>.</div></details>
<details><summary>When does the money arrive?</summary><div class="body">Transfers to India usually arrive within 1 hour. Same-day cut-off is 22:00 UAE time.</div></details>
</div>Drawer
Code
<button class="bes-btn bes-btn--secondary" data-bes-drawer-open="dc-drawer">Open transaction drawer</button>
<div class="bes-drawer-scrim" id="dc-drawer-scrim"></div>
<aside class="bes-drawer" id="dc-drawer" role="dialog" aria-labelledby="dc-dr-t">
<div class="head"><h3 id="dc-dr-t">Carrefour · <bdi class="bes-amount">−AED 89.50</bdi></h3>
<button class="bes-iconbtn" data-bes-drawer-close="dc-drawer" aria-label="Close">✕</button></div>
<div class="body">
<div class="bes-rows">
<div class="bes-row"><span class="k">Status</span><span class="v"><span class="bes-badge bes-badge--success">Completed</span></span></div>
<div class="bes-row"><span class="k">Card</span><span class="v"><bdi class="bes-amount">••8834</bdi> · Contactless</span></div>
<div class="bes-row"><span class="k">Reference</span><span class="v"><bdi class="bes-amount">TXN-88213-AE</bdi></span></div>
</div>
</div>
</aside>Tooltip & popover
Code
<div class="demo-row">
<span class="bes-pop">
<button class="bes-iconbtn bes-iconbtn--bordered" aria-label="Copy IBAN">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none"><rect x="6" y="6" width="9" height="9" rx="1.5" stroke="currentColor" stroke-width="1.5"/><path d="M12 6V4.5A1.5 1.5 0 0 0 10.5 3H4.5A1.5 1.5 0 0 0 3 4.5v6A1.5 1.5 0 0 0 4.5 12H6" stroke="currentColor" stroke-width="1.5"/></svg>
</button>
<span class="tip" role="tooltip">Copy IBAN</span>
</span>
<span style="font-size:13px;color:var(--bes-text-secondary)">Hover or focus the button — tooltips name icon-only controls, never carry unique information.</span>
</div>