@hegemonart/get-design-done 1.14.8 → 1.15.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.
@@ -0,0 +1,411 @@
1
+ <!-- Source: nextlevelbuilder/ui-ux-pro-max-skill (MIT) — data/stacks/react.csv (framer-motion rows) -->
2
+
3
+ # Framer Motion Patterns
4
+
5
+ Framer Motion is the standard animation library for React. It abstracts the browser's animation primitives into a declarative API that stays out of your way for common cases while exposing full physics-based control when you need it. This reference covers implementation patterns — not just the API, but _why_ each pattern exists and when to apply it.
6
+
7
+ ---
8
+
9
+ ## 1. Basics — motion components
10
+
11
+ Any HTML (or SVG) element can become a motion component by prefixing it with `motion.`. The most common variants are `motion.div`, `motion.span`, `motion.button`, and `motion.li`, but `motion.section`, `motion.a`, `motion.img`, and any other HTML element follow the same pattern.
12
+
13
+ The four primary animation props are:
14
+
15
+ - **`initial`** — the state the element starts in before it mounts (or before a transition begins). Without `initial`, the element won't animate _from_ anything — it'll just be in the `animate` state on mount. Always provide `initial` when you want an entrance animation.
16
+ - **`animate`** — the state the element should animate _to_. Framer drives the element toward this state whenever it changes.
17
+ - **`exit`** — the state the element animates _to_ when it unmounts. Requires `<AnimatePresence>` as a parent — see Section 3.
18
+ - **`transition`** — controls how the animation happens (spring, tween, duration, ease). If omitted, Framer applies a spring by default for layout/positional changes and a tween for opacity.
19
+
20
+ A basic fade-in example:
21
+
22
+ ```tsx
23
+ import { motion } from 'framer-motion'
24
+
25
+ function FadeIn({ children }: { children: React.ReactNode }) {
26
+ return (
27
+ <motion.div
28
+ initial={{ opacity: 0, y: 8 }}
29
+ animate={{ opacity: 1, y: 0 }}
30
+ transition={{ type: 'tween', duration: 0.2, ease: 'easeOut' }}
31
+ >
32
+ {children}
33
+ </motion.div>
34
+ )
35
+ }
36
+ ```
37
+
38
+ The slight `y: 8` offset on entry gives the element a sense of emerging from below — a common, subtle entrance pattern. The `easeOut` ease starts fast and slows at the end, which feels responsive.
39
+
40
+ ---
41
+
42
+ ## 2. Spring vs. Tween Configuration
43
+
44
+ Framer Motion supports two fundamentally different animation models. Choosing between them is not arbitrary — each has a domain where it clearly wins.
45
+
46
+ ### Spring physics (preferred for UI motion)
47
+
48
+ Springs model the physics of a real spring: the element overshoots slightly and settles. This is why spring motion _feels natural_ — real objects in the world have inertia and settle under physical forces. For UI, spring motion communicates responsiveness and quality.
49
+
50
+ `type: "spring"` is Framer's default for layout animations and positional changes. Key parameters:
51
+
52
+ - **`stiffness`** (100–800): controls how forcefully the spring pulls toward the target. High stiffness = arrives fast and decisively. Low stiffness = slow, lazy arrival.
53
+ - **`damping`** (10–30): controls oscillation resistance. High damping = settles without overshoot. Low damping = bouncy.
54
+ - **`mass`** (0.5–2): adds inertia. Higher mass makes the element feel heavier and slower to respond.
55
+
56
+ The relationship that matters in practice:
57
+ - **High stiffness + high damping = snappy** — fast arrival, no bounce. This is the production UI default.
58
+ - **Low stiffness + low damping = bouncy** — never use in production UI. Bounce feels playful and toy-like, which is wrong for most product contexts.
59
+
60
+ **Hard constraint: `bounce: 0` always for icon cross-fades and micro-interactions.** The `bounce` shorthand parameter is a convenience alias — setting it to 0 ensures no oscillation. "Bounce must be zero" is a non-negotiable rule for any interaction that fires frequently or in information-dense UI.
61
+
62
+ Recommended production preset:
63
+
64
+ ```tsx
65
+ transition={{ type: 'spring', stiffness: 400, damping: 30 }}
66
+ ```
67
+
68
+ This is snappy and clean. It arrives fast and settles without any visible oscillation. Use it as the default for hover lifts, modal entries, and element transitions.
69
+
70
+ ### Tween (for duration-controlled, eased animations)
71
+
72
+ Tween animations run over a fixed duration with a specified easing curve. Use them when exact timing matters — opacity fades, color transitions, anything where you need predictable, duration-controlled behavior rather than physics.
73
+
74
+ ```tsx
75
+ transition={{ type: 'tween', duration: 0.2, ease: 'easeOut' }}
76
+ ```
77
+
78
+ Ease guidance:
79
+ - **`"easeOut"`** for entrances — starts fast (feels responsive), decelerates to rest.
80
+ - **`"easeIn"`** for exits — accelerates away, gets out of the way quickly.
81
+ - **`"easeInOut"`** for emphasis transitions — smooth acceleration and deceleration, used when the same element transitions between states (not entering/exiting).
82
+
83
+ The rule of thumb: use springs for movement and scale; use tweens for opacity, color, and blur.
84
+
85
+ ---
86
+
87
+ ## 3. AnimatePresence
88
+
89
+ `AnimatePresence` is the component that enables exit animations. Without it, React unmounts components immediately, and `exit` props are never executed — the element simply vanishes.
90
+
91
+ ```tsx
92
+ import { AnimatePresence, motion } from 'framer-motion'
93
+
94
+ function ToastContainer({ toasts }) {
95
+ return (
96
+ <AnimatePresence>
97
+ {toasts.map(toast => (
98
+ <motion.div
99
+ key={toast.id}
100
+ initial={{ opacity: 0, y: -10 }}
101
+ animate={{ opacity: 1, y: 0 }}
102
+ exit={{ opacity: 0, y: -10 }}
103
+ transition={{ type: 'spring', stiffness: 400, damping: 30 }}
104
+ >
105
+ {toast.message}
106
+ </motion.div>
107
+ ))}
108
+ </AnimatePresence>
109
+ )
110
+ }
111
+ ```
112
+
113
+ Every child of `AnimatePresence` that will be conditionally rendered **must have a `key` prop**. Framer uses the key to track which element is entering and which is exiting. Without a key, exit animations will not fire.
114
+
115
+ ### AnimatePresence `mode` prop
116
+
117
+ The `mode` prop controls how entering and exiting elements interact during a transition:
118
+
119
+ - **`mode: "wait"`** — the exiting element completes its exit animation fully before the entering element begins its entrance. Use this for route transitions and tab panel swaps, where showing two elements simultaneously would be confusing.
120
+ - **`mode: "sync"`** — enter and exit animations run simultaneously. Use this when you're swapping UI elements (like icon cross-fades) and want both transitions to happen at once.
121
+ - **`mode: "popLayout"`** — the exiting element immediately pops out of the document flow so surrounding elements can animate into their new positions right away, while the exiting element still plays its exit animation. Ideal for list item removal.
122
+
123
+ ### Critical rule: `<AnimatePresence initial={false}>`
124
+
125
+ When `AnimatePresence` wraps persistent UI that already exists on first render — like a tab panel that's visible on page load, or a sidebar that's open by default — you must pass `initial={false}`:
126
+
127
+ ```tsx
128
+ <AnimatePresence initial={false}>
129
+ {isOpen && (
130
+ <motion.div key="panel" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
131
+ {children}
132
+ </motion.div>
133
+ )}
134
+ </AnimatePresence>
135
+ ```
136
+
137
+ Without `initial={false}`, every component inside `AnimatePresence` will play its entrance animation on first mount, even when the user didn't trigger it. This is jarring and wrong — **never animate on initial load for existing UI**.
138
+
139
+ ---
140
+
141
+ ## 4. Layout Animations
142
+
143
+ The `layout` prop is one of Framer Motion's most powerful features. Add `layout` to a `motion` component, and Framer automatically detects when the component's position or size changes in the DOM and animates it from the old layout to the new one — even if the change was caused by other elements shifting around it.
144
+
145
+ ```tsx
146
+ <motion.div layout className="card">
147
+ {isExpanded ? <FullContent /> : <Summary />}
148
+ </motion.div>
149
+ ```
150
+
151
+ When `isExpanded` changes, Framer measures the old and new layouts and smoothly animates the transition. This works for position changes caused by sibling elements reordering, parent resizing, or content toggling.
152
+
153
+ **`layoutId` — shared element transitions:** Assign the same `layoutId` to two different components, and Framer will morph between them when one unmounts and the other mounts. This is the canonical implementation for expanding card → detail transitions and hero → full-view animations.
154
+
155
+ ```tsx
156
+ // Card in list view
157
+ <motion.img layoutId={`product-image-${id}`} src={thumbnail} />
158
+
159
+ // Same image in expanded modal
160
+ <motion.img layoutId={`product-image-${id}`} src={fullImage} />
161
+ ```
162
+
163
+ When the list-view card unmounts and the modal mounts, Framer animates the image from its list-view position to its modal position. The two components don't need to coexist — the transition bridges the gap.
164
+
165
+ **Important:** ensure only one component with a given `layoutId` is mounted at a time. Two simultaneously mounted components with the same `layoutId` will fight each other and produce broken animations.
166
+
167
+ **`layout="position"`:** Use this variant when you only want to animate the element's position, not its size. This prevents layout animation from attempting to smoothly resize the element when content-length changes cause size changes.
168
+
169
+ ---
170
+
171
+ ## 5. Variants and Orchestration
172
+
173
+ Variants let you define named animation states as objects and apply them declaratively, rather than repeating animation values inline throughout your component tree.
174
+
175
+ ```tsx
176
+ const containerVariants = {
177
+ hidden: { opacity: 0 },
178
+ visible: {
179
+ opacity: 1,
180
+ transition: {
181
+ staggerChildren: 0.05,
182
+ delayChildren: 0.1,
183
+ },
184
+ },
185
+ }
186
+
187
+ const itemVariants = {
188
+ hidden: { opacity: 0, y: 10 },
189
+ visible: { opacity: 1, y: 0 },
190
+ }
191
+
192
+ function AnimatedList({ items }) {
193
+ return (
194
+ <motion.ul
195
+ variants={containerVariants}
196
+ initial="hidden"
197
+ animate="visible"
198
+ >
199
+ {items.map(item => (
200
+ <motion.li key={item.id} variants={itemVariants}>
201
+ {item.label}
202
+ </motion.li>
203
+ ))}
204
+ </motion.ul>
205
+ )
206
+ }
207
+ ```
208
+
209
+ **Propagation:** When a parent has `variants` and `animate="visible"`, child `motion` components that also have `variants` will automatically receive the same `animate` value — they don't need their own `animate` prop. Framer propagates the state name down the tree.
210
+
211
+ **`staggerChildren`** delays each child's animation start by the specified seconds after the previous child. A value of `0.05` means each item enters 50ms after the previous — the standard for list entrance animations. More than `0.08` seconds starts to feel slow; more than 6–8 items should stagger in parallel (`staggerChildren` + `staggerDirection` with a cap).
212
+
213
+ **`delayChildren`** adds an initial delay before the first child begins, which gives the parent time to render visibly before its children start entering.
214
+
215
+ ---
216
+
217
+ ## 6. Gesture-Driven Motion
218
+
219
+ Framer Motion provides props that animate elements in response to user gestures without requiring event handlers or state.
220
+
221
+ **`whileHover`:** The animation state while the pointer is hovering. Prefer subtle transforms — `scale: 1.02` for a gentle grow or `y: -2` for a subtle lift. Avoid large scale values (> 1.05) on UI elements; they feel unstable.
222
+
223
+ ```tsx
224
+ <motion.button whileHover={{ scale: 1.02 }} transition={{ type: 'spring', stiffness: 400, damping: 30 }}>
225
+ Save draft
226
+ </motion.button>
227
+ ```
228
+
229
+ **`whileTap`:** The animation state while the element is pressed. The canonical scale-on-press value is **0.96**. Never go below 0.95 (looks broken) and never above 0.98 (imperceptible) for primary interactive elements.
230
+
231
+ ```tsx
232
+ <motion.button
233
+ whileHover={{ scale: 1.02 }}
234
+ whileTap={{ scale: 0.96 }}
235
+ transition={{ type: 'spring', stiffness: 400, damping: 30 }}
236
+ >
237
+ Confirm
238
+ </motion.button>
239
+ ```
240
+
241
+ **`drag` + `dragConstraints`:** Enable drag with `drag` (either `true`, `"x"`, or `"y"`), and bound the drag region with `dragConstraints`. Use `dragElastic: 0.1` for a subtle resistance when dragging beyond the constraint boundaries — this gives physical feedback that the user has reached an edge.
242
+
243
+ ```tsx
244
+ <motion.div
245
+ drag="x"
246
+ dragConstraints={{ left: -100, right: 100 }}
247
+ dragElastic={0.1}
248
+ whileDrag={{ scale: 1.05, cursor: 'grabbing' }}
249
+ >
250
+ Drag me
251
+ </motion.div>
252
+ ```
253
+
254
+ **`whileDrag`:** Applies an animation state while the element is being dragged. Use it to provide visual feedback that the element is in motion (subtle scale-up, shadow, cursor change).
255
+
256
+ ---
257
+
258
+ ## 7. Scroll-Linked Animations
259
+
260
+ **`useScroll()` + `useTransform()`:** For animations that respond continuously to scroll position, use `useScroll` to get `scrollYProgress` (a motion value from 0 to 1 representing document scroll) and `useTransform` to map that range to any animated value.
261
+
262
+ ```tsx
263
+ import { useScroll, useTransform, motion } from 'framer-motion'
264
+
265
+ function ParallaxHero() {
266
+ const { scrollYProgress } = useScroll()
267
+ const y = useTransform(scrollYProgress, [0, 1], [0, -200])
268
+
269
+ return (
270
+ <motion.div style={{ y }}>
271
+ <HeroImage />
272
+ </motion.div>
273
+ )
274
+ }
275
+ ```
276
+
277
+ Use `style={{ y }}` (not `animate`) for scroll-linked values because `animate` creates discrete state transitions, while `style` with a motion value creates continuous, real-time updates.
278
+
279
+ **`whileInView` + `viewport`:** For elements that should animate when they enter the viewport (the most common scroll animation pattern), `whileInView` is more reliable than `useScroll`. It triggers a state change rather than a continuous value mapping, which is easier to reason about and less prone to performance issues.
280
+
281
+ ```tsx
282
+ <motion.section
283
+ initial={{ opacity: 0, y: 20 }}
284
+ whileInView={{ opacity: 1, y: 0 }}
285
+ viewport={{ once: true, margin: '-100px' }}
286
+ transition={{ type: 'tween', duration: 0.4, ease: 'easeOut' }}
287
+ >
288
+ <FeatureSection />
289
+ </motion.section>
290
+ ```
291
+
292
+ **`viewport={{ once: true }}`** is the preferred option for entrance animations — the element animates in once and stays visible. Without `once: true`, the element will animate every time it enters the viewport, which is usually wrong for entrance effects (and can feel janky during fast scrolling).
293
+
294
+ ---
295
+
296
+ ## 8. prefers-reduced-motion Compliance
297
+
298
+ Respecting `prefers-reduced-motion` is **mandatory** for accessibility compliance. Some users have vestibular disorders or motion sensitivity — for them, unnecessary motion causes real physical discomfort. This is not optional polish; it is a WCAG 2.1 requirement.
299
+
300
+ ### Per-component approach with `useReducedMotion`
301
+
302
+ ```tsx
303
+ import { useReducedMotion, motion } from 'framer-motion'
304
+
305
+ function AnimatedCard() {
306
+ const prefersReducedMotion = useReducedMotion()
307
+
308
+ const transition = prefersReducedMotion
309
+ ? { duration: 0 }
310
+ : { type: 'spring', stiffness: 400, damping: 30 }
311
+
312
+ return (
313
+ <motion.div
314
+ initial={{ opacity: 0, y: prefersReducedMotion ? 0 : 10 }}
315
+ animate={{ opacity: 1, y: 0 }}
316
+ transition={transition}
317
+ >
318
+ Content
319
+ </motion.div>
320
+ )
321
+ }
322
+ ```
323
+
324
+ When `prefersReducedMotion` is true, set `duration: 0` and remove any positional animation values — the element should appear instantly without motion.
325
+
326
+ ### App-wide approach with `MotionConfig` (preferred)
327
+
328
+ The cleanest solution for most applications is to wrap the app root with `MotionConfig` using `reducedMotion: "user"`. This instructs Framer to automatically apply reduced-motion behavior for all motion components in the subtree when the OS preference is set — no per-component logic needed.
329
+
330
+ ```tsx
331
+ import { MotionConfig } from 'framer-motion'
332
+
333
+ function App() {
334
+ return (
335
+ <MotionConfig reducedMotion="user">
336
+ <Router />
337
+ </MotionConfig>
338
+ )
339
+ }
340
+ ```
341
+
342
+ `reducedMotion: "user"` reads the OS setting via `prefers-reduced-motion` media query and disables animations globally when it's set. This is the preferred approach because it's zero-maintenance — adding new animated components automatically inherits the behavior.
343
+
344
+ ---
345
+
346
+ ## 9. 60fps Performance Rules
347
+
348
+ Animation performance on the web comes down to a single principle: **only animate properties that the browser can handle on the GPU compositor thread**. Everything else requires the browser to recalculate layout or repaint pixels — work that happens on the main thread and causes dropped frames.
349
+
350
+ ### Properties that are GPU-safe (always use these)
351
+
352
+ - `x`, `y` — map to `translateX()` and `translateY()` in `transform`
353
+ - `scale`, `scaleX`, `scaleY` — map to `scale()` in `transform`
354
+ - `rotate`, `rotateX`, `rotateY`, `rotateZ`
355
+ - `skewX`, `skewY`
356
+ - `opacity`
357
+
358
+ These properties run on the compositor thread and never block the main thread, regardless of how complex the rest of the page is.
359
+
360
+ ### Properties that cause jank (never animate these)
361
+
362
+ - `width`, `height` — triggers layout recalculation on every frame
363
+ - `margin`, `padding` — shifts surrounding elements, triggers full reflow
364
+ - `border-width`
365
+ - `left`, `top`, `right`, `bottom` (on positioned elements) — triggers layout
366
+ - `font-size` — triggers layout and repaint
367
+
368
+ If you need to animate a size change, use `scale` on a wrapper. If you need to animate position, use `x`/`y` rather than `left`/`top`. This distinction is why Framer's layout animation system (Section 4) works — it uses `transform` internally even when responding to layout-driven position changes.
369
+
370
+ ### `will-change: transform`
371
+
372
+ Only add `will-change: transform` when you observe a first-frame stutter on a specific element. Do not add it preemptively — it forces the browser to allocate a separate GPU layer for the element immediately, consuming GPU memory whether or not the animation is actually playing. Reserve it for elements with known performance issues after profiling.
373
+
374
+ ---
375
+
376
+ ## 10. MotionConfig
377
+
378
+ `MotionConfig` is a context provider that configures all `motion` components within its subtree. Use it for:
379
+
380
+ - **Reduced motion compliance** — as shown in Section 8, `reducedMotion: "user"` is the cleanest global solution.
381
+ - **Global transition defaults** — set a default transition for all motion components so individual components don't need to repeat the same `transition` prop.
382
+ - **Custom ease functions** — define a custom cubic-bezier ease once and reference it by name throughout the tree.
383
+
384
+ ```tsx
385
+ <MotionConfig
386
+ transition={{ type: 'spring', stiffness: 300, damping: 25 }}
387
+ reducedMotion="user"
388
+ >
389
+ <App />
390
+ </MotionConfig>
391
+ ```
392
+
393
+ With this configuration, every `motion` component that doesn't specify its own `transition` will use the spring default, and the reduced-motion preference is respected automatically.
394
+
395
+ ---
396
+
397
+ ## 11. Common Pitfalls (from UUPM react.csv data)
398
+
399
+ These are the mistakes that appear most frequently in codebases using Framer Motion:
400
+
401
+ **Missing `initial` with `animate`:** If you add `animate={{ opacity: 1 }}` without `initial={{ opacity: 0 }}`, the element is already at opacity 1 on mount — there's nothing to animate from. Always pair `animate` with `initial` when you want an entrance effect.
402
+
403
+ **Missing `key` props in `AnimatePresence`:** Exit animations will silently not fire if children of `AnimatePresence` don't have `key` props. Framer can't identify which element is leaving without a stable key.
404
+
405
+ **Variant propagation only reaches `motion` children:** Parent variants propagate to child `motion` components, but not to plain HTML children or non-motion React components. If a list item is a plain `<li>` instead of `<motion.li>`, it won't receive the parent's variant orchestration.
406
+
407
+ **`layoutId` conflicts:** If two components with the same `layoutId` are both mounted simultaneously — even briefly during a transition — Framer doesn't know which is the source and which is the target. The result is erratic, broken animation. Ensure mutual exclusivity: when one mounts, the other must have already unmounted.
408
+
409
+ **Unnecessary `AnimatePresence` nesting:** Nesting `AnimatePresence` inside another `AnimatePresence` complicates exit orchestration — inner exits may not complete before outer exits begin. Keep the tree flat; use a single `AnimatePresence` at the appropriate level.
410
+
411
+ **Wrapping everything in `motion.div`:** `motion.div` carries a slightly larger bundle footprint than a plain `div` because it registers the element with Framer's animation engine. Don't wrap static, unanimated elements. Only wrap elements that actually need animation. Reserve `motion.*` for elements where animation provides genuine value.
@@ -0,0 +1,219 @@
1
+ # Gestalt Principles
2
+
3
+ <!-- UUPM ux-guidelines.csv rows deduped into this file and heuristics.md/anti-patterns.md/priority-matrix.md — see .planning/research/uupm-import/ux-guidelines-reconciliation.md -->
4
+ <!-- Source: nextlevelbuilder/ui-ux-pro-max-skill (MIT) — data/ux-guidelines.csv (deduped) -->
5
+
6
+ Gestalt psychology explains how the human visual system automatically organizes individual elements into coherent wholes. Designers who understand Gestalt principles can use them intentionally — grouping what belongs together, separating what is distinct, directing attention flow, and reducing cognitive effort. Designers who ignore these principles will inadvertently create layouts that confuse perception, because the visual system will apply Gestalt organization regardless of the designer's intent.
7
+
8
+ The eight principles below are not rules to follow in isolation — they interact. A single design decision often activates multiple principles simultaneously. The audit checklist at the end of this file helps identify where principles are being violated or underutilized.
9
+
10
+ ---
11
+
12
+ ## 1. Proximity
13
+
14
+ **Definition:** Elements that are physically close to each other are perceived as belonging to the same group, regardless of their visual appearance. Distance communicates separation; closeness communicates relationship.
15
+
16
+ **Design application:** Related controls — a label and its input, an action button and its target, a heading and its body text — should be separated by no more than 8px. Unrelated elements should be separated by at least 32px. When this discipline is applied consistently, users read the layout's meaning before reading its content: the structure itself communicates relationships. Proximity is the most fundamental grouping tool available, and it costs nothing but intentionality.
17
+
18
+ Proximity violations are among the most common layout defects. The symptom is a layout where users do not immediately know which label belongs to which input, or which button acts on which content area. The fix is almost always to increase the distance between unrelated groups and decrease the distance within groups.
19
+
20
+ **Scoring rubric — audit by looking for:**
21
+ - Label-to-input gap: should be ≤8px; flag anything ≥16px
22
+ - Button-to-target gap: a button that acts on a specific element should be adjacent to it, not floating at a distance
23
+ - Section separation: distinct content sections should be separated by ≥32px; flag sections that bleed into each other
24
+ - Orphaned elements: any element that has equal distance to two different groups is ambiguous and should be assigned
25
+
26
+ **CSS/HTML grep signatures:**
27
+ ```
28
+ gap-2 # ≤8px — appropriate for related elements
29
+ gap-8 # 32px — appropriate for section separation; flag if used between related elements
30
+ mb-8 # check if this is separating related or unrelated content
31
+ p-0 # elements with no internal padding may create proximity confusion at borders
32
+ ```
33
+
34
+ ---
35
+
36
+ ## 2. Similarity
37
+
38
+ **Definition:** Elements that share visual properties — color, shape, size, texture, or orientation — are perceived as belonging to the same category. The visual system uses similarity as a shortcut for classification: if it looks the same, it is the same kind of thing.
39
+
40
+ **Design application:** Consistency in visual treatment is not merely an aesthetic preference — it communicates semantic meaning. All primary buttons should look identical: same size, same color, same weight. All destructive actions should look identical: same red, same border treatment, same position relative to the confirm/cancel pair. All secondary navigation items should be visually indistinguishable from each other. When similar-role elements look different, users assume they are different kinds of things, which creates confusion and erodes trust in the interface's logic.
41
+
42
+ Icon weight is one of the most commonly violated similarity contexts. Mixing outline icons with filled icons signals two different visual registers that users will try to interpret as meaningful — even when the mixing is accidental.
43
+
44
+ **Scoring rubric — audit by looking for:**
45
+ - Button variants: are all primary buttons identical? Are all secondary buttons identical? Flag mixed variants on the same screen.
46
+ - Icon weight: are all icons from the same weight family (all outline or all filled)? Flag mixing.
47
+ - List items: do all items in a list have identical visual treatment? Flag items that are visually distinct without a semantic reason.
48
+ - Form fields: are all text inputs styled identically? Flag inconsistencies.
49
+
50
+ **CSS/HTML grep signatures:**
51
+ ```
52
+ btn-primary.*btn-secondary # flag if both appear in same component with identical visual weight
53
+ icon-outline.*icon-filled # flag mixed icon weight patterns
54
+ variant="primary" # audit all variant prop usages for consistency
55
+ ```
56
+
57
+ ---
58
+
59
+ ## 3. Continuity
60
+
61
+ **Definition:** The eye naturally follows lines, curves, and paths in the direction they are already moving. When elements are aligned along an invisible axis, the eye connects them into a continuous flow and expects the line to continue.
62
+
63
+ **Design application:** Use alignment to create invisible flow lines that direct the eye through the layout in the intended sequence. Left-aligned content columns create a strong left-edge flow line that the eye tracks downward. A row of icons creates a horizontal flow line. A step indicator with connected segments creates a continuous path that the eye follows from start to finish. Carousels and horizontal scrollers leverage continuity by partially revealing the next item — the visible edge implies that more content continues in the same direction.
64
+
65
+ Continuity is disrupted when elements break expected alignment without a purposeful reason. An element that juts out of an otherwise aligned column creates a visual interrupt — which can be used intentionally to draw attention, or accidentally to create confusion.
66
+
67
+ **Scoring rubric — audit by looking for:**
68
+ - Alignment consistency: are all left-aligned elements aligned to the same grid column? Flag arbitrary left-offset elements.
69
+ - Step indicators: does the visual path between steps flow clearly? Flag broken or visually interrupted step flows.
70
+ - Carousel edge reveals: does the last visible item partially reveal the next? Flag carousels that do not imply continuation.
71
+ - Broken columns: are there elements that break column alignment without a documented reason?
72
+
73
+ **CSS/HTML grep signatures:**
74
+ ```
75
+ ml-auto # right-alignment break — check if intentional
76
+ text-center # mixed with text-left in same flow — flag if alignment signal is inconsistent
77
+ translate-x # horizontal animation — verify it implies continuity, not arbitrary motion
78
+ ```
79
+
80
+ ---
81
+
82
+ ## 4. Closure
83
+
84
+ **Definition:** The human visual system actively completes incomplete shapes, filling in missing information to perceive a whole. Users will "see" a rectangle even if its corners are open, or a circle even if its arc is broken, because the mind prefers complete, recognizable forms over fragments.
85
+
86
+ **Design application:** Closure is widely used in logos and icons to create forms that feel complete while being visually light. In UI, closure explains why partial borders can suggest containment without a full rectangle: a top border on a card, or a left border on a quoted text block, implies a region even without three additional sides. Progress indicators with open ends imply continuation; closed rings imply completion. Skeleton loading states use closure — partial shapes that the user's mind completes as content — to make loading feel purposeful rather than empty.
87
+
88
+ Closure can also be violated: a progress bar that ends before reaching the container's right edge correctly communicates incompletion, but if it ends at an arbitrary position with no visual context, users may perceive a broken UI rather than a progress state.
89
+
90
+ **Scoring rubric — audit by looking for:**
91
+ - Progress indicators: does the fill/track relationship clearly communicate completion percentage? Flag indicators where progress direction is ambiguous.
92
+ - Partial borders: do partial borders clearly imply the group they define? Flag partial borders that could be mistaken for decorative rules.
93
+ - Skeleton states: do skeleton shapes meaningfully correspond to the content they represent? Flag skeletons that are too abstract to prime recognition.
94
+ - Logo/icon edges: do open edges in icons close convincingly at standard display sizes?
95
+
96
+ **CSS/HTML grep signatures:**
97
+ ```
98
+ border-l # left-only border — check if closure context is clear
99
+ border-t # top-only border — check if closure context is clear
100
+ rounded-full # complete closure (circle/pill) — appropriate for completion states
101
+ w-1/2 # partial fill — verify progress context is established by container
102
+ ```
103
+
104
+ ---
105
+
106
+ ## 5. Figure-Ground
107
+
108
+ **Definition:** The visual system constantly distinguishes between a subject (figure) and its context (ground). Figures are perceived as having form, existing in front, and being the focus of attention. Ground is perceived as formless, behind, and non-focal. This distinction happens automatically and is fundamental to perceiving anything at all.
109
+
110
+ **Design application:** Every interactive element, content region, and overlay depends on successful figure-ground separation. Modal dialogs work because the scrim pushes the page content to ground and the dialog to figure. Buttons work because their filled background distinguishes them from the text-on-ground surrounding them. Navigation bars work because their elevated background separates them from the page content they sit above.
111
+
112
+ The practical rule: foreground elements must have at least 3:1 contrast ratio against their background, and for text, 4.5:1 for body text (WCAG AA). But figure-ground extends beyond contrast — blur, shadow, and opacity all contribute. A high-contrast element on a cluttered background may still fail to read as figure if the background is too visually active.
113
+
114
+ **Scoring rubric — audit by looking for:**
115
+ - Modal scrim: is the background content pushed to ground with sufficient opacity or blur? Flag modals where the page behind is at full visibility and full saturation.
116
+ - Button states: do buttons clearly read as figure against all surface colors they appear on? Flag buttons with insufficient background contrast.
117
+ - Active navigation items: are selected/active states clearly distinguished from non-selected? Flag flat navigation with only a color difference.
118
+ - Card separation: do cards separate from the page surface? Flag cards with no shadow, border, or background differentiation.
119
+
120
+ **CSS/HTML grep signatures:**
121
+ ```
122
+ bg-white.*text-white # invisibility risk — figure and ground collapsed
123
+ bg-black/50 # modal scrim — verify opacity is sufficient
124
+ z-index|z-[0-9] # stacking context — verify figure-ground intent is preserved
125
+ opacity-0.*opacity-100 # transition — verify figure emerges cleanly from ground
126
+ ```
127
+
128
+ ---
129
+
130
+ ## 6. Common Fate
131
+
132
+ **Definition:** Elements that move together — in the same direction, at the same speed, and with the same timing — are perceived as belonging to the same group, even if they are spatially separated. Movement is a powerful grouping signal precisely because it overrides static proximity and similarity cues.
133
+
134
+ **Design application:** When a group of elements should be perceived as a unit, animate them with shared timing. A card that expands while its child elements simultaneously rearrange communicates that the card and its contents are one object. A list that reorders with synchronized item movement communicates that the list is a coherent set. Conversely, animating elements at different speeds signals that they are independent objects — which can be used to establish hierarchy (parent first, then children) by staggering their entrance timing.
135
+
136
+ Staggered animation (where sub-elements enter sequentially with a small delay) is a specific application of common fate that establishes a visual hierarchy within a group: the first element that moves is perceived as most important, and the trailing elements are perceived as its dependents.
137
+
138
+ **Scoring rubric — audit by looking for:**
139
+ - Group animations: when a container appears or changes, do its children animate with it or independently? Flag children that animate on unrelated timings.
140
+ - List reorder: when items reorder, do they move with shared timing that communicates the reorder as one operation? Flag lists where individual items move asynchronously.
141
+ - Exit animations: when a group exits, do all elements leave together? Flag cases where parts of a group exit before the container.
142
+
143
+ **CSS/HTML grep signatures:**
144
+ ```
145
+ transition-all # check if used on group container vs. children separately
146
+ stagger # check stagger timing for hierarchy signal
147
+ animate-*.*animate-* # multiple simultaneous animations — verify they share timing
148
+ delay-[0-9] # stagger implementation — verify delay communicates hierarchy
149
+ ```
150
+
151
+ ---
152
+
153
+ ## 7. Common Region
154
+
155
+ **Definition:** Elements enclosed within a clearly defined boundary — a border, a background color, a shadow, or any other perceptual container — are perceived as belonging to the same group, even if they are not close to each other. Common region overrides proximity: two elements far apart within the same bounded region are perceived as more related than two elements close together on either side of a region boundary.
156
+
157
+ **Design application:** Cards, panels, table rows, form field groups, and toolbars all leverage common region by using visual boundaries to say "these things go together." The boundary does not need to be a literal border — a distinct background color works equally well. This is why alternating row colors in a data table immediately communicate that each row is a distinct unit, and why a card with a white background on a grey page surface reads as a contained group without needing a border.
158
+
159
+ Common region is also useful for communicating hierarchy: nested regions (a card within a page, a sub-section within a card) communicate nested relationships. The visual boundary at each level tells the eye exactly how far a group extends.
160
+
161
+ **Scoring rubric — audit by looking for:**
162
+ - Card boundaries: do cards have a visible boundary (shadow, border, or background) that clearly separates them from their surroundings? Flag cards that blend into the page surface.
163
+ - Form groups: are related form fields visually grouped within a shared container? Flag forms where field groups are separated only by vertical spacing without a region signal.
164
+ - Table rows: are table rows distinguishable as individual regions? Flag tables with no row separation signal.
165
+ - Nested regions: are nested groupings visually distinguishable from their parent container?
166
+
167
+ **CSS/HTML grep signatures:**
168
+ ```
169
+ rounded.*shadow # card pattern — verify region boundary is sufficient
170
+ bg-gray-50.*bg-white # alternating region backgrounds — appropriate for table rows, list items
171
+ border.*rounded # explicit region boundary — verify visual weight is appropriate for nesting level
172
+ divide-y # table row divider — check if combined with sufficient vertical padding
173
+ ```
174
+
175
+ ---
176
+
177
+ ## 8. Prägnanz (Law of Simplicity)
178
+
179
+ **Definition:** The visual system always interprets ambiguous inputs in the simplest possible way. When multiple interpretations of a visual input are possible, the mind chooses the interpretation that requires the least cognitive work. Complexity is resolved toward simplicity automatically.
180
+
181
+ **Design application:** Prefer simple, recognizable shapes over complex, irregular ones. Remove any visual element that does not communicate something. Every decoration that does not carry meaning adds to the cognitive load the user must process before reaching the content that actually matters. This is the principle behind minimalism in UI design — not because minimalism is aesthetically superior, but because unnecessary visual complexity consumes perceptual resources that should be directed at the interface's actual purpose.
182
+
183
+ Prägnanz also implies that when two layouts can communicate the same information, the simpler one is better. A three-color palette is simpler to parse than a seven-color palette, even if the seven-color palette is "more interesting." A consistent component structure is simpler to navigate than a varied one, even if the variation is intentional.
184
+
185
+ **Scoring rubric — audit by looking for:**
186
+ - Decorative elements: identify any visual element that serves no communicative purpose. Flag gradients, textures, and ornamental icons that do not carry semantic meaning.
187
+ - Color count: how many distinct colors appear on a single screen? Flag screens with more than 4–5 distinct colors where the additional colors are not semantically required.
188
+ - Shadow and border redundancy: are both shadow and border used simultaneously on the same element without a reason? Flag redundant depth cues.
189
+ - Animation without purpose: identify any animation that does not communicate state change, progress, or relationship. Flag animations that exist for decoration alone.
190
+
191
+ **CSS/HTML grep signatures:**
192
+ ```
193
+ bg-gradient # decorative gradient — verify it communicates something
194
+ border.*shadow # redundant boundary signals — flag unless both serve distinct purposes
195
+ animate-bounce # decorative animation — flag if it does not communicate a meaningful state
196
+ after:.*before: # pseudo-element decorations — verify each is communicative
197
+ ```
198
+
199
+ ---
200
+
201
+ ## Gestalt Audit Checklist
202
+
203
+ Use this checklist when auditing a screen for Gestalt compliance. Each item maps to one or more principles.
204
+
205
+ 1. **Proximity check:** Can you identify every visual group by spacing alone, without relying on borders or background colors? Related elements should cluster tightly (≤8px); unrelated groups should breathe apart (≥32px). Flag any element where group membership is ambiguous from spacing alone.
206
+
207
+ 2. **Similarity check:** Do all elements of the same semantic role share identical visual treatment? Primary buttons match primary buttons. Destructive actions match destructive actions. Icons use consistent weight. Flag any visual inconsistency that users might interpret as a semantic difference.
208
+
209
+ 3. **Continuity check:** Does the layout create a clear reading path through its most important content? Can you trace an invisible line — horizontal, vertical, or diagonal — that connects the primary focal points in intended viewing order? Flag layouts where the reading path requires backtracking.
210
+
211
+ 4. **Closure check:** Are any incomplete shapes used? If so, do they close convincingly at the display size and resolution? Do progress indicators clearly communicate fill direction and completion scale? Flag ambiguous incomplete shapes.
212
+
213
+ 5. **Figure-ground check:** Does every interactive element have sufficient contrast against its background? Do modals and overlays effectively push page content to ground? Are active navigation states clearly elevated above inactive ones? Flag anything where figure and ground are insufficiently distinct.
214
+
215
+ 6. **Common fate check:** When elements animate, do grouped elements share timing? Is stagger used intentionally to signal hierarchy rather than arbitrarily? Flag groups where member elements animate independently with no shared timing.
216
+
217
+ 7. **Common region check:** Are all logically related groups of elements contained within a visible boundary (card, panel, background, or border)? Can a user identify group membership from the region boundary alone? Flag groups that rely only on proximity without a region signal in contexts where the proximity alone is insufficient.
218
+
219
+ 8. **Prägnanz check:** Remove one element at a time and ask: does the screen communicate less information without it? If the answer is no for any element, that element is decorative noise. Flag decorative elements, redundant visual signals, and any source of visual complexity that does not carry proportionate communicative value.