Motion — spatial & perceived depth
Pseudo-3D motion on flat surfaces: pointer/gyroscope parallax, depth-on-scroll, and 3D transitions (flip, cube, perspective shared- element). These imply depth without real geometry — they are a PERCEPTUAL projection of the depth axis (`themes/depth.kmd § R4`). Strictly gated by the reduced-motion contract. Material parity: extends M3 motion with the spatial/expressive depth cues M3 leaves implicit.
Facet Visual do Koder Design. Sub-spec of
motion.kmd. Material parity: https://m3.material.io/styles/motion + the expressive spatial cues M3 leaves to the implementer.
On a flat screen, depth (depth.kmd) can be implied by motion. This is perceptual, never a real layer — it must not reorder the stacking bands (depth.kmd § R3) and it is always the first thing to switch off under reduced-motion.
Reduced-motion is mandatory — see
physics.kmd. Every effect here MUST degrade to a static, non-parallaxed layout.
R1 — Parallax (pointer & gyroscope)
Layers translate at a rate inverse to their depth — near layers move more, far layers move less (distant scenery).
| Token | Meaning | Default |
|---|---|---|
--kds-parallax-max |
Max layer translation (px) at depth 1 | 12 |
--kds-parallax-falloff |
Multiplier per depth level deeper | 0.6 |
- A layer at depth
dmoves--kds-parallax-max × falloff^(d-1). - Pointer: track cursor offset from element centre (desktop).
- Gyroscope:
deviceorientationβ/γ on mobile (opt-in; needs userpermission on iOS — fall back to no parallax if denied).
- Clamp total travel so text never clips its container.
R2 — Depth-on-scroll
As the viewport scrolls, layers parallax vertically by depth — a hero image recedes slower than its foreground caption.
- Driven by
scroll-timeline/IntersectionObserver, never ascroll-jank
requestAnimationFramestorm. - Magnitude scales with depth (R1 falloff).
- MUST NOT trigger layout (transform only — no top/height animation).
R3 — 3D transitions
Perspective transitions between two states of the same surface:
| Pattern | Use | Notes |
|---|---|---|
| Flip | Reveal back of a card (front ↔ details) | rotateY(180deg); both faces backface-visibility: hidden |
| Cube | Step through a small sequence (≤ 4 faces) | rotateY on a transform-style: preserve-3d parent |
| Perspective shared-element | Hand off an element between screens with depth | Extends the 2D shared-axis pattern in transitions.kmd |
perspectivebetween600–1000px(closer = more dramatic; default800px).- Lighting is consistent: a face rotating away dims toward the shadow
side of the rig (
lighting.kmd § R1). - Duration from
easing-duration.kmd; spring option fromphysics.kmd.
R4 — Performance
- Animate
transform+opacityonly (compositor-friendly). - Budget: 60 fps; parallax handlers throttled to one update per frame
(
requestAnimationFrame), never per pointer event. will-change: transformonly while active; removed at rest.- Disable all effects when the tab is backgrounded.
R5 — Forbidden patterns
- ❌ Any effect that ignores
prefers-reduced-motion(R1 intro). - ❌ Parallax that reorders real stacking bands (
depth.kmd § R3). - ❌ Animating layout properties (topleftwidth/height) for depth.
- ❌ Parallax >
--kds-parallax-maxthat clips or detaches content. - ❌ Gyroscope parallax without a no-permission fallback (R1).
- ❌ Flip/cube on surfaces carrying critical text that becomes
unreadable mid-rotation (keep faces legible or fade).
R6 — Accessibility
prefers-reduced-motion: reduce→ static layout, no parallax, no3D rotation (cross-fade or instant swap instead).
- Parallax is decorative: it MUST NOT move focusable targets out from
under the pointer/keyboard focus.
- Motion sickness ceiling: no rotation faster than 30°/s in the user's
gaze area (mirrors
xr-preview.kmd § R8).
T1-T3 — Tests
T1 — Reduced-motion: with prefers-reduced-motion, parallax travel is 0 px and 3D transitions resolve as a static state change. T2 — Transform-only: scrollparallax effects animate only transform opacity (no layout-triggering property in the animation). T3 — Falloff: a layer at depth 2 moves --kds-parallax-falloff× the travel of a depth-1 layer (within 1 px).
Live demo
<div data-kds-3d-demo"depth-parallax" data-aria-label"Interactive parallax demo: move the pointer to see layers shift by depth level. Static under reduced-motion.">/div
Cross-link
themes/motion/physics.kmd— reduced-motion contract (mandatory)themes/motion/transitions.kmd— 2D pattern catalog (R3 extends it)themes/depth.kmd— the axis these effects project (§ R4)themes/lighting.kmd— face shading during 3D rotation (R3)