Issues are tracked on GitHub (pmndrs/examples) via the gh CLI; external PRs are a triage surface. See docs/agents/issue-tracker.md.
Canonical vocabulary: needs-triage, needs-info, ready-for-agent, ready-for-human, wontfix. See docs/agents/triage-labels.md.
Closed vocabulary, held as the enum on tags in schemas/pmndrs.schema.json β the same treatment libraries gets, and bin/validate-pmndrs-metadata.mjs reads both lists out of that file. An unknown tag fails pnpm lint:metadata, and every example carries at least one.
Closed because a tag is a filter: the badges on the example page and the cards set ?tag=, so a misspelt tag is a pill that returns nothing rather than a cosmetic slip. The cost is a line in the schema for each new term β paid in the same pull request as the example that needs it, and the editor offers the list while you type, since every pmndrs.json points $schema at that file.
One axis: the technique. Not what the scene depicts (arkanoid, minecraft, a brand) β the title and the description already say that β and not the API that implements it (MeshReflectorMaterial, useAnimations), which the source does. reflections, not meshreflectormaterial.
Form is kebab-case and lower-case. Between a singular and a plural, the form the catalog already carries more often wins, and a tie goes to the singular β which is why it is shader and animation but reflections and particles.
Single-context: one CONTEXT.md + docs/adr/ at the repo root (created lazily by /domain-modeling). See docs/agents/domain.md.
apps/website has Tailwind v4 + shadcn/ui set up (style base-maia β the Base UI half of the registry, so the primitives are @base-ui/react and composition is the render prop, not asChild β base colour neutral, icons lucide). The vendored shadcn skill in .claude/skills/shadcn/ is the source of truth for adding, updating and styling components β use it, and run the shadcn CLI rather than hand-writing registry files.
Components come from the CLI, never from a fetch. pnpm dlx shadcn@latest add <component> (docs) β it resolves the registry for our style/baseColor/iconLibrary, pulls transitive components, and writes to the aliases in components.json. Never copy a component out of the docs site, curl a registry JSON, or hand-write a file into components/ui/: those bypass the preset and drift from what shadcn@latest info reports as installed. Reading the docs for a component's API is fine β installing from them is not.
components/ui/* is vendored, not ours β never edit it, and never delete one either. Those files must stay what the registry emits (modulo prettier, which the repo runs over everything), so that shadcn@latest add <component> --overwrite is always a safe no-op and any of them can be swapped for the stock version tomorrow. If a component doesn't do what you need, the fix goes at the call site β className for layout, the built-in variant/size props for looks, composition (wrap it, or hand it a render element) for behaviour β or into the theme tokens in app/globals.css. Never into the component file. If you genuinely cannot express it from outside, write your own component next to it under components/ rather than forking the vendored one.
Two corollaries, both learned the hard way while moving the registry from Radix to Base UI:
- A prop the wrapper doesn't forward is not a reason to add it. The registry picks a subset of the primitive's props on purpose. Drop the call that needed the extra prop and take the component's default instead β a positioner default that differs by 7px is not worth a file that
--overwritewill silently revert. - "Replace it with our own markup" is still touching it. Swapping a vendored component out of a call site because its Base UI behaviour differs from the Radix one β and deleting the file once nothing imports it β leaves the same hole: the next
addbrings back a component nobody uses, and the behaviour that was tuned lives in hand-rolled markup instead. Reach for the component's own data attributes from the call site first (data-hovering,data-scrolling,data-open, β¦); they are the API for exactly this.
The colour tokens are Material Design 3. Every shadcn token in app/globals.css reads an --md-sys-color-* role, and material-theme-builder derives all of them from one source hex. Retuning the palette means changing MCU_SOURCE in app/layout.tsx β or the scheme / contrast / core-colour overrides in the builder() call next to it β never editing a token by hand. The site runs scheme: "monochrome", which derives every role from the source's tone alone and discards its hue: the chrome is greyscale on purpose, so that the only colour on a example page is the example. MCU_SOURCE still matters β swap the scheme and the mint comes straight back. Anything the m3 roles don't cover belongs in that call's customColors, which mints --md-sys-color-<name> and a matching -on- foreground.
It has to stay a build-time call. layout.tsx is a server component, so builder(...).toCss() runs once at build and the CSS ships inside the prerendered HTML. Don't move it into a client component, and don't reach for the package's <Mcu>: this app is output: "export", so the browser paints the whole page well before hydration, and anything that supplies the colours later gives you a frame with none of them. Verify a change here by grepping the built out/index.html for --md-sys-color-surface: β not just by looking at the running app, where hydration hides the gap.
Two things to know before styling:
- The whole app is on Tailwind. There is no
<Style>component and no@scopeblock left anywhere inapps/websiteβ every rule is a utility at its call site, acomponents/ui/*variant, or a token inapp/globals.css. Don't reintroduce injected<style>: it lands unlayered, so it outranks Preflight and every utility, and a rule that always wins is a rule nobody can override from a call site. The two things utilities can't express β the source-of-truth palette, and Preflight itself β already have homes inglobals.css. - Sources are declared explicitly. Tailwind's automatic source detection finds nothing in this app, so
app/globals.csslists@sourceentries. Add one if you put components somewhere new. - A repeated group of controls is one tab stop, not N.
hooks/use-roving-tabindex.tsgives the example list and the example bar a roving tabindex β arrows move within the group, Tab moves past it. The whole site is six tab stops; if you add a control, check it is not a seventh hiding inside one of those groups. Hand the hook the container's ref and spread what it returns.
examples/ is deliberately Tailwind-free; don't introduce it there.