addons-cm/.claude/skills/odoo-web-design/references/responsive.md

5.6 KiB
Raw Blame History

Responsive layout

Breakpoints — Bootstrap 5, and only these

Name min-width Typical device
(base) phones, 320575 px
sm 576px large phones
md 768px tablets
lg 992px small laptops
xl 1200px desktops
xxl 1400px large desktops

Mobile-first, min-width only:

/* ✅ base = mobile, enhance upwards */
.products-grid {
    display: grid;
    gap: var(--ac-space-md);
    grid-template-columns: 1fr;
}

@media (min-width: 768px) {
    .products-grid {
        grid-template-columns: repeat(2, 1fr);
    }
}

@media (min-width: 1200px) {
    .products-grid {
        grid-template-columns: repeat(4, 1fr);
    }
}
/* ❌ desktop-first bands: overlapping ranges, unclear cascade, private breakpoints */
@media (max-width: 1599px) and (min-width: 1400px) {  }
@media (max-width: 720px) {  }

Prefer intrinsic layout — most grids need no media query at all

.products-grid {
    display: grid;
    gap: var(--ac-space-md);
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
}

min(100%, 16rem) is what keeps it from overflowing at 320 px. Reach for a media query only when the layout must genuinely change (sidebar moves, table becomes cards), not merely resize.

Fluid type and space

:root {
    --ac-text-sm: clamp(0.875rem, 0.85rem + 0.1vw, 0.9375rem);
    --ac-text-base: clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
    --ac-text-lg: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem);
    --ac-text-xl: clamp(1.5rem, 1.3rem + 1vw, 2.25rem);
    --ac-space-md: clamp(1rem, 0.9rem + 0.5vw, 1.5rem);
}

One continuous scale beats three step-changes at breakpoints. Never redefine a token inside a media query to make it smaller.

Body copy: 1rem minimum, line-height ≥ 1.5, measure 4575 characters (max-width: 65ch).

Touch targets and pointer

WCAG 2.2 AA (§2.5.8 Target Size, Minimum) asks for 24×24 px. The familiar 44×44 px is §2.5.5, level AAA — treat it as the house rule for anything a thumb hits, applied by pointer type rather than by viewport width: a 1024 px tablet needs the big target, a 1024 px laptop doesn't.

.qty-control .btn,
.tag-filter-badge {
    min-width: 2.25rem; /* 36px — comfortable with a mouse */
    min-height: 2.25rem;
}

@media (pointer: coarse) {
    .qty-control .btn,
    .tag-filter-badge {
        min-width: 2.75rem; /* 44px */
        min-height: 2.75rem;
    }
}

@media (hover: hover) and (pointer: fine) {
    .product-card:hover {
        transform: translateY(-2px);
        box-shadow: var(--ac-shadow-md);
    }
}

Hover effects belong inside @media (hover: hover) — on touch they stick after a tap and look broken.

Tables → cards, without restructuring

@media (max-width: 767.98px) {
    .checkout-summary-table thead {
        /* keep it in the a11y tree; hide visually only */
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
    }
    .checkout-summary-table tr {
        display: grid;
        grid-template-columns: 1fr auto;
        gap: var(--ac-space-xs);
        padding: var(--ac-space-sm) 0;
        border-bottom: 1px solid var(--ac-border-light);
    }
}

This is the one place a max-width query is honest: it's an override of the desktop table. Keep the markup semantic — never emit different HTML per viewport.

Sticky bars (cart / checkout actions on mobile)

.checkout-actions {
    position: sticky;
    bottom: 0;
    z-index: var(--ac-z-sticky);
    padding: var(--ac-space-sm);
    padding-bottom: calc(var(--ac-space-sm) + env(safe-area-inset-bottom, 0px));
    background: var(--ac-surface, #fff);
    box-shadow: 0 -2px 8px rgb(0 0 0 / 8%);
}

env(safe-area-inset-bottom) keeps the button above the iOS home indicator. Sticky elements must not eat more than ~20 % of a 568 px-tall viewport.

Images

<img class="product-card-img" t-att-src="product['image_url']" t-att-alt="product['name']"
     loading="lazy" decoding="async" width="400" height="400"/>
  • width/height always — they reserve the box and prevent layout shift (CLS).
  • loading="lazy" for everything below the fold; never on the LCP image (first product row, hero) — that delays the largest paint.
  • CSS: max-width: 100%; height: auto; plus object-fit: cover with a fixed aspect-ratio when cards must align.
  • Odoo serves resized variants: /web/image/product.product/<id>/image_256 etc. Ask for the size you render, not image_1920 scaled down in CSS.

Container and page rhythm

.eskaera-page {
    width: 100%;
    max-width: 75rem;
    margin-inline: auto;
    padding-inline: clamp(1rem, 4vw, 2rem);
}

Use logical properties (margin-inline, padding-block, inset-inline-start) — Odoo auto-generates an RTL stylesheet, and logical properties survive the flip without a second rule.

Test matrix

Width Why
320 px smallest supported; the one that breaks first
360 / 390 px real Android / iPhone
768 px tablet portrait, first breakpoint
1024 px tablet landscape / small laptop
1280 / 1440 px desktop
1280 px @ 200 % zoom WCAG 1.4.4 reflow — equivalent to 640 px

Run the stack (docker-compose up -d, http://localhost:8070) and check, per viewport: no horizontal scroll, no clipped text, touch targets still ≥ 44 px on coarse pointers, sticky bars not covering content, images not stretched.

DevTools device toolbar is enough for layout; check touch targets by actually tapping on a phone once before calling it done.