BESBanking Experience System
Docs Components Journey Showcase
Draft — not yet human-verified. Every gate passing here is automated. No screen-reader, Arabic native-speaker or usability testing has been carried out yet, and all Arabic UI copy is machine-drafted. What has and hasn't been verified
Core components

09 — Core Components: Inputs & Forms

Banking Experience System (BES) · Core Components · v2.0.0-draft

Live example

This IBAN doesn't look right — UAE IBANs have 23 characters starting with AE. Paste it with or without spaces.
Leaves AED 20,000.00 available
Code
<div class="demo-stack">
  <div class="bes-field">
    <label class="bes-label" for="d-iban">Recipient IBAN</label>
    <input class="bes-input bes-input--ltr" id="d-iban" spellcheck="false" aria-invalid="true" aria-describedby="d-iban-e" value="AE07 0331 2345">
    <div class="bes-error-msg" id="d-iban-e">This IBAN doesn't look right — UAE IBANs have 23 characters starting with AE. Paste it with or without spaces.</div>
  </div>
  <div class="bes-field">
    <label class="bes-label" for="d-amt">Amount</label>
    <div class="bes-currency">
      <span class="code" aria-hidden="true">AED</span>
      <input class="bes-input" id="d-amt" inputmode="decimal" value="5,000" data-bes-currency data-max="25000" data-currency="AED" data-left-msg="Leaves {x} available" data-over-msg="That's {x} more than your available balance">
    </div>
    <div class="bes-context-line">Leaves AED 20,000.00 available</div>
  </div>
  <label class="bes-check">
    <input type="checkbox">
    <span>I accept the <a href="#">Key Facts Statement (PDF)</a><span class="desc">Version 3.2 · required to continue</span></span>
  </label>
  <label class="bes-switch">
    <input type="checkbox" checked>
    <span class="track" aria-hidden="true"></span>
    <span>International payments <span class="state">On</span></span>
  </label>
</div>

Components: Text Input · Currency Input · Search · Select · Combobox · Date Picker · Date Range Picker · Checkbox · Radio Group · Switch · PIN Entry · Code Entry · File Upload · Character Count · Form primitives (Label, Help text, Error message, Fieldset).

Form doctrine (applies to all): prevent errors before validating (accept all reasonable formats, strip stray characters); validate on submit by default — inline on-blur validation permitted only for high-confidence format fields (IBAN, card number, Emirates ID, email); on error, preserve input, show an error summary at the top with focus moved to it, prefix page title with "Error:", and pair inline messages per field. Server-side validation always; never use validation to gate eligibility (route instead). "Required" is stated in words on the minority class — if most fields are required, mark only the optional ones ("(optional)").


Text Input

Anatomy: label (always visible, above field — never placeholder-as-label) · field · optional prefix/suffix slots · help text · error message (icon + text, border.error) · optional character count.

States: default · hover · focus · filled · disabled · read-only (visually distinct from disabled; used for verified prefilled data) · error · ✦ verified (check + source note: "From UAE PASS") · ✦ masked (sensitive entries).

Usage: one thing per field; forgiving inputs (trim, collapse spaces); inputmode/autocomplete set per data type; paste never blocked — especially on IBANs, card numbers and passwords.

Financial instances: IBAN input (LTR always, even in RTL forms; groups display as AE07 0331 2345 6789 0123 456; strips spaces/hyphens on paste; validates checksum on blur with specific message: "This IBAN doesn't look right — UAE IBANs have 23 characters starting with AE"); reference/purpose field (character count, disallowed-character stripping for payment rails).

A11y: programmatic label association; error text linked via description; error icon not color-only; 16px+ font (prevents mobile zoom-jump).

RTL: label/help right-aligned; the field itself may hold an LTR run (IBAN, email) with dir="ltr" embedding and start-anchored caret behavior documented; prefix/suffix flip logically (a currency suffix in Arabic appears at the visual left).


Currency Input

Purpose: the most consequential input in the system — amount entry.

Anatomy: label · currency indicator (fixed code or currency selector) · amount field (type.amount.lg, tnum) · optional quick-amount chips (AED 100 / 500 / 1,000 / Max) · context line (available balance and/or remaining preview) · help/error.

Behavior: locale-aware live grouping (10,000); precision clamp per currency.precision; forgiving paste ("AED 1,250.50" → 1250.50, "٥٬٠٠٠٫٥٠" → 5000.50); no spinner arrows; Backspace works on groups naturally; Eastern/Western digit entry both accepted, normalized (doc 07 §6). Paste is forgiving about form, never about meaning: input that could be read two ways ("1.234,56") is rejected rather than guessed, because a silently wrong amount is worse than a rejected one. An empty or half-typed field is incomplete, not in error — it carries no aria-invalid.

States: default · focus · error (over limit, over balance, below minimum — each with specific copy and the offending constraint shown: "The maximum for instant transfers is AED 50,000.00") · ✦ balance-exceeded (shows shortfall: "AED 1,200.00 more than your available balance") · disabled.

Financial: "Max" chip uses balance.available minus known fees, stated ("Max sends AED 24,985.00 after fees"); remaining-balance preview updates live: "Leaves AED 1,250.00". For FX flows, paired dual fields (you send / they get) with last-edited field as source of truth and the other marked ≈.

A11y: announces formatted value on blur ("Amount: 5,000 dirhams"); quick chips are buttons with full labels; error messages reference limits numerically. The context line is structurally bound to the field — it carries an id, the field references it via aria-describedby, and it is a polite live region — so aria-invalid is never set without a reachable reason (WCAG 3.3.1, 4.1.3). Setting a validation message without that binding is a build defect, not a content gap: the field goes invalid and the user is never told why.

RTL: amount remains LTR isolated run; currency indicator at logical start (visual right in Arabic); chips mirror order.


Search

Anatomy: input with search icon · clear button (when filled) · optional scope selector · results behavior defined by consuming pattern.

States: default · focus · filled · loading (inline spinner) · no-results (with recovery: "No transactions matching 'Careem'. Try a shorter word or filter by date.").

Financial: transaction search matches merchant, amount ("89.50"), reference, and normalized Arabic/Latin merchant names; recent searches stored locally, excluded for shared/kiosk surfaces.

RTL/A11y: icon at inline-start; clear button ≥44px; results count announced.


Select

Anatomy: label · trigger (value + chevron) · listbox surface · options (text + optional icon/description).

States: default · open · focus · disabled · error · option selected/disabled.

Usage: ≤ ~12 known options (account pickers, statement periods); more → Combobox. Never for binary choices (use Radio/Switch).

Financial instance — Account Selector is a specialized Select (doc 14) showing name, masked number, available balance per option.

A11y: native select on mobile web where feasible; otherwise full listbox keyboard pattern; selected option announced with its description ("Current account ••4521 — AED 25,000.00 available").

RTL: chevron mirrors; option icons inline-start.


Combobox

Purpose: filterable long lists — beneficiary picker, bank picker (200+ institutions), country/currency picker.

Anatomy: input + listbox with type-ahead filtering · optional grouped sections (Recent / All) · empty state with add-new path ("No beneficiary found — Add new beneficiary").

States: as Select + filtering · loading async options.

Financial: bank picker matches English and Arabic names and SWIFT codes; country picker for remittance shows corridor availability and delivery methods inline.

A11y: combobox pattern with live results count ("6 banks found"); selection persists on blur; no keyboard traps.


Date Picker / Date Range Picker

Anatomy: labeled input (typed entry always allowed — "31/08/2026") · calendar surface · month/year navigation · today marker · range: start/end fields + span highlight.

States: default · open · focus · error (invalid, out-of-window: "Statements are available for the last 7 years") · disabled dates (with reason on focus: settlement holidays).

Usage: memorable dates (DOB, expiry) use segmented fields (DD/MM/YYYY), never a calendar. Ranges offer presets first: This month · Last month · Last 90 days · Custom.

Financial: scheduled transfers block non-processing days with explanation ("Transfers don't process on Sunday. We'll send it Monday 1 Sep."); future-dating shows the effective debit date. Hijri display: secondary line where enabled ("1 Muharram 1449"), Gregorian remains canonical for processing.

A11y: full keyboard grid navigation; typed entry as first-class path; announced format hint.

RTL: calendar grid mirrors (week start per locale — Sat/Sun/Mon configurable, UAE default Monday); numerals per locale policy; chevrons mirror.


Checkbox

Anatomy: box · label (tappable as one target) · optional description · optional indeterminate (bulk tables only).

States: unchecked · checked · indeterminate · focus · disabled · error.

Usage: independent opt-ins and multi-select. Consent rules: one checkbox per consent — never bundled ("I agree to terms and marketing" is banned); never pre-ticked; consent checkboxes carry the purpose inline, not only behind a link.

A11y: native input semantics; error at group level for "you must accept" cases with a specific message ("You need to accept the Key Facts Statement to continue").

RTL: box at inline-start (visual right in Arabic); check glyph unmirrored.


Radio Group

Anatomy: group label (fieldset/legend) · options (radio + label + optional description + optional trailing content like a fee).

States: as checkbox minus indeterminate; exactly-one-selected model, no deselection.

Financial instance — option cards: payout method selection renders radios as cards with fee/time metadata ("Bank transfer — fee AED 15.00, within 1 hour" / "Cash pickup — fee AED 20.00, minutes"). Selection re-renders the FX block with change highlight.

A11y: arrow-key navigation within group; group label announced first; metadata part of the option's accessible description.


Switch

Purpose: immediate-effect binary settings (card freeze, international payments on/off, balance privacy).

Anatomy: track+thumb · label · state word ("On"/"Off") adjacent — color alone never carries state.

States: on · off · focus · disabled · pending (async settings show an in-flight state; the switch does not lie by flipping before the backend confirms).

Usage: never inside forms that submit (use Checkbox); never for choices needing confirmation — freezing a card may flip instantly, but unfreezing into higher risk may route through confirmation (pattern decides).

A11y: switch role with state announced; pending announced ("Turning off international payments…").

RTL: thumb rests at inline-start when off; motion mirrors.


PIN Entry

Purpose: local secrets — card PIN set/change, app PIN.

Anatomy: masked digit cells (4–6) · system or custom keypad · paste allowed (WCAG 2.2 §3.3.8 Accessible Authentication — password managers must work; velocity/fraud controls belong server-side) · shuffled-keypad option (high-security surfaces) · attempt counter messaging.

States: empty · filling · error (with remaining attempts: "Incorrect PIN. 2 attempts remaining.") · locked (with recovery path and support link — never a dead end).

A11y: cells announced as "PIN digit 1 of 4, filled"; timeout generous; no haptic-only feedback. Screen-capture blocked on this surface.

RTL: digits LTR always; cells fill left-to-right in both directions (numeric convention), documented so testers don't file it as a bug.


Code Entry (one-time codes)

Purpose: verification codes from email/authenticator/push contexts. Note: SMS OTP is deprecated as an authentication method (sunset 31 Mar 2026 per regulator); this component ships for residual non-auth verifications and legacy migration only, and carries the deprecation notice in its doc.

Anatomy: 6 cells with auto-advance · paste-from-clipboard supported (unlike PIN) · resend with countdown · "didn't get it?" recovery.

States: empty · filling · verifying (spinner, cells locked) · error (specific: expired vs wrong) · resent confirmation.

A11y: single input semantically (one field, styled as cells) so screen readers treat it as one code; autocomplete="one-time-code".


File Upload

Purpose: KYC documents, dispute evidence, address proof.

Anatomy: dropzone/button ("Upload trade license") · accepted formats + max size stated upfront · file rows (name, size, thumbnail for images, remove) · progress per file · camera-capture path on mobile.

States: idle · dragging-over · uploading (per-file progress, cancellable) · uploaded · error (too large / wrong format / failed — each with the fix: "Maximum size is 10 MB. Try a photo instead of a scan.") · ✦ verifying (document under review) · ✦ verified / rejected (with reason and re-upload path).

A11y: button path is primary (drag is enhancement); progress announced; errors focusable.

RTL: file rows mirror; filenames are LTR isolated runs.


Form primitives

  • Label: always visible, above the field, sentence case; optional-marker "(optional)".
  • Help text: beneath label, before input; persistent (not tooltip-hidden); used for "why we ask" in KYC ("We're required to verify your address. A utility bill from the last 3 months works.").
  • Error message: icon + specific text; says what to do, not just what's wrong ("Enter the IBAN without spaces — we'll format it for you" beats "Invalid IBAN").
  • Error summary: top-of-form list of links to each error; receives focus on failed submit; title "There's a problem".
  • Fieldset/legend: groups related inputs (address, card details) for screen-reader context.

All components in this family — live

Every component from this specification, rendered by the coded library. Expand "Code" to copy the markup.

Select & Combobox

Code
<div class="demo-row" style="align-items:flex-start">
  <div class="bes-field" style="inline-size:220px;margin-bottom:0">
    <label class="bes-label" for="si-per">Statement period</label>
    <div class="bes-select"><select id="si-per"><option>August 2026</option><option>July 2026</option><option>June 2026</option></select></div>
  </div>
  <div class="bes-field" style="inline-size:300px;margin-bottom:0">
    <label class="bes-label" for="si-bank">Recipient's bank (type to filter)</label>
    <div class="bes-combo">
      <input class="bes-input" id="si-bank" role="combobox" aria-expanded="false" aria-autocomplete="list" autocomplete="off" placeholder="Search by name or code">
      <ul class="bes-combo-list" hidden>
        <li data-value="enbd" data-label="Emirates NBD">Emirates NBD <span class="s">EBILAEAD</span></li>
        <li data-value="fab" data-label="First Abu Dhabi Bank">First Abu Dhabi Bank <span class="s">NBADAEAA</span></li>
        <li data-value="icici" data-label="ICICI Bank (India)">ICICI Bank (India) <span class="s">ICICINBB</span></li>
        <li class="empty" hidden>No bank found — check the SWIFT code</li>
      </ul>
      <span class="bes-combo-count" aria-live="polite"></span>
    </div>
  </div>
</div>

Date picker — processing-day aware

Code
<div class="bes-datepicker" data-bes-datepicker data-disabled-days="0" data-disabled-note="Transfers don't process on Sundays — we'll send the next business day."></div>

PIN & code entry

App PIN (no paste)

Verification code (paste allowed)

Code
<div class="demo-row" style="align-items:flex-start;gap:32px">
  <div><p class="bes-label" style="margin:0 0 8px">App PIN (no paste)</p>
    <div class="bes-pin">
      <input aria-label="PIN digit 1" type="password"><input aria-label="PIN digit 2" type="password">
      <input aria-label="PIN digit 3" type="password"><input aria-label="PIN digit 4" type="password">
    </div>
  </div>
  <div><p class="bes-label" style="margin:0 0 8px">Verification code (paste allowed)</p>
    <div class="bes-pin bes-pin--code">
      <input aria-label="Code digit 1"><input aria-label="Code digit 2"><input aria-label="Code digit 3">
      <input aria-label="Code digit 4"><input aria-label="Code digit 5"><input aria-label="Code digit 6">
    </div>
  </div>
</div>

File upload & character count

PDF or photo · max 10 MB
trade-license-2026.pdf 1.2 MB · Verified
Code
<div class="demo-stack" style="max-width:460px">
  <div class="bes-upload">
    <button class="bes-btn bes-btn--secondary bes-btn--sm" type="button">Upload trade license</button>
    <div class="hint">PDF or photo · max 10 MB</div>
  </div>
  <div class="bes-file">
    <span class="n">trade-license-2026.pdf</span>
    <span class="meta">1.2 MB · <span style="color:var(--bes-status-success);font-weight:600">Verified</span></span>
  </div>
  <div class="bes-field" style="margin-bottom:0">
    <label class="bes-label" for="si-ref">Payment reference <span class="opt">(optional)</span></label>
    <input class="bes-input" id="si-ref" maxlength="35" value="Rent September">
    <div class="bes-charcount" data-bes-charcount="si-ref"></div>
  </div>
</div>

Radio group as option cards

Code
<div class="demo-stack" style="max-width:420px" role="radiogroup" aria-label="Payout method">
  <label class="bes-check" style="border:1px solid var(--bes-border-input);border-radius:12px;padding:13px 14px">
    <input type="radio" name="si-payout" checked>
    <span><b>Bank transfer</b><span class="desc">Fee AED 15.00 · within 1 hour</span></span>
  </label>
  <label class="bes-check" style="border:1px solid var(--bes-border-input);border-radius:12px;padding:13px 14px">
    <input type="radio" name="si-payout">
    <span><b>Cash pickup</b><span class="desc">Fee AED 20.00 · minutes · 2,400 locations</span></span>
  </label>
</div>