three-d-scene
Skill0 starsChanged 6 months agoMITView on GitHub
Author React Three Fiber scenes — camera setup, drei utilities, instancing, suspense boundaries, and mobile-fallback strategy; auto-activates on `/scene` and R3F files.
In this repository10 other files it gives its agents.
- code-presentationSkill
- color-engineeringSkill
- dashboard-designSkill
- data-visualizationSkill
- design-system-keeperSkill
- images-and-mediaSkill
- motion-designSkill
- rich-ui-patternsSkill
- svg-illustrationSkill
- ui-design-principlesSkill
The file
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 0 are distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 54
Always wrap content in <Suspense> — drei/textures/gltf throw promises.
- Very specific: line 66
Default position: pull back enough that the subject occupies ~60% of frame.
- Very specific: line 109
Use useFrame for animation, not React state.
- Very specific: line 129
Reduced-motion: if prefers-reduced-motion, render a still-image poster (rendered once at build, or <Canvas frameloop="never"> after first …
- Very specific: line 131
Low-end device: use <PerformanceMonitor onDecline={…}> to drop DPR (<AdaptiveDpr pixelated />) and disable post-processing when fps < 40.
- Very specific: line 208
Do not put critical content inside <Canvas> — screen readers can't see it.
- Very specific: line 217
Always lazy-loaded by the consuming page (dynamic(import …, { ssr: false })).
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Three-D Scene
R3F scenes are heavy. Always scoped to a single component, always behind a Suspense boundary, always with a still-image fallback for mobile and reduced-motion users. Adding three, @react-three/fiber, @react-three/drei to deps requires explicit user confirmation (spec 6.8).
Constitution (verbatim — applies to every artifact)
- Max 300 lines per file. If a generated component would exceed, split before writing.
- No hardcoded design values. Colors, spacing, radii, shadows, durations, easings — all from tokens.
- Every interactive element has hover, active, focus-visible, and disabled states.
- All motion respects
prefers-reduced-motion.- Every image / illustration has alt text or
aria-hiddenif decorative.- Dark mode is never an afterthought — emitted alongside light from the start.
- Stack-respect: never introduce a new framework or styling system; adapt to what's there.
Bundled templates
The four templates in spec 6.8. Pick the closest match before writing custom geometry.
| Template | Use |
ambient-particles | Decorative background; instanced points or sprites drifting |
product-showcase | Single hero object, slow auto-orbit, soft env lighting |
hero-gradient-mesh | Animated noise-driven plane; cheap, looks expensive |
scroll-driven-camera | Camera path tied to scroll Y; storytelling sections |
Canvas setup
<Canvas
camera={{ position: [0, 0, 6], fov: 45 }}
dpr={[1, 2]} // cap retina cost
gl={{ antialias: true, powerPreference: "high-performance" }}
frameloop="demand" // render only on change; switch to "always" only when truly animating every frame
>
<Suspense fallback={null}>
<Scene />
</Suspense>
</Canvas>Rules:
dpr={[1, 2]}— uncapped DPR on 3× phones is the #1 perf foot-gun.frameloop="demand"for static scenes; explicitinvalidate()after state change.- Always wrap content in
<Suspense>— drei/textures/gltf throw promises.
Camera
| Scene | Type | fov | Notes |
| Hero / product | Perspective | 35–45 | Lower fov = less distortion, more cinematic |
| Wide environment | Perspective | 60–75 | Standard |
| UI overlays | Orthographic | — | Pixel-perfect, no perspective drift |
Default position: pull back enough that the subject occupies ~60% of frame. Use <PerspectiveCamera makeDefault> from drei when you need to switch cameras at runtime.
drei utilities to prefer
| Utility | Use instead of |
OrbitControls | hand-rolled mouse listeners |
Environment (preset="city" | "studio" | "sunset") | manual HDRIs |
ContactShadows | shadow-map setup for a single ground shadow |
Float | custom sin-wave hover |
Instances + Instance | manual instanced-mesh wiring |
Html | DOM-in-3D overlays |
useGLTF | hand-rolled loader |
Bounds + useBounds | manual fit-to-view |
PerformanceMonitor | manual fps watcher |
AdaptiveDpr, AdaptiveEvents | manual dpr scaling |
Environment preset is the cheapest way to get good lighting; reach for it before adding lights manually.
Performance pitfalls
1. Re-creating geometry/material per frame
Define geometry and materials outside useFrame, or memoize.
const geom = useMemo(() => new THREE.SphereGeometry(1, 32, 32), []);2. Hundreds of meshes vs instanced mesh
Anything > ~50 repeated meshes uses <Instances>:
<Instances limit={1000}>
<sphereGeometry args={[0.05, 8, 8]} />
<meshStandardMaterial color={"var(--color-brand)" /* via uniform; see tokens note */} />
{points.map((p, i) => <Instance key={i} position={p} />)}
</Instances>(Token integration: pass tokens as JS values resolved at runtime via getComputedStyle, or accept a color prop and let the wrapper component read the token.)
3. Sub-tree state changes triggering re-render
Use useFrame for animation, not React state. State only at scene boundaries.
4. Loading hero models on the main bundle
Lazy-load the scene component:
const Scene = dynamic(() => import("./scene"), { ssr: false, loading: () => <Fallback /> });5. Shadow-map cost
shadow-mapSize defaults are heavy. Set [1024, 1024] for hero, [512, 512] for ambient. Or skip dynamic shadows entirely and use ContactShadows.
6. Antialiasing on high-DPR mobile
On phones, prefer antialias: false + <Postprocessing>'s SMAA, or just accept aliasing. Test on a real device.
Mobile-fallback strategy
Three layers, applied in order:
- Reduced-motion: if
prefers-reduced-motion, render a still-image poster (rendered once at build, or<Canvas frameloop="never">after first paint). - Small viewport: below
768px, swap the<Canvas>for the same poster image. - Low-end device: use
<PerformanceMonitor onDecline={…}>to drop DPR (<AdaptiveDpr pixelated />) and disable post-processing when fps < 40.
function HeroScene() {
const reduced = useReducedMotion();
const isSmall = useMediaQuery("(max-width: 768px)");
if (reduced || isSmall) return <HeroPoster />;
return (
<Canvas dpr={[1, 2]} frameloop="demand">
<AdaptiveDpr pixelated />
<Suspense fallback={<HeroPoster />}>
<Scene />
</Suspense>
</Canvas>
);
}The poster is an SVG or static PNG generated alongside the scene — never leave a blank box.
Suspense boundaries
One per resource group. Don't gate the whole canvas on a single boundary if some assets can stream in independently.
<Canvas>
<Suspense fallback={null}>
<Environment preset="studio" />
</Suspense>
<Suspense fallback={<PlaceholderBox />}>
<HeroModel />
</Suspense>
</Canvas>Outside the canvas, wrap the canvas itself in a Suspense for code-splitting:
<Suspense fallback={<HeroPoster />}>
<HeroScene />
</Suspense>Token integration
Three.js takes raw color values; tokens live in CSS. Bridge at the wrapper:
function readToken(name: string) {
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
function HeroScene() {
const brand = useMemo(() => readToken("--color-brand"), []);
return <Canvas>…<meshStandardMaterial color={brand} />…</Canvas>;
}Re-read on theme change (subscribe to a theme context or MutationObserver on data-theme). Never paste a hex into the scene file.
Accessibility
A 3D scene is decorative by default. Wrap the <Canvas> in a div with:
<div role="img" aria-label="Animated hero showing floating product cards">
<Canvas …>…</Canvas>
</div>Or, if the scene is purely ornamental and the surrounding text covers meaning:
<div aria-hidden="true"><Canvas …>…</Canvas></div>Do not put critical content inside <Canvas> — screen readers can't see it. Use drei <Html> for any text that must be announced.
File layout
- Scene component:
components/scenes/<Slug>.tsx— ≤ 300 lines. - Heavy geometry/shaders split into siblings:
<Slug>Mesh.tsx,<Slug>Materials.tsx. - Posters:
public/scenes/<slug>.poster.svg(or.pngif photographic). - Always lazy-loaded by the consuming page (
dynamic(import …, { ssr: false })).
Anti-patterns
<Canvas>without Suspense.- Uncapped DPR on retina (
dpromitted). - Loading
.glbfiles > 2 MB without compression (.glb→ Draco/Meshopt; document the trade). - Listening to scroll/mouse with React state — use refs and
useFrame. - Postprocessing stacks (Bloom + DOF + SSAO) on mobile.
- Animating CSS variables from inside the canvas — bridge in JS, not via DOM thrash.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

