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-emptywith 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 abx-btnshows its spinner (BadgerAdminUI.setBusy). Storefront: swap the label forcommon.processingand 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/5xxCMS pages, elseerror/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.htmlshows 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. Adangerconfirm 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), orth:data-confirm="#{…}"withonsubmit="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, witharia-invalid="true"on the bad field. Storefront: the same order, with the error linked byaria-describedby. - Mark required fields, not optional ones, with
*followed by the visually hiddencommon.required. - Set
autocompleteon 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-liveregion; errors that block a step userole="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-visiblewith the focus ring (--color-primary-ringon the storefront,--bx-focus-ringin 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,-warningor-error; muted text is--color-gray-500or darker (-400is for icons and disabled marks). On dark sections use--color-on-dark-accentand its-contrastpair, which the Design Brief keeps legible../dev-scripts/ui-snapshots.sh contrastchecks every pattern page (Pattern library). - Icon-only buttons have an
aria-labelfrom a message key (button.close,#{depot.pager.page(2)}), and the icon isaria-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-tableturns rows into cards under 768px using each cell'sdata-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.*andsort.*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.logform values, basket contents or Stripe objects (Observability).
Money, dates and language¶
- Money:
#shopMoney.format(amount)in templates,ShopMoney/ShopCurrencyServicein Java,shopAmount(minor)in theme scripts. Never a typed£(Money and currency). - Dates:
#shopDates.format(date, pattern), never#datesor#temporals, which print UTC (Dates and time zones). - Counts: one
choicekey per phrase (catalogue.productCount), never number plus word.
Performance is UX¶
- No layout shift. Product images sit in a box shaped by
--product-image-ratiowithobject-fit: cover; give other images their width and height or anaspect-ratio. - Load the first image eagerly, the rest lazily.
fragment/responsive-imagedoes this when the caller setsresponsiveImagePriorityfor 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).