9.3 KiB
| name | description | metadata | ||
|---|---|---|---|---|
| odoo-web-design | Professional finish for the Odoo frontend of this repo — visual polish, responsive layout and WCAG accessibility across CSS, QWeb/HTML and JS. Use when styling or reviewing website/portal pages (website_sale_aplicoop), adding UI components, or fixing responsive/a11y/design-consistency issues. |
|
Web design & finish (Odoo 18 frontend)
Covers the three layers together: static/src/css/, static/src/js/, and the QWeb templates in
views/*.xml. For language-level conventions see odoo-qweb-html, odoo-javascript,
odoo-xml-views; this skill is about the finish: does it look designed, does it work at 320 px,
does it work without a mouse.
Inherited non-negotiables
- No logic in QWeb — prepare everything in the controller.
- No business logic in JS — UI and events only.
- No inline
style="..."in templates. A style needs a class, and the class needs a token. - Every user-visible string is translatable (see §4).
1. Work order
Never start by editing a component. Always:
- Audit with the commands in §7. Write down what is inconsistent before touching anything.
- Tokens first (
base/variables.css). Every value a component needs must already exist as a token. - Component by component, one file each, media queries co-located at the bottom of that file.
- Accessibility pass per template, top to bottom.
- Verify — lint, contrast, keyboard-only, 320 px, 200 % zoom.
Doing 3 before 2 is how a codebase ends up with 61 !important and three shades of the same blue.
2. Design tokens
Odoo 18 ships Bootstrap 5.3 with $variable-prefix: ''
(/media/snt/data/odoo/ocb/addons/web/static/src/scss/bootstrap_overridden.scss:51), so Bootstrap's
root custom properties are emitted unprefixed into web.assets_frontend (via
import_bootstrap.scss → @import "root"). Available in plain CSS, no SCSS needed:
--primary --secondary --success --danger --warning --info --light --dark
--body-color --body-bg --emphasis-color --secondary-bg --tertiary-bg --border-color
--border-radius --border-radius-sm --border-radius-lg --border-radius-xl --border-radius-pill
--link-color --link-hover-color --focus-ring-color --focus-ring-width
--box-shadow --box-shadow-sm --box-shadow-lg --font-sans-serif --gray-100 … --gray-900
Rules:
- Alias, don't copy.
--ac-color-primary: var(--primary, #007bff);keeps the website theme editor in control of the palette. A hardcoded brand hex breaks theming silently. - Hex literals live only in
base/variables.css. A hex anywhere else is a defect. - Prefix new tokens (
--ac-…, aplicoop). Bare names like--primary-colorsit in Bootstrap's unprefixed namespace and will collide as Odoo adds tokens. - Never redefine a token inside a media query to shrink it — use
clamp()so it scales continuously instead of jumping at one width. - Space and radius come from the scale, never from a typed-in number. If you need a value the scale doesn't have, the scale is wrong — fix the scale.
3. Responsive
- Mobile-first,
min-widthonly. Bootstrap 5 breakpoints exclusively: 576 / 768 / 992 / 1200 / 1400. No 480/720/1600 one-offs — a private breakpoint is a design decision nobody else knows about. - One range per rule. Pairing
min-widthandmax-widthto carve a band is almost always a symptom of desktop-first thinking leaking in. - Media queries belong with their component, at the bottom of its file. A central
responsive.cssdrifts out of sync with the components it overrides and forces!important. Don't add to it; empty it as you touch each component. If a rule inresponsive.cssspans multiple components or has no clear owner, move it to the relevant layout partial (e.g.layout/grid.css) rather than duplicating it per component; note any such moves in the audit output. - Prefer intrinsic layout to breakpoints:
repeat(auto-fill, minmax(min(100%, 16rem), 1fr)),clamp(),flex-wrap. Most grids need zero media queries. remfor everything;pxonly for hairlines, borders and shadow offsets.- Touch targets: 24×24 px is the AA floor (WCAG 2.2 §2.5.8); 44×44 px is §2.5.5, level AAA,
and the size a thumb actually needs — apply it under
@media (pointer: coarse). Quantity steppers and icon buttons are the usual offenders. - Must survive 320 px wide and 200 % zoom with no horizontal scroll.
Details and recipes: references/responsive.md.
4. Accessibility — WCAG 2.2 AA baseline
- Semantics before ARIA:
<button>,<nav>,<ul>,<label for>. ARIA only when no element expresses the meaning. A<div onclick>is never acceptable. - Contrast: 4.5:1 body text, 3:1 for ≥24 px (or bold ≥19 px), 3:1 for form-control borders, icon-only buttons and focus rings.
- Focus: never
outline: nonewithout a replacement. Style:focus-visible, not:focus— the latter also fires on mouse click and gets removed out of frustration. - Every input has a real
<label>. A placeholder is not a label. - Icon-only control → translatable
aria-label, or.visually-hiddentext (Bootstrap ships the class). - Dynamic updates (cart total, quantity, result count) →
aria-live="polite"region. - Images: meaningful
alt, oralt=""when decorative. Never omit the attribute. - Status is never conveyed by colour alone — pair it with text or an icon.
- Every animation you add needs a
@media (prefers-reduced-motion: reduce)escape.
Translatable QWeb attributes (auto-extracted, odoo/tools/translate.py:163), plus their
t-attf- forms:
string help placeholder alt title label data-tooltip confirm
aria-label aria-keyshortcuts aria-placeholder aria-roledescription aria-valuetext
aria-description is not translated — use aria-label or visually-hidden text instead.
Strings injected from JS go through window.i18nManager.get(key) (served by /eskaera/i18n), with
the key added server-side — never a hardcoded literal, including in live-region announcements.
Per-component patterns: references/accessibility.md.
5. JS for finish (UI only)
- Delegated events — content is lazy-loaded and infinite-scrolled, direct listeners die.
- After injecting nodes: move or restore focus deliberately, announce the new count in the live
region, and set
aria-busyon the container while fetching. - Check
matchMedia("(prefers-reduced-motion: reduce)")before animating or smooth-scrolling. - Toggle classes, don't write
element.style.*— inline styles from JS are the same defect as inline styles in templates, just later. - Debounce search input ≥ 200 ms and announce "N results" politely.
- Batch DOM reads and writes; never read
offsetHeightinside a loop that also writes.
6. Asset delivery
website_sale.css pulls its 17 partials with @import. Odoo rewrites those to absolute URLs and
hoists them to the top of the bundle (odoo/addons/base/models/assetsbundle.py:909 and :525), so
they ship as ~17 extra render-blocking requests outside the minified bundle. For a production
finish, prefer listing each file in the manifest's web.assets_frontend (order: variables →
utilities → layout → components → sections). Only rename them to .scss if the maintainer
explicitly requests SCSS compilation so Odoo inlines the imports at compile time.
7. Audit & verify
If /media/snt/data/addons-cm/website_sale_aplicoop does not exist, run the audit commands from the
repository root of the current workspace instead, and report which path was used.
cd /media/snt/data/addons-cm/website_sale_aplicoop
# Breakpoint sprawl — should only ever list 576/768/992/1200/1400, all min-width
grep -rho "@media[^{]*" static/src/css | sort | uniq -c | sort -rn
# Hardcoded colours outside the token file (expect: none)
grep -rn "#[0-9a-fA-F]\{3,8\}\b" static/src/css --include="*.css" | grep -v "base/variables.css"
# Specificity debt
grep -rc "!important" static/src/css/*/*.css | grep -v ':0'
# Focus handling: :focus-visible should dominate, outline:none must be paired with a replacement
grep -rn ":focus-visible\|outline: *none\|outline: *0" static/src/css
# Motion safety (expect at least one per animating component)
grep -rn "prefers-reduced-motion" static/src/css
# Inline styles in templates (expect: none)
grep -rn 'style="' views/*.xml
# Images without alt
grep -o "<img[^>]*" views/*.xml | grep -v "alt="
# Interactive divs/spans (expect: none)
grep -rn "<div[^>]*t-on-click\|<span[^>]*t-on-click\|<div[^>]*onclick" views/*.xml
Then, always:
pre-commit run --all-files # prettier (width 100, indent 4) over css/js/xml + eslint
docker-compose up -d # http://localhost:8070 — check at 320/768/1280 and 200% zoom
Manual passes that no grep replaces: Tab through the whole page (visible focus everywhere, order matches visual order, no trap), and check contrast on the actual rendered colours.
8. References
references/accessibility.md— WCAG AA patterns as QWeb snippets, per component.references/responsive.md— breakpoints, intrinsic layout, images, test matrix.references/css-architecture.md— file layout, naming, specificity, killing!important.references/finish-checklist.md— the polish pass: states, rhythm, empty/loading/error.
Repo docs: docs/QWEB_BEST_PRACTICES.md, docs/LAZY_LOADING.md.