Skip to content

UX guidelines

Short rules for shopper and staff screens, taken from what the platform already does. Each links to the detailed page where there is one. "Storefront" means the shop's themes; "admin" means the bx- screens.

States: empty, loading, error

  • Every list has an empty state that says what to do next. Storefront listings show a title, one sentence and a way on (<theme>.plp.emptyTitle / .emptyBody / .emptyCta). Admin uses .bx-empty with a title, description and, where there is one, the action that fills it.
  • Show a feature's empty tile or tab only when the site has the feature on. An empty state for something the shop doesn't do is noise (Account area).
  • Reserve the space while loading. Use a skeleton the size of the content (the product gallery and variant picker do this) rather than a spinner that makes the page jump.
  • A button that's working says so. Admin: aria-busy="true" on a bx-btn shows its spinner (BadgerAdminUI.setBusy). Storefront: swap the label for common.processing and disable it.
  • Errors say what happened and what to do, in the shopper's words. Never show an exception message or internal code. Shop error pages are the shop's 4xx / 5xx CMS pages, else error/4xx.html / 5xx.html (Site customisation).
  • Admin errors reach the admin error page. Don't catch an exception in a controller just to show a message; error/admin.html shows a friendly sentence and a log reference support can search for (Admin UI components).

Confirmations and destructive actions

  • Write the confirmation as the outcome, with its reach: "Removes it from 3 products", not "Are you sure?" (Admin UI components).
  • Admin dialogs use BadgerAdminUI.confirm / data-bx-confirm. A danger confirm focuses Cancel, so Enter can't delete.
  • The bigger the blast radius, the more the user types. Deleting or replacing a lot asks for a typed word (data-bx-confirm-type). Tenant actions and system initialisation ask for an exact phrase the server re-checks (TypedConfirmation.matches).
  • Actions that change who can do what ask for proof, not a password. Guard them with @RequiresStepup, and offer the form only once the grant is in place, so it's never filled in only to be bounced (Admin stepup).
  • Prefer undo to a dialog when the delete can be undone (soft delete): a toast with Undo.
  • Storefront confirmations open in place. Use <details> for a form that needs a second step (cancel an order, sign out everywhere), or th:data-confirm="#{…}" with onsubmit="return confirm(this.dataset.confirm)" for a one-line prompt. No Bootstrap modals: they differ between themes (Storefront text).

Forms

  • Every field has a visible label. A placeholder is an example, not a label.
  • Help sits under the field, errors next to it. Admin: .bx-field > .bx-label, the input, .bx-help, .bx-error, with aria-invalid="true" on the bad field. Storefront: the same order, with the error linked by aria-describedby.
  • Mark required fields, not optional ones, with * followed by the visually hidden common.required.
  • Set autocomplete on every personal field: given-name, family-name, email, tel, shipping postal-code, username, current-password, new-password, as checkout and sign-in already do. autocomplete="off" only on fields a browser must not fill (codes, search).
  • Text inputs are at least 16px on phones, so iOS doesn't zoom on focus.
  • Errors a shopper reads are message keys. Throw ShopperMessageException(key, english, args…) and resolve it where it's shown; bean validation uses {key} messages. Arguments are shown to the shopper, so no ids or enum names (Storefront text).
  • Storefront forms work without JavaScript. A plain form posted through its extension; script only improves it (the sort dropdown keeps a no-JS Apply button, account forms post normally).
  • Admin forms track changes. <form data-bx-dirty> shows the save bar and warns before leaving with unsaved changes (Admin UI components). Every admin write carries the CSRF token (Back-office CSRF protection).

Feedback

  • Admin saves show the save bar, then a toast. Use BadgerAdminUI.toast(message, type); don't add another toast library.
  • One save bar per form. Many-field pages (settings, product edit) save in one action, not per field.
  • Storefront changes are announced. Basket and checkout updates go to an aria-live region; errors that block a step use role="alert". Adding to the basket shows the added pop-over or mini basket, so the shopper sees the result without leaving the page.
  • Say what happened, not that something happened: "2 items added", "Saved 3 settings".

Accessibility (WCAG 2.2 AA)

  • Targets are at least 24 by 24px (WCAG 2.5.8). Ours are 44px on phones: page numbers, product-card buttons, breadcrumb links on phones and donation rows already are. Grow the hit area with padding or a pseudo-element, not by shrinking neighbours.
  • Focus is always visible. Style :focus-visible with the focus ring (--color-primary-ring on the storefront, --bx-focus-ring in admin); never remove the outline without a replacement.
  • Contrast is 4.5:1 for text and 3:1 for controls and focus indicators. Brand-coloured text is --color-primary-text, not --color-primary; text on a status fill is --color-on-success, -warning or -error; muted text is --color-gray-500 or darker (-400 is for icons and disabled marks). On dark sections use --color-on-dark-accent and its -contrast pair, which the Design Brief keeps legible. ./dev-scripts/ui-snapshots.sh contrast checks every pattern page (Pattern library).
  • Icon-only buttons have an aria-label from a message key (button.close, #{depot.pager.page(2)}), and the icon is aria-hidden.
  • Mark the current item with aria-current="page" (the pager, Nova's nav). Most theme navs don't yet (Storefront inventory).
  • Respect reduced motion. New animation stops under prefers-reduced-motion: reduce (Foundations).
  • Pages declare their language: th:lang="${#locale.language}" on <html>, as every Nova template does.
  • Tables stay readable on phones. Admin .bx-table turns rows into cards under 768px using each cell's data-label; storefront lists are card rows that wrap, not tables that scroll sideways (Account area).
  • Colour never carries meaning alone. A status pill has a word as well as a colour.

Content style

  • Sentence case for headings, buttons and labels: "Save changes", "Add to basket".
  • Plain English, British spelling. Say what the thing does for the reader, not what it is called internally ("Payout on its way", not "Payout event received").
  • One idea per sentence; lead with the figure or the action (the back-office emails put the amount first: Back-office emails).
  • Buttons say what they do: "Delete 3 products", not "OK".
  • Storefront wording is always a message key, including screen-reader text, placeholders and script strings. Reuse button.*, common.* and sort.* before adding a key (Storefront text). Staff-only screens and emails stay English.
  • No personal data in logs or the browser console. Log ids, never emails, names or addresses, and don't console.log form values, basket contents or Stripe objects (Observability).

Money, dates and language

  • Money: #shopMoney.format(amount) in templates, ShopMoney / ShopCurrencyService in Java, shopAmount(minor) in theme scripts. Never a typed £ (Money and currency).
  • Dates: #shopDates.format(date, pattern), never #dates or #temporals, which print UTC (Dates and time zones).
  • Counts: one choice key per phrase (catalogue.productCount), never number plus word.

Performance is UX

  • No layout shift. Product images sit in a box shaped by --product-image-ratio with object-fit: cover; give other images their width and height or an aspect-ratio.
  • Load the first image eagerly, the rest lazily. fragment/responsive-image does this when the caller sets responsiveImagePriority for the first card or the product hero.
  • Chrome on every page uses inline SVG (fragment/icons.html, or the theme's icon file), not an icon font.
  • Keep the critical path short: no new render-blocking stylesheets or third-party fonts, and page-specific CSS and JS load with the template that needs them (Performance).