Web App Responsiveness
web-apps specs/web-apps/responsiveness.kmd
Responsividade e conformidade mobile para web apps Koder (admin, dashboards, consoles SaaS): breakpoints, touch targets, iOS Safari, hover, tabelas, formulários. Distinto de landing pages (specs/ landing-pages/), que tem regras próprias.
When this spec applies
All triggers
- Implementar ou ajustar responsividade em web app Koder (admin/dashboard/console)
- Validar conformidade mobile em iOS Safari ou Android Chrome
- Resolver tabela / formulário que quebra em viewport pequeno
Specification body
Spec — Web App Responsiveness
Applicability
All Koder web applications with user-facing UI pages — admin panels, dashboards, SaaS consoles, and any route served beyond a static landing page. Examples:
id.koder.dev/admin/*,id.koder.dev/ui/*flow.koder.dev/*edictus.koder.dev/*,kompass.koder.dev/*- Any route under a product domain that is not
site/index.html
Out of scope: static landing pages (site/index.html) — those are covered by specs/landing-pages/products.kmd.
1. Breakpoints
Three breakpoints, mobile-first:
| Name | Range | CSS custom property |
|---|---|---|
| mobile | 0 – 767 px | (base, no min-width query) |
| tablet | 768 – 1199 px | @media (min-width: 768px) |
| desktop | ≥ 1200 px | @media (min-width: 1200px) |
All layout styles must be written mobile-first: base styles target the narrowest viewport; add overrides at 768px and 1200px. Do not use max-width queries for primary layout breakpoints — they produce desktop-first CSS and break future additions. max-width is only acceptable to undo a desktop rule that should not apply on mobile.
Sidebar reference implementation (mobile-first):
/* Base (mobile): sidebar is a hidden drawer */
.app-shell {
display: grid;
grid-template-columns: 1fr; /* single column — no sidebar column */
}
.sidebar {
position: fixed;
inset: 0;
transform: translateX(-100%); /* off-screen */
z-index: 200;
transition: transform 0.25s ease;
}
.sidebar.is-open {
transform: translateX(0);
}
.mobile-topbar { display: flex; } /* hamburger bar visible on mobile */
/* Desktop (≥ 1200 px): sidebar becomes a persistent column */
@media (min-width: 1200px) {
.app-shell {
grid-template-columns: 240px 1fr; /* sidebar + content */
}
.sidebar {
position: sticky;
top: 0;
height: 100svh;
transform: none; /* always visible */
}
.mobile-topbar { display: none; } /* no hamburger needed */
}
Common wrong pattern (avoid):
/* ❌ Wrong — desktop-first, sidebar hidden only below 900 px */
.app-shell { grid-template-columns: 240px 1fr; }
@media (max-width: 900px) { .sidebar { display: none; } }
/* ❌ Wrong — arbitrary breakpoint not aligned to spec */
@media (max-width: 900px) { ... }
2. Touch Targets
- All interactive elements (buttons, links, checkboxes, form controls, icon buttons) must have a minimum clickable area of 44 × 44 px (Apple HIG minimum).
- If the visual element is smaller, add
paddingor use::before/::afterto extend the hit area. - Interactive elements must never overlap or be spaced less than 8 px apart on mobile.
3. Navigation
3.1 Sidebar / Nav rail
- Desktop (≥ 1200 px): persistent sidebar visible (sticky column,
240pxwide by default). - Tablet + Mobile (< 1200 px): sidebar collapses to a full-height slide-over drawer (preferred) or an icon-only rail (acceptable). Default: drawer triggered by a top-bar hamburger button. Use the drawer for new implementations — icon-only rail only if there is a strong product reason.
- The sidebar drawer must be
position: fixed; inset: 0; z-index: 200and slide in from the left withtransform: translateX(-100%) → translateX(0). - A semi-transparent backdrop (
position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 190) must appear behind the open drawer and close it on tap.
3.2 Top bar
- Must be present on all three breakpoints.
- On mobile: product logo + menu button. No horizontal nav items visible; they move into the drawer.
- Minimum height:
52px.
3.3 Menu button (hamburger)
- Required in the top bar when sidebar is not visible.
- Must be at least 44 × 44 px.
- Must toggle a drawer with
aria-expandedon the triggering button.
4. Typography
| Context | Minimum font-size |
|---|---|
| Body / paragraphs | 14px |
| Form inputs | 16px (prevents iOS auto-zoom on focus) |
| Labels / captions | 12px |
| Headings (h1–h3) | Scale relative; h1 may shrink on mobile but never below 20px |
- Line height: ≥ 1.4 for body copy.
- Never use
font-size < 12px.
5. Forms
- Inputs, selects, and textareas must be
width: 100%on mobile. - Labels must stack above their inputs on mobile (not inline-left).
font-size: 16pxon all<input>,<select>,<textarea>— prevents iOS Safari from zooming in on focus.- Submit buttons must span full width on mobile.
6. Tables
Data tables must not break the layout. On mobile (< 768 px):
- Option A (preferred): Wrap the table in
overflow-x: auto— table scrolls horizontally inside the wrapper. - Option B: Reflow to card/list layout via CSS.
- Never allow
table-layout: fixedwith large fixed widths that causeoverflow: hiddento clip data silently.
/* Option A */
.table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
7. Modals and Dialogs
On mobile:
- Full-width (
width: 100%; max-width: 100%) or bottom-sheet pattern. max-height: 90svhwithoverflow-y: autoon the scrollable content area.- Never extend below the fold without a scroll container.
position: fixedworks on iOS for modals — no workaround needed for modals (only for sticky bars — see §8).
8. iOS Safari — Required Rules
8.1 Viewport height
Never use height: 100vh or min-height: 100vh on full-screen containers. iOS Safari includes the browser chrome in 100vh, causing content to be partially hidden.
Use 100svh (small viewport height — excludes browser chrome):
/* Wrong */
.app-shell { min-height: 100vh; }
/* Correct */
.app-shell {
min-height: 100svh;
/* Fallback for browsers without svh support */
min-height: -webkit-fill-available;
}
8.2 Safe area insets (notch / home indicator)
Containers that extend to screen edges must account for the notch and home indicator:
body {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
.top-bar {
padding-top: calc(12px + env(safe-area-inset-top));
}
.bottom-bar, .tab-bar {
padding-bottom: calc(8px + env(safe-area-inset-bottom));
}
Required in the <head>: <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">.
viewport-fit=cover is what enables env(safe-area-inset-*) to work.
8.3 position: fixed caveats
position: fixed elements (top bars, sidebars, bottom nav) may not behave correctly on iOS when the virtual keyboard is open. Do not rely on position: fixed for elements that must stay visible while an input is focused. Consider repositioning to position: sticky within a scrollable parent for such cases.
8.4 Meta viewport — forbidden values
Never add user-scalable=no or maximum-scale=1 to the meta viewport tag. These break accessibility (pinch-to-zoom) and violate WCAG 1.4.4.
<!-- Forbidden -->
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<!-- Correct -->
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
9. Hover — Touchscreen Safety
All :hover rules applied to cards, rows, buttons, and interactive containers must be wrapped in @media (hover: hover). This prevents "stuck hover" on touchscreens (the hover state activates on tap and never clears).
/* Wrong */
.card:hover {
background: var(--surface-raised);
transform: translateY(-2px);
}
/* Correct */
@media (hover: hover) {
.card:hover {
background: var(--surface-raised);
transform: translateY(-2px);
}
}
Exception: color-only changes on text links (underline, color) do not require the wrapper.
10. Images and Media
max-width: 100%on all<img>,<video>,<iframe>.- Never set
widthorheightas inline style with a fixed pixel value that exceeds the viewport. - Use
object-fit: coverorobject-fit: containfor images in fixed-size containers. - Decorative images:
alt="".
11. Scrolling
- Set
overflow-x: hiddenonly on<body>or the root app shell — never on content containers where it would clip visible content. - Scrollable lists and panes must use
-webkit-overflow-scrolling: touch(momentum scrolling on iOS). - Avoid nested scroll containers where possible — they produce poor UX on mobile.
12. Build & Deploy
12.1 Asset cleanup
Build tools (Vite, Webpack, etc.) append a content hash to output filenames (index-CBGY9LJz.css). When a rebuild produces a new hash, the old files stay on disk unless explicitly removed. Browsers that cached the old index.html (which references old hashes) will continue loading old CSS/JS even after a new deploy.
Rules:
- Always clean the build output directory before or after each build:
rm -rf dist/*or setbuild.emptyOutDir: trueinvite.config.js. - If the server's static directory is updated by
cp, clear it first:rm -rf /dest/* && cp -r dist/. /dest/. - Never accumulate multiple generations of hashed files in the same directory on the server.
12.2 Cache-Control headers
| Resource | Required header |
|---|---|
index.html (entry point) |
Cache-Control: no-cache |
Hashed assets (*.css, *.js with hash in filename) |
Cache-Control: max-age=31536000, immutable |
Non-hashed static files (icon.svg, favicon.ico) |
Cache-Control: max-age=86400 |
index.html must never be cached long-term. It is the only resource that tells browsers which hashed assets to load. If it is stale, browsers serve old CSS/JS even after a redeploy.
If the web server cannot be configured per-file (e.g., koder-id serving static files directly), ensure the application sets the appropriate headers when serving index.html vs. assets.
13. Pre-deploy Checklist
Before publishing a new page or route:
- Headless breakpoint test — run a Playwright/Puppeteer script that opens the page at 390 px, 768 px, 1024 px, and 1440 px. At < 1200 px: sidebar must be
position: fixed; transform: translateX(-100%)and.mobile-topbarmust havedisplay: flex. At ≥ 1200 px: sidebar must beposition: stickyand topbar must bedisplay: none. - No
max-widthas primary breakpoint — grep stylesheet formax-widthqueries. Any@media (max-width: NNNpx)block that controls the sidebar, grid layout, or main navigation is a bug. The only accepted primary breakpoints aremin-width: 768pxandmin-width: 1200px. - Input font-size ≥ 16px — grep
<input,<select,<textarea; verify CSS sets font-size to 16px on all three. - No bare
100vhon full-screen containers — grep forheight: 100vhandmin-height: 100vh; each occurrence must be accompanied by a100svhoverride on the next line. viewport-fit=coverin meta viewport tag — required if anyenv(safe-area-inset-*)is used.- No
user-scalable=no— grep the meta viewport tag; must not be present. - Hover rules wrapped — grep the stylesheet for
:hoveroutside of@media (hover: hover) {; flag any hit on cards, rows, table rows, or non-link interactive elements. - Touch targets ≥ 44px — verify interactive elements (icon buttons in particular) have
min-width: 44px; min-height: 44pxor equivalent padding. - Tables scroll horizontally — verify all
<table>elements are wrapped in adivwithoverflow-x: auto. - Modals scroll internally — check modals have
max-height: 90svh; overflow-y: autoon the content area. - Build artifacts clean — verify the deploy directory contains only files from the current build (no old hashed files from previous builds).
- Cache headers — verify
index.htmlis served withCache-Control: no-cacheand hashed assets withmax-age=31536000, immutable.
14. Audit Integration
/k-housekeep does not audit web-app routes by default (too dynamic for static grep). To trigger an audit of a specific app:
/k-housekeep platform/id
The audit will:
- Grep all
*.cssand*.html/*.svelte/*.vue/*.tsxfiles underplatform/id/for the checks in §13. - Report violations. Does not auto-fix web app UI — changes are application code, not markup templates.
- List violations in the housekeep report under a new section "Web app audit —
platform/id".
Auto-fix is limited to:
- Replacing bare
100vh→100svhin CSS - Adding
viewport-fit=coverto the meta viewport tag if missing
Everything else is reported for manual fix.
Headless breakpoint test (auto-runnable when Playwright is installed):
# Run from the module root — replace id.koder.dev/admin with the actual URL
npx playwright test --browser=chromium meta/docs/stack/specs/web-apps/responsive-smoke.test.js
A reusable smoke test template lives at meta/docs/stack/specs/web-apps/responsive-smoke.test.js.