# Showpiece

> Hero-grade set pieces for ultraweb builds — 2D canvas, shader/mesh gradients, particles, R3F/three.js 3D — under taste's hard gate — only when DIRECTION.md demands it, 60fps verified on mid hardware, static fallback and reduced-motion path mandatory, one per site. Covers the cost ladder (CSS → canvas → shader → R3F), next/dynamic ssr:false mounting, and LCP protection. Invoke in the ultraweb motion phase (Phase 9) when DIRECTION.md commissions it, or when the user says "WebGL hero", "3D product view", "shader gradient", "particles", "something jaw-dropping in the hero", "an interactive centerpiece", or "make a 3D view shareable / deep-linkable".

- Skill: `blyatiful1/showpiece` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add blyatiful1/showpiece`
- Raw SKILL.md: https://api.skillmd.com/api/skills/blyatiful1/showpiece/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: blyatiful1 (https://skillmd.com/u/blyatiful1)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/blyatiful1/showpiece

---


# showpiece — one set piece, fully earned

**Stage:** Phase 9 — Motion (DIRECTION-gated) - **Reads:** design/DIRECTION.md (signature move), design/SYSTEM.md, design/BRIEF.md - **Writes:** ONE signature element (components/showpiece/*) + its static fallback

## Standard

Taste's rule verbatim: 3D, shaders, canvas only when the direction demands it, it runs 60fps on mid hardware, and there's a static fallback — a fast plain site beats a janky impressive one, always. One showpiece per site; it IS the signature move or directly serves it. First-grade additionally means:

- **The showpiece never owns LCP.** Hero headline and CTA render from server HTML and paint first; the canvas mounts behind or after them.
- **The static fallback is itself designed** — a poster frame (gradient, SVG composition, or treated image via `imagery`) that would pass `gate-visual` alone. Reduced-motion users, no-WebGL browsers, and the pre-hydration frame all see it; it is not a degradation, it is the second edition of the design.
- **The fallback carries the argument in words, not just a picture.** A showpiece asserts a claim — about the product, the craft, the world; the accessible edition must state that claim, not gesture at it. Every canvas/WebGL/R3F section ships a sibling narrative — 2–4 sentences of real copy authored in Phase 8 (`ultraweb:copywriting`), never template alt text like "an interactive 3D scene" — in an `sr-only` block that becomes visible under `prefers-reduced-motion`. The meaning survives without the spectacle; that is the test. (Germany's BFSG, in force since 2025-06-28, makes this a legal floor for in-scope DACH builds — gate-accessibility owns the scoping — and the constitution's own floor everywhere else.)
- **Three exits wired**: `prefers-reduced-motion` → static; WebGL/context unavailable → static; tab hidden or element offscreen → animation loop paused.
- **A navigable scene is app state, not a demo reel.** If the showpiece is an explorable 3D/canvas view — an R3F scene, multiple camera framings, a scene selector — its camera (position, target, fov) and active scene belong in the URL, not trapped in `useState`. Encode them into a `?view=` search param and restore on mount, so the moment is bookmarkable, shareable, and reachable by back/forward — a place, not a reel. `ultraweb:routing` owns the URL contract; the discipline that keeps it cheap is in the mounting pattern below.
- **Cheapest rung wins.** Climb the cost ladder only as far as the direction requires.
- **Progressive Spectacle Tiers (`award-canon`) is the operational form of this gate.** The three exits above ARE the tiers — a complete static/semantic baseline (no-JS, no-WebGL, reduced-motion), a CSS/Motion-enhanced tier, an optional WebGL top rung — identical content across all three, selected by capability + preference. Build the static tier FIRST as the durable edition, never a degraded stub.
- **Weight as a Feature (`award-canon`): payload is a headline constraint set before richness, not after.** The winners that lasted ship full 3D worlds in single-digit MB — Messenger 5.7MB initial (Developer Site of the Year 2025), Bruno Simon ~2.8MB, Orano 901KB gzipped; set a byte budget before the first shader, because a 9MB/6s page will not win regardless of beauty. Most OLDER canvas-only winners in the corpus are now dead or replaced — Messenger is the living exception, and even it ships essentially no static/SEO path — so the semantic layer is what survives.
- **What the set piece should BE (`award-canon`).** One Material World — commit every surface to a single primitive (ice, glass, points of light, grain) so the piece reads as one world, not a pile of unrelated effects; Fake-Depth Before Real Depth — layered 2D parallax, baked light, and matcaps buy the *look* of depth at a fraction of the cost, which is why the cost ladder climbs slowly; and one persistent hero object examined through scroll out-executes ten decorated sections.

## Process

1. **Gate check — all four or stop:** (a) DIRECTION.md names the showpiece as/serving the signature move; (b) the site type has the energy budget per taste (portfolio/agency/product-hero — not a local-business brochure); (c) a first-grade static fallback is achievable; (d) you will profile before shipping. Any "no" → `scroll-motion` or `physics` likely delivers the signature cheaper.
2. Pick the lowest sufficient rung on the cost ladder (below).
3. **Build the static fallback FIRST** and drop it into the layout. The page must be shippable at this point.
4. Implement the live piece behind `next/dynamic` with `ssr: false` (this call must live in a `"use client"` file), using the fallback as `loading` state so there is never a blank frame.
5. Wire the three exits; cap `devicePixelRatio` at 2; pause the loop when `document.hidden` or the element leaves the viewport (IntersectionObserver).
6. **Verify empirically:** DevTools performance recording ≥5s of interaction — steady 60fps, no long tasks >50ms; re-run at 4x CPU throttle and confirm it stays fluid (≥30fps) or falls back; measure the client-bundle delta from the network log (encodedBodySize per gate-performance's cold-load step) or `npx next experimental-analyze --output` before/after — Next 16's build prints no sizes — the delta is a design decision, record it in SYSTEM.md.

## Cost ladder

- **CSS set piece (0kb JS).** Animated gradient via `@theme` `--animate-*` keyframes, masked display type, blend modes, layered SVG. Try to kill the showpiece here first — many "WebGL" briefs are satisfied by a great CSS backdrop.
- **2D canvas (0kb deps).** Particle fields, generative lines, noise flows. Cap ~200–400 particles on mid hardware; zero object allocation inside the draw loop (pre-allocate arrays, reuse vectors); one `requestAnimationFrame` loop, DPR-capped.
- **Shader gradient (WebGL, small).** Animated mesh/noise gradient as a hero backdrop — either a maintained shader-gradient library (verify current package options and React 19 compat against docs first) or a hand-rolled fragment shader on a raw WebGL quad (~150 lines, no dependency).
- **R3F 3D (heaviest).** `@react-three/fiber` + `@react-three/drei` for product views, 3D type, scenes. three.js core alone adds on the order of 150kb+ min+gzip to the client bundle — the largest single dependency decision in the build; verify current versions and React 19 compatibility against docs before installing. Discipline: `dpr={[1, 2]}`, demand-driven frameloop for static-ish scenes, compressed models, no per-frame allocations in `useFrame`.

**Side branch, not a rung — vector-authored moments (anime.js, +~19 KB gz per STACK.md).** A commissioned SVG timeline — multi-path draw, morph, motion path, split-text choreography — is a different medium, not a cheaper canvas, so it never sits between the two 0-dep rungs and never excuses skipping them: if CSS or a 2D canvas satisfies the brief, they still win. Take the branch only when the moment is *inherently* vector (a diagram that draws itself, a mark that assembles, a route that traces) and DIRECTION.md names it at intensity ≥2 (a scrubbed or pinned SVG timeline needs 3); ultraweb:animejs owns the install gate. Everything above holds unchanged — one showpiece per site, and the three exits wired: reduced motion → the final drawn state, no-WebGL/no-JS/pre-hydration → the server-rendered static SVG, hidden tab or offscreen → the timeline paused.

**Past the top rung — when the scene is the site (`ultraweb:set-design`).** The ladder above ends at ONE set piece: a canvas that lives in one section, mounts behind server-rendered text, and can be deleted without the site changing shape. When DIRECTION.md commissions a *site-scale* immersion instead — a canvas persisting across routes, a camera the scroll drives through more than one page, one material world every route shares — that is not a taller rung, it is a different commission, and this skill hands it upward. The hand-off is one-way and narrow: escalate only when archetype 12 is committed, intensity 3 is recorded, and DIRECTION.md names `ultraweb:set-design` together with its route scope, its byte budget and its static edition, in writing. Everything on this page still binds — the three exits, the designed static edition, the `sr-only` narrative that carries the argument, the byte budget, the 60fps + 4×-throttle proof — and it binds at *every* route the scene survives, not just the one it started on. A brief that only wants a big hero stays here.

## Mounting pattern

```tsx
// components/showpiece/index.tsx
"use client";
import dynamic from "next/dynamic";
import { StaticPoster } from "./static-poster"; // the designed fallback

const Scene = dynamic(() => import("./scene"), {
  ssr: false,
  loading: () => <StaticPoster />,
});

export function Showpiece() {
  if (typeof window !== "undefined" &&
      window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
    return <StaticPoster />;
  }
  return <Scene />;
}
```

Inside `scene.tsx`: feature-detect the WebGL context and return `<StaticPoster />` on failure; register `visibilitychange` + IntersectionObserver to pause the loop. The hero section renders headline/CTA as server HTML and places `<Showpiece />` in the background/media slot — never the other way around.

**Scene state in the URL (navigable scenes only).** Serialize a small pose object to base64 in `?view=` and hydrate from it on mount. Commit discrete scenes with `router.push` (each is a back-button entry); stream continuous camera drags with `router.replace` so a drag doesn't spam history — and write back only once a tween settles, never inside `useFrame`.

```tsx
// inside the "use client" scene wrapper — navigable scenes only
const params = useSearchParams();
const router = useRouter();                         // next/navigation
const initial = decodePose(params.get("view"));    // base64 → { pos, target, fov }

// on settle (tween end / OrbitControls 'end' event) — NOT per frame:
const persist = (pose: Pose, discrete: boolean) => {
  const href = `?view=${encodePose(pose)}`;
  discrete ? router.push(href, { scroll: false })
           : router.replace(href, { scroll: false });
};
```

## Pass criteria

Record all six in design/SYSTEM.md (decision + bundle delta) and design/QA.md (measurements) before the gates run:

1. Performance recording shows steady 60fps over ≥5s of interaction, zero long tasks >50ms.
2. 4x CPU throttle stays fluid (≥30fps) or the piece detects and falls back.
3. LCP element is server-rendered text/image, not the canvas — confirmed in DevTools.
4. Reduced-motion emulation renders `StaticPoster` with its narrative text visible, and the poster plus that narrative carry the argument on their own (real copy, not template alt text).
5. Kill WebGL (or test a no-WebGL context): static path renders, console clean.
6. Client bundle delta measured before/after from the network log or `next experimental-analyze` (never the `next build` output — Next 16 prints no sizes) and accepted deliberately.

## Anti-patterns

- Scene imported statically (grep the showpiece for imports without `dynamic(`) — three.js lands in the shared client bundle.
- Canvas as the LCP element — hero text must paint from server HTML first.
- `requestAnimationFrame` loop with no `document.hidden`/IntersectionObserver pause — burns battery on hidden tabs; grep the raf callsite for a visibility guard.
- Allocations per frame: `new THREE.Vector3(...)` or object literals inside `useFrame`/the draw loop — GC hitching.
- A gray box or `null` as the fallback — the fallback is a designed deliverable, not an apology.
- Two showpieces — taste allows one signature move; the second dilutes both.
- A second set piece justified as "the site is immersive", with no DIRECTION.md line naming `ultraweb:set-design`, its route scope, and its byte budget — that is two showpieces wearing one word.
- Particle-count flexing: thousands of particles at 20fps loses to hundreds at 60fps every time.
- A spinning 3D object with no relation to the brief — decoration is not direction; if DIRECTION.md can't say what it means, cut it.
- Skipping the 4x-throttle run — "60fps on my machine" is not "60fps on mid hardware".
- Camera or scene locked in `useState` on a navigable piece — a view no link can reach and the back button can't undo is a demo reel wearing a URL bar. Grep an explorable scene for `useSearchParams`; its absence is the smell.
- Writing the URL every frame — `router.replace` inside `useFrame` thrashes history and re-renders; sync only when the tween settles.
- An `sr-only` narrative that parrots template alt text ("an interactive 3D scene") instead of the claim the visual makes — it must carry the argument, or it fails the screen-reader user (and, on an in-scope DACH build, the BFSG).

## Worked example — Studio Norra, Oslo agency portfolio index

Moved to `references/example.md` — read only when this build's case is genuinely ambiguous; the sections above are the decision material.

## Composes with

Moved to `references/composes.md` — the handoff map; load it when orchestrating this skill against its neighbors.

