Files
K-D-C 6457347dd6 docs(patterns): add 'Patterns We Like' scrapbook + link from README
A running list of UI effects/interactions worth keeping, each with the four-point
bar (does a job, respects reduced-motion, transform/opacity only, degrades) it
must clear before shipping. README structure + quick-start now point to it.
2026-06-06 11:37:57 +00:00

5.4 KiB
Raw Permalink Blame History

Patterns We Like

A running scrapbook of UI effects, interactions and micro-patterns we've come across and want to keep — so a future app or project can reach for something proven instead of reinventing it.

This list sits slightly apart from the canonical principles on purpose. Those say "clarity over cleverness" and "motion is feedback, never decorates." The entries here are often delight effects that flirt with that line. That's the point of the doc: keep the tempting stuff in one place and record the bar each must clear before it earns a spot in a shipping product. A pattern we like is not a pattern we'll always use.

The bar (when a delight effect earns its place)

Before any effect below ships in a KDC product, it has to clear all four:

  • It does a job. It communicates state, affordance, continuity or confirmation — not just "looks nice." If it's purely decorative, it doesn't ship (per Motion).
  • It respects prefers-reduced-motion: reduce — collapses to a static/instant state, no exceptions.
  • It animates transform / opacity only — never width / height / layout. Compositor-friendly or it doesn't go in.
  • It degrades gracefully — works without it, and doesn't trap focus, block input, or break on keyboard/touch.

If an entry can't meet the bar in a given spot, that's a "no" for that spot — not a reason to drop the pattern from the list.

How to add an entry

Keep it light — append a block in this shape. Don't turn it into a form.

### <name>
**Status:** idea | trialed | adopted-in-<app>
**What it is:** one line.
**Why we like it:** one or two lines.
**Snippet:** the smallest working version.
**Fits / avoid:** where it earns its place, and where it doesn't.
**Watch-outs:** the honest gotchas — performance, a11y, edge cases.
**Found:** where it came from (link / who / when).

Status legendidea (caught our eye, untried) · trialed (prototyped in a branch) · adopted-in-<app> (shipped, name the app).


Entries

Proximity-magnification dock

Status: idea

What it is: macOS-dock-style hover magnification — across a row (or column) of equal items, the one nearest the cursor scales up, neighbours scale less, tapering to nothing past a set radius. The bulge follows the pointer.

Why we like it: tiny amount of code, self-resetting (far items naturally land back at scale 1), and scale is compositor-cheap. Used in the right place it doubles as a targeting affordance — it literally points at the thing you're about to click.

Snippet — the whole trick is "distance → 0..1 closeness → size":

// Vanilla, horizontal. 120 = reach in px; 0.5 = max growth (→ 1.5×).
onpointermove = e =>
  document.querySelectorAll(".dock > *").forEach(el => {
    const r = el.getBoundingClientRect();
    const t = Math.max(0, 1 - Math.abs(e.clientX - (r.x + r.width / 2)) / 120);
    el.style.scale = 1 + t * 0.5;
  });

In our apps (all React 19) it belongs in an effect with cached rects + rAF + cleanup, and gated on reduced-motion:

useEffect(() => {
  if (matchMedia("(prefers-reduced-motion: reduce)").matches) return; // clears the bar
  const items = [...dockRef.current.children];
  let rects = items.map(el => el.getBoundingClientRect());            // cache — don't measure per-move
  const remeasure = () => { rects = items.map(el => el.getBoundingClientRect()); };
  const onMove = e => requestAnimationFrame(() => items.forEach((el, i) => {
    const c = rects[i].x + rects[i].width / 2;                        // use clientY + rect.y for a vertical dock
    const t = Math.max(0, 1 - Math.abs(e.clientX - c) / 120);
    el.style.scale = 1 + t * 0.5;
  }));
  window.addEventListener("pointermove", onMove);
  window.addEventListener("resize", remeasure);
  return () => { window.removeEventListener("pointermove", onMove); window.removeEventListener("resize", remeasure); };
}, []);

Fits / avoid:

  • A row/column of ≥ 5 equal icon targets you actually click — tool palettes, icon rails, app launchers. (KDC candidate: the floor-plan tool palette in kdc_void_plannerPalette.tsx, a vertical rail of ~10 tool buttons. Vertical, so drive it off clientY.)
  • 23 item navs — the ripple is invisible with so few items.
  • Step indicators / status strips — those communicate progress/state, not click targets. Magnifying them is semantically confusing and fails the bar (it does no job).

Watch-outs:

  • Layout thrash. getBoundingClientRect() per pointermove forces synchronous layout. Cache the rects (re-measure on resize/scroll), and batch the writes in requestAnimationFrame.
  • Horizontal-only as written (clientX). Swap to clientY + rect.y for a vertical rail.
  • Neighbours overlap rather than push apart. scale doesn't reflow, so a growing item grows over its neighbours instead of displacing them (the real Dock shoves siblings outward). Looks fine with generous gaps/padding; less polished without.
  • transform-origin. For a docked edge, set the origin to that edge (e.g. transform-origin: bottom / left) so items grow out of the dock, not in both directions.
  • Reduced motion + input parity. Bail when prefers-reduced-motion is set; never let the scale affect hit-testing/keyboard order.

Found: Paul, May 2026 — a "proximity hack" snippet. Decoded + React-adapted in-session.