@brunosps00/dev-workflow 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +72 -0
- package/README.md +7 -4
- package/lib/doctor.js +17 -2
- package/lib/init.js +16 -6
- package/lib/migrate-skills.js +189 -39
- package/lib/removed-bundled-skills.js +4 -2
- package/lib/skill-registry.js +12 -2
- package/lib/uninstall.js +61 -5
- package/package.json +1 -1
- package/scaffold/en/agent-instructions.md +1 -1
- package/scaffold/en/commands/dw-brainstorm.md +2 -2
- package/scaffold/en/commands/dw-bugfix.md +1 -1
- package/scaffold/en/commands/dw-commit.md +1 -1
- package/scaffold/en/commands/dw-dockerize.md +2 -2
- package/scaffold/en/commands/dw-functional-doc.md +1 -1
- package/scaffold/en/commands/dw-harness-audit.md +1 -1
- package/scaffold/en/commands/dw-intel.md +1 -1
- package/scaffold/en/commands/dw-learn.md +1 -1
- package/scaffold/en/commands/dw-new-project.md +3 -3
- package/scaffold/en/commands/dw-plan.md +2 -2
- package/scaffold/en/commands/dw-qa.md +6 -5
- package/scaffold/en/commands/dw-redesign-ui.md +3 -3
- package/scaffold/en/commands/dw-review.md +2 -2
- package/scaffold/en/commands/dw-skill-health.md +24 -1
- package/scaffold/en/commands/dw-update.md +28 -11
- package/scaffold/en/references/invariants.md +4 -0
- package/scaffold/en/templates/project-onepager.md +1 -1
- package/scaffold/pt-br/agent-instructions.md +1 -1
- package/scaffold/pt-br/commands/dw-brainstorm.md +2 -2
- package/scaffold/pt-br/commands/dw-bugfix.md +1 -1
- package/scaffold/pt-br/commands/dw-commit.md +1 -1
- package/scaffold/pt-br/commands/dw-dockerize.md +2 -2
- package/scaffold/pt-br/commands/dw-functional-doc.md +1 -1
- package/scaffold/pt-br/commands/dw-harness-audit.md +1 -1
- package/scaffold/pt-br/commands/dw-intel.md +1 -1
- package/scaffold/pt-br/commands/dw-learn.md +1 -1
- package/scaffold/pt-br/commands/dw-new-project.md +3 -3
- package/scaffold/pt-br/commands/dw-plan.md +2 -2
- package/scaffold/pt-br/commands/dw-qa.md +6 -5
- package/scaffold/pt-br/commands/dw-redesign-ui.md +3 -3
- package/scaffold/pt-br/commands/dw-review.md +2 -2
- package/scaffold/pt-br/commands/dw-skill-health.md +24 -1
- package/scaffold/pt-br/commands/dw-update.md +28 -11
- package/scaffold/pt-br/references/invariants.md +5 -0
- package/scaffold/pt-br/templates/project-onepager.md +1 -1
- package/scaffold/scripts/hooks/session-cost.mjs +62 -21
- package/scaffold/skills/docker-compose-recipes/references/compose-composition.md +1 -1
- package/scaffold/skills/dw-llm-eval/SKILL.md +3 -1
- package/scaffold/skills/dw-llm-eval/references/integration-with-dev-workflow-commands-detail.md +2 -2
- package/scaffold/skills/dw-llm-eval/references/reference-dataset.md +57 -3
- package/scaffold/skills/dw-ui-discipline/SKILL.md +8 -5
- package/scaffold/skills/dw-ui-discipline/references/accessibility-floor.md +20 -8
- package/scaffold/skills/dw-ui-discipline/references/content-stress.md +246 -0
- package/scaffold/skills/dw-ui-discipline/references/motion.md +205 -35
- package/scaffold/skills/dw-ui-discipline/references/responsive.md +44 -2
|
@@ -1,52 +1,222 @@
|
|
|
1
|
-
# Motion — easing, duration,
|
|
1
|
+
# Motion — whether to animate, easing, duration, physicality, performance
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
> Anthropic's frontend-design skill).
|
|
3
|
+
> First version adapted from [impeccable](https://github.com/pbakaus/impeccable) (Apache-2.0, derived from
|
|
4
|
+
> Anthropic's frontend-design skill). Rewritten 2026-11 from the `emil-design-eng`, `review-animations`,
|
|
5
|
+
> `animation-performance` and `remove-animations` skills in
|
|
6
|
+
> [`emilkowalski/skills`](https://github.com/emilkowalski/skills) (MIT © 2026 Emil Kowalski): the
|
|
7
|
+
> frequency gate, easing table, curves, physicality rules, escalation triggers and removal gate come from
|
|
8
|
+
> there. Read when adding, reviewing or removing animation or transitions.
|
|
5
9
|
|
|
6
|
-
|
|
10
|
+
Project motion tokens (`DESIGN.md`, CSS variables, Tailwind config) win over the values below. These are
|
|
11
|
+
the defaults when the project has none, and the bar when reviewing motion it already has.
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
doesn't answer one of those, cut it. Decorative motion is a visual-slop pattern.
|
|
13
|
+
## 1. Should it animate at all?
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
Decide this before choosing any curve. Frequency first:
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
| How often the user sees it | Decision |
|
|
18
|
+
|---|---|
|
|
19
|
+
| 100+ times a day (keyboard shortcuts, command palette, tab switches, in-app route changes, list rows) | No animation. Any duration reads as delay. |
|
|
20
|
+
| Tens of times a day (hover, frequent toggles, list expansion) | Remove, or reduce to near-imperceptible |
|
|
21
|
+
| Occasional (modals, drawers, toasts) | Standard motion |
|
|
22
|
+
| Rare or first-time (onboarding, success, empty state) | Room for delight |
|
|
15
23
|
|
|
16
|
-
-
|
|
17
|
-
(ease-out-expo) or `ease-out`.
|
|
18
|
-
- **Exit** (element leaves): ease-in — accelerate away. `cubic-bezier(0.4, 0, 1, 1)`.
|
|
19
|
-
- **Move/resize on screen:** ease-in-out (standard) `cubic-bezier(0.4, 0, 0.2, 1)`.
|
|
20
|
-
- Never linear for UI (only for continuous things like spinners/marquees).
|
|
24
|
+
**Keyboard-initiated actions never animate.** They repeat hundreds of times a day.
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
Then purpose. Valid purposes: spatial consistency (where did this come from / go), state indication,
|
|
27
|
+
explanation, feedback, preventing a jarring change. "It looks nice" on a frequently seen element is not a
|
|
28
|
+
purpose. Functional data (charts, tables, numbers) does not re-animate on every mount or refresh; it
|
|
29
|
+
animates only to show a real change in value.
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- Large (page/route, full-screen sheets): 300–500ms.
|
|
27
|
-
- Over ~500ms feels sluggish; under ~80ms is imperceptible (just snap with no transition).
|
|
31
|
+
Deleting everything is also wrong: a toast that would otherwise teleport in, or a panel that needs to show
|
|
32
|
+
where it came from, earns its motion.
|
|
28
33
|
|
|
29
|
-
##
|
|
34
|
+
## 2. Easing
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
| Motion | Easing |
|
|
37
|
+
|---|---|
|
|
38
|
+
| Entering or exiting | `ease-out` (starts fast, feels responsive) |
|
|
39
|
+
| Moving or morphing on screen | `ease-in-out` |
|
|
40
|
+
| Hover, color change | `ease` |
|
|
41
|
+
| Constant motion (marquee, progress, spinner) | `linear` |
|
|
42
|
+
| Default | `ease-out` |
|
|
33
43
|
|
|
34
|
-
|
|
44
|
+
**Never `ease-in` on UI**, exits included. It starts slow and delays the exact moment the user is watching;
|
|
45
|
+
`ease-out` at 200ms feels faster than `ease-in` at 200ms.
|
|
35
46
|
|
|
36
|
-
|
|
37
|
-
Animating `width`, `height`, `top`, `left`, `margin` janks. Use `transform: translate/scale` instead.
|
|
47
|
+
The built-in keywords are weak. Default curves when the project has none:
|
|
38
48
|
|
|
39
|
-
|
|
49
|
+
```css
|
|
50
|
+
--ease-out: cubic-bezier(0.23, 1, 0.32, 1); /* UI enter/exit */
|
|
51
|
+
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1); /* on-screen movement */
|
|
52
|
+
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); /* iOS-like drawer/sheet */
|
|
53
|
+
```
|
|
40
54
|
|
|
41
|
-
|
|
55
|
+
### Bounce and springs
|
|
42
56
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
No bounce, elastic or overshooting `cubic-bezier` (e.g. `cubic-bezier(0.68, -0.55, 0.27, 1.55)`) on UI
|
|
58
|
+
transitions — the `ui-slop-detect` gate flags these as `bounce-easing`.
|
|
59
|
+
|
|
60
|
+
**Springs are allowed only for gesture-driven motion:** drag with momentum, drag-to-dismiss sheets,
|
|
61
|
+
swipe, and anything the user may reverse mid-motion. Springs keep velocity when interrupted, which is what
|
|
62
|
+
a gesture needs. Keep bounce subtle:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
{ type: "spring", duration: 0.5, bounce: 0.2 } // bounce 0.1–0.3
|
|
50
66
|
```
|
|
51
67
|
|
|
52
|
-
|
|
68
|
+
A spring on a button, dropdown, modal or toast is out of scope for this exception.
|
|
69
|
+
|
|
70
|
+
## 3. Duration
|
|
71
|
+
|
|
72
|
+
| Element | Duration |
|
|
73
|
+
|---|---|
|
|
74
|
+
| Button press feedback | 100–160ms |
|
|
75
|
+
| Tooltips, small popovers | 125–200ms |
|
|
76
|
+
| Dropdowns, selects | 150–250ms |
|
|
77
|
+
| Modals, drawers | 200–500ms |
|
|
78
|
+
| Marketing / explanatory | Can be longer |
|
|
79
|
+
|
|
80
|
+
**UI animations stay under 300ms** unless a reason is written down. Under ~80ms is imperceptible: snap
|
|
81
|
+
instead. After the first tooltip in a group opens, the next ones open instantly (no delay, no animation).
|
|
82
|
+
|
|
83
|
+
## 4. Physicality
|
|
84
|
+
|
|
85
|
+
- **Never `scale(0)`.** Nothing appears from nothing. Start from `scale(0.95)` (0.9–0.97) with `opacity: 0`.
|
|
86
|
+
- **Popovers, dropdowns and tooltips scale from their trigger**, not the center:
|
|
87
|
+
`transform-origin: var(--transform-origin)` (Base UI / Radix expose it). **Modals are exempt** — they
|
|
88
|
+
appear centered and keep `transform-origin: center`.
|
|
89
|
+
- **Press feedback:** `transform: scale(0.97)` on `:active` (0.95–0.98), 100–160ms `ease-out`, on every
|
|
90
|
+
pressable element. It works on touch, unlike hover.
|
|
91
|
+
- **`translateY(100%)`** moves an element by its own height, so drawers and toasts of unknown size need no
|
|
92
|
+
hardcoded pixels.
|
|
93
|
+
|
|
94
|
+
## 5. Interruptibility and timing
|
|
95
|
+
|
|
96
|
+
- **Transitions over keyframes** for anything triggered rapidly (toasts being added, toggles). A transition
|
|
97
|
+
retargets from its current state; a keyframe animation restarts from zero.
|
|
98
|
+
- Entry without JS: `@starting-style` on the transitioned element.
|
|
99
|
+
- **Asymmetric timing:** slow where the user is deciding (a hold-to-confirm, a destructive press), fast
|
|
100
|
+
where the system responds (the release, the result).
|
|
101
|
+
- **Stagger** group entrances 30–80ms per item, capped so the whole group finishes within ~300ms.
|
|
102
|
+
Stagger entrances, not exits, and never block interaction while it plays.
|
|
103
|
+
- An imperfect crossfade can be bridged with `filter: blur(2px)` during the transition; keep any animated
|
|
104
|
+
blur under 20px.
|
|
105
|
+
|
|
106
|
+
## 6. Performance
|
|
107
|
+
|
|
108
|
+
Every frame has **16.7ms at 60fps and 8ms at 120Hz** (most phones). Design against 8ms. A fast laptop hides
|
|
109
|
+
almost every problem below; say which findings were verified from code and which need a recording on a
|
|
110
|
+
mid-range device.
|
|
111
|
+
|
|
112
|
+
| Tier | Properties | Cost |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| Composite only | `transform`, `opacity` (and in current browsers `filter`, `clip-path`) | Cheapest |
|
|
115
|
+
| Paint + composite | `box-shadow`, `border-radius`, `color` | Redraw every frame |
|
|
116
|
+
| Layout + paint + composite | `width`, `height`, `padding`, `margin`, `top`, `left`, `border-width` | Most expensive |
|
|
117
|
+
|
|
118
|
+
- Animate `transform` and `opacity` by default. Swaps: `width`/`height` → `scale()`; `top`/`left`/`margin` →
|
|
119
|
+
`translate()`; animated `box-shadow` → `filter: drop-shadow()`.
|
|
120
|
+
- **Never `transition: all`.** List the exact properties; `all` animates whatever changes next, including
|
|
121
|
+
layout properties nobody meant to animate. (Tailwind: `transition-transform`, `transition-opacity`, not
|
|
122
|
+
`transition-all`.)
|
|
123
|
+
- Do not drive child transforms through a CSS variable set on a parent; it recalculates style for every
|
|
124
|
+
child. Set `transform` on the element itself.
|
|
125
|
+
- In React, a hand-rolled animation writes to `ref.current.style`, never `setState` every frame.
|
|
126
|
+
- Predetermined motion runs in CSS or WAAPI (off the main thread); JS/rAF for dynamic, gesture-driven
|
|
127
|
+
motion. Motion's `x`/`y`/`scale` shorthands are hardware-accelerated from `motion` 14.2; on older versions
|
|
128
|
+
animate the full `transform` string. Layout animations (`layout`, `layoutId`) always run on the main
|
|
129
|
+
thread.
|
|
130
|
+
- `will-change` is the last step, added only after a measured problem (a 1px shift at start/end, dropped
|
|
131
|
+
frames on a recording). Never on `*` or large containers.
|
|
132
|
+
- Name the cause before changing code. "Added `will-change`, seems smoother" is not a diagnosis.
|
|
133
|
+
|
|
134
|
+
## 7. Accessibility
|
|
135
|
+
|
|
136
|
+
- **`prefers-reduced-motion` means fewer and gentler, not zero.** Drop movement (translate, scale,
|
|
137
|
+
parallax, slide); keep the opacity and color changes that explain a state change.
|
|
138
|
+
|
|
139
|
+
```css
|
|
140
|
+
@media (prefers-reduced-motion: reduce) {
|
|
141
|
+
.drawer { transition: opacity 200ms ease; transform: none; }
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```jsx
|
|
146
|
+
const reduce = useReducedMotion();
|
|
147
|
+
const closedX = reduce ? 0 : "-100%";
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
A global `animation-duration: 0.01ms !important` reset is an acceptable project-wide fallback, not the
|
|
151
|
+
design: state changes still have to be visible.
|
|
152
|
+
- **Gate hover motion** behind `@media (hover: hover) and (pointer: fine)`. Touch fires a fake hover on tap
|
|
153
|
+
and leaves it stuck (see `responsive.md`).
|
|
154
|
+
|
|
155
|
+
## 8. Removing motion
|
|
156
|
+
|
|
157
|
+
When the job is to cut motion (an over-animated product, a performance pass), walk each animation through
|
|
158
|
+
this gate and record the deciding branch:
|
|
159
|
+
|
|
160
|
+
1. Keyboard-triggered? → **delete**.
|
|
161
|
+
2. Seen 100+ times a day? → **delete**.
|
|
162
|
+
3. On functional data? → **delete**, unless it shows a real change in value.
|
|
163
|
+
4. No nameable purpose? → **keep** if it is a rare first-time moment, otherwise **delete**.
|
|
164
|
+
5. Purpose, seen tens of times a day? → **reduce**.
|
|
165
|
+
6. Purpose, rarer: over 300ms, or a slide/scale where a fade carries the same meaning? → **reduce**,
|
|
166
|
+
otherwise **keep**.
|
|
167
|
+
|
|
168
|
+
Marketing pages get more latitude (one hero entrance, explanation), but scroll-reveal on every section
|
|
169
|
+
still fails.
|
|
170
|
+
|
|
171
|
+
| Today | Reduced to |
|
|
172
|
+
|---|---|
|
|
173
|
+
| Hover with scale, lift or shadow growth | Color/opacity only, ≤150ms, `ease` |
|
|
174
|
+
| Slide or scale entrance on a frequent surface | Opacity only, 150ms, `ease-out` |
|
|
175
|
+
| UI animation over 300ms | 200–250ms, same curve |
|
|
176
|
+
| Stagger over 80ms per item or 300ms total | 30ms per item, or fade the group as one |
|
|
177
|
+
| Infinite loop (pulse, shimmer, glow) on a persistent element | Run once, or keep the static end state |
|
|
178
|
+
|
|
179
|
+
Spinners and skeletons on work that resolves under 300ms are deleted too (see `state-matrix.md`).
|
|
180
|
+
|
|
181
|
+
**Removal hazards** — check before deleting, or the removal becomes a regression:
|
|
182
|
+
|
|
183
|
+
- **Hidden start states.** `initial={{ opacity: 0 }}`, a keyframe from `opacity: 0` with
|
|
184
|
+
`fill-mode: forwards`, a reveal class waiting for an observer. Remove the start state with the animation
|
|
185
|
+
or the element stays invisible.
|
|
186
|
+
- **Code waiting on the animation:** `onAnimationEnd`, `transitionend`, `onExitComplete`, `setTimeout`
|
|
187
|
+
sized to a duration.
|
|
188
|
+
- **Exit animations delaying unmount** (`AnimatePresence`): move what ran on exit-complete to the close.
|
|
189
|
+
- **Layout animations masking a jump** (`layout`, FLIP): if the jump appears, keep the animation and report
|
|
190
|
+
the layout shift.
|
|
191
|
+
- **Focus moved in an animation callback** must move immediately.
|
|
192
|
+
- **Tests and selectors** that wait for animation classes or durations.
|
|
193
|
+
|
|
194
|
+
Then delete what the removal orphaned (unused `@keyframes`, `motion.div` with no motion props left, empty
|
|
195
|
+
`AnimatePresence`, unused variants and imports). Propose — never perform — removing a motion library
|
|
196
|
+
dependency.
|
|
197
|
+
|
|
198
|
+
## 9. Review escalation triggers
|
|
199
|
+
|
|
200
|
+
`/dw-review` treats each of these in a UI diff as a candidate finding (still subject to `dw-review-rigor`
|
|
201
|
+
refutation). Default severity **medium**; **high** when the motion runs on a keyboard-triggered or
|
|
202
|
+
100+/day action, or when it blocks interaction.
|
|
203
|
+
|
|
204
|
+
- `transition: all` / Tailwind `transition-all`.
|
|
205
|
+
- `scale(0)` as a start state.
|
|
206
|
+
- `ease-in` on any UI enter, exit or interaction.
|
|
207
|
+
- Animation on a keyboard shortcut, command-palette toggle or other 100+/day action.
|
|
208
|
+
- UI duration over 300ms with no stated reason.
|
|
209
|
+
- `transform-origin: center` on a trigger-anchored popover, dropdown or tooltip.
|
|
210
|
+
- Keyframes on toasts, toggles or anything triggered rapidly.
|
|
211
|
+
- Animating `width`, `height`, `margin`, `padding`, `top` or `left` when a transform does the same.
|
|
212
|
+
- A CSS variable on a parent driving child transforms.
|
|
213
|
+
- Movement with no `prefers-reduced-motion` handling.
|
|
214
|
+
- `:hover` motion not gated by `(hover: hover) and (pointer: fine)`.
|
|
215
|
+
- Bounce/overshoot easing, or a spring outside gesture-driven motion.
|
|
216
|
+
- Symmetric timing on a press-and-hold or destructive-confirm interaction.
|
|
217
|
+
|
|
218
|
+
Fix preference, earliest first: delete → reduce → fix easing → fix origin → make interruptible → move to
|
|
219
|
+
composite-only properties → asymmetric timing → polish → accessibility and cohesion.
|
|
220
|
+
|
|
221
|
+
When feel cannot be judged from code (hover feel, perceived speed, gesture physics), say so and ask for a
|
|
222
|
+
slow-motion pass (DevTools animation panel at 2–5× duration) or a real device, instead of guessing.
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# Responsive — mobile-first, container queries, fluid space
|
|
2
2
|
|
|
3
3
|
> Patterns adapted from [impeccable](https://github.com/pbakaus/impeccable) (Apache-2.0, derived from
|
|
4
|
-
> Anthropic's frontend-design skill).
|
|
4
|
+
> Anthropic's frontend-design skill). The "Mobile platform fixes" section is adapted from the
|
|
5
|
+
> `mobile-native` skill in [`emilkowalski/skills`](https://github.com/emilkowalski/skills)
|
|
6
|
+
> (MIT © 2026 Emil Kowalski). Read when a surface must work across devices.
|
|
5
7
|
|
|
6
8
|
## Mobile-first
|
|
7
9
|
|
|
@@ -43,10 +45,50 @@ Keep body text fixed (≥16px); fluid body harms readability.
|
|
|
43
45
|
- Touch targets ≥ 24×24 CSS px (≥ 44×44 recommended on mobile). Spacing between targets matters as much
|
|
44
46
|
as size.
|
|
45
47
|
- Don't hide primary actions behind hover — hover doesn't exist on touch. Provide a tap-visible path.
|
|
46
|
-
- Respect safe-area insets on mobile (`env(safe-area-inset-*)`) for notches/home indicators
|
|
48
|
+
- Respect safe-area insets on mobile (`env(safe-area-inset-*)`) for notches/home indicators — this needs
|
|
49
|
+
`viewport-fit=cover` in the viewport meta, or every inset is `0px` (see below).
|
|
47
50
|
|
|
48
51
|
## Layout reflow
|
|
49
52
|
|
|
50
53
|
State explicitly how elements reorganize between breakpoints: stack vs. side-by-side, what collapses into
|
|
51
54
|
a menu, what hides (and whether hidden content is still reachable). "Hidden on mobile with no alternative"
|
|
52
55
|
is a bug, not a layout decision.
|
|
56
|
+
|
|
57
|
+
## Mobile platform fixes
|
|
58
|
+
|
|
59
|
+
Most "the app feels janky on mobile" reports are not animation problems: they are a tap delay, a gray
|
|
60
|
+
flash on tap, or a hover state that will not let go. **None of these reproduce in desktop device
|
|
61
|
+
emulation.** Gate by capability with media queries, never by user-agent sniffing or screen width — touch
|
|
62
|
+
and mouse coexist (iPads with trackpads, touchscreen laptops). Apply each fix where its reason applies, not
|
|
63
|
+
globally out of habit.
|
|
64
|
+
|
|
65
|
+
| Symptom | Cause | Fix |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| Hover state stuck after tap | Touch fakes `:hover` on the first tap and keeps it | Put `:hover` rules inside `@media (hover: hover) and (pointer: fine)` (Tailwind v4 `hover:` already does; v3 needs `future.hoverOnlyWhenSupported`) and give touch an `:active` state |
|
|
68
|
+
| Gray/blue flash on tap | Browser tap highlight | `html { -webkit-tap-highlight-color: transparent; }`, then make sure every tappable element has its own `:active` feedback |
|
|
69
|
+
| Bottom-pinned UI hidden under the URL bar | `100vh` is the largest viewport | `100dvh` for app shells, drawers and bottom-pinned UI; `100svh` for heroes (stable, no shift on scroll) |
|
|
70
|
+
| Page zooms into an input and stays zoomed | iOS zooms on inputs under 16px | `font-size: 16px` on `input, textarea, select` (scope it with `@media (pointer: coarse)` if desktop needs smaller). **Never** `user-scalable=no` or `maximum-scale=1` — that is an accessibility failure (`accessibility-floor.md` §11) |
|
|
71
|
+
| Tap feels laggy | Double-tap-zoom wait; feedback on release | `touch-action: manipulation` on buttons, links, `[role="button"]`; press feedback on `:active` (100–160ms `ease-out`, see `motion.md`), `pointerdown` not `click` when JS must react |
|
|
72
|
+
| Pull-to-refresh or rubber band hijacks an app | Scroll chaining to the page | `overscroll-behavior: none` on `html, body` in apps (not documents); `contain` on inner scrollers. Never a `touchmove` + `preventDefault()` listener |
|
|
73
|
+
| Content cut by the notch / home indicator | Safe area | `viewport-fit=cover` in the viewport meta + `padding-top: env(safe-area-inset-top)` / `padding-bottom: env(safe-area-inset-bottom, 0px)` on fixed headers, tab bars, toasts and sheets |
|
|
74
|
+
| Long-press selects a button label | Text selection on controls | `user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;` on controls only — never on `body`; users copy addresses and order numbers |
|
|
75
|
+
| Carousel drags the page vertically | Ambiguous gesture axis | `touch-action: pan-y` on a horizontal carousel, `none` only on surfaces that own every axis; prefer native `scroll-snap-type: x mandatory` |
|
|
76
|
+
| Status bar color wrong in one scheme | Single `theme-color` | One `<meta name="theme-color" media="(prefers-color-scheme: …)">` per scheme, matching the top of the page, plus `color-scheme: light dark` |
|
|
77
|
+
|
|
78
|
+
Inputs also set the right keyboard: `type="email"` / `type="tel"`, `inputmode="numeric"` for codes and
|
|
79
|
+
`"decimal"` for amounts, `autocapitalize="none"` on usernames and codes, `enterkeyhint` naming what Return
|
|
80
|
+
does.
|
|
81
|
+
|
|
82
|
+
Baseline for a mobile-facing app:
|
|
83
|
+
|
|
84
|
+
```html
|
|
85
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`interactive-widget=resizes-content` makes the Android software keyboard shrink the layout viewport, so
|
|
89
|
+
`100dvh` and bottom-pinned inputs react to it the way they do on iOS.
|
|
90
|
+
|
|
91
|
+
**Real hardware is the bar.** Sticky hover, tap delay, URL-bar height, input zoom, overscroll, safe areas
|
|
92
|
+
and the software keyboard only show on a device: dev server on `0.0.0.0`, open it by LAN IP, inspect with
|
|
93
|
+
Safari Web Inspector or `chrome://inspect`. When no device is available, report which fixes were verified
|
|
94
|
+
from code and which still need a phone — never declare them fixed from emulation.
|