
sanity-plugin-best-practices
★ 14by sanity-io · part of sanity-io/plugins
Anti-patterns and best practices for building Sanity Studio plugins in this monorepo. Use when writing, reviewing, or refactoring plugin code under plugins/ — especially for styling/CSS, component performance, and runtime cost. Triggers on vanilla-extract, raw <style> tags, styled-components, theming, inline styles, or questions about how plugins should be structured here.
This is the playbook your agent receives when the skill activates — you don't need to read it to use the skill, but it's here to audit before installing.
Sanity Plugin Best Practices
Conventions for authoring plugins in the sanity-io/plugins monorepo, captured as concrete
anti-patterns and the preferred approach. These rules encode what the existing plugins already do
so new and migrated code stays consistent, fast, and theme-aware.
This complements the broader React performance guide in the
vercel-react-best-practices skill — read that for
general React/Next.js patterns, and this skill for plugin-specific guidance.
When to Use This Skill
Use this skill when you are:
- Writing or reviewing a plugin component under
plugins/(.ts/.tsx) or a test-studio example. - Adding or changing styling: authoring
.css.ts(vanilla-extract), reaching for<style>tags, inlinestyle={{}}, CSS files, orstyled-components. - Migrating an external plugin into the monorepo (pair with the
plugin-transferskill). - Investigating runtime cost, re-renders, or styling that does not respect the Studio theme.
- Deciding how a plugin should be structured to match the rest of the repo.
Styling: the one-line rule
vanilla-extract is the styling solution for everything you author —
in new plugins and existing ones. It compiles to a static stylesheet (zero per-render and
per-instance cost), is type-safe and scoped, and lives in a .css.ts next to the component:
- vanilla-extract
style()— static styles (the common case). - vanilla-extract
createVar()+assignInlineVars()— dynamic styles that read the live@sanity/uitheme (via theuseTheme_v2()hook) or vary with props/state. - Import a third-party
.cssonce at module scope — for prebuilt stylesheets you don't author (katex, easymde, …). - Never render raw
<style>tags (in JSX, viadangerouslySetInnerHTML, by appending a<style>/<link>todocument.head, or viauseInsertionEffect). They re-parse on every render, duplicate per instance, bypass the theme, and are an injection risk. CSS-in-JS is a last resort: only when the CSS is genuinely dynamic (from runtime data, able to be any CSS) reach forstyled-components— the most performant CSS-in-JS — and we try hard never to need it.
styled-components is legacy — the Studio's old styling library, still in some plugins. Migrate
existing styling usage to vanilla-extract (carefully, preserving visual fidelity; not during a
transfer's initial port). Its only remaining sanctioned use is the last-resort CSS-in-JS escape hatch
in rule 4 (arbitrary CSS from runtime data). See
references/styling.md.
See references/styling.md for the full rationale and examples.
General principles
- Build on
@sanity/ui. Compose Studio primitives (Box,Card,Flex,Stack,Text,Button, …) and read design tokens with theuseTheme_v2()hook (color...,space[...],radius[...],font...) instead of hardcoding colors, spacing, or fonts. - Style with the priority above: vanilla-extract for all new styling —
style()for static,createVar()+assignInlineVars()(from@vanilla-extract/dynamic) for dynamic; never raw<style>tags. - Encapsulate styles in components; don't scatter
className. Wrap each vanilla-extract class in a small component that spreads props onto the@sanity/uiprimitive, keeping the same export name and API as the oldstyled()so call sites are unchanged — composition stays as clean as before. To override a primitive's own styles, use theselectors: {'&&': {...}}specificity trick (styled-components guaranteed override ordering in the CSSOM; vanilla-extract does not). Seereferences/styling.md. styled-componentsis legacy — migrate it to vanilla-extract. Never add it for ordinary styling (its only sanctioned use is the last-resort CSS-in-JS escape hatch for arbitrary runtime CSS); convert existing styling usage to vanilla-extract (carefully, preserving visual fidelity) using the patterns above. Until a plugin is fully migrated, keep itsstyled-componentspeer +catalog:devDependency aligned so it resolves to the workspace@sanity/styled-componentsoverride; remove them once migrated, and lock it in withno-restricted-imports. Seereferences/styling.md.- Use
lodash-es, neverlodash(matchesAGENTS.md). - Never use
forwardRef. On React 19refis a regular prop — declareref?: Ref<T>in your props and use it directly. The lint config bans theforwardRefimport. Seereferences/refs.md. - Apply the React performance rules from
vercel-react-best-practices(don't define components inside components, batch DOM/CSS writes, hoist static JSX, etc.).
npx skills add https://github.com/sanity-io/plugins --skill sanity-plugin-best-practicesRun this in your project — your agent picks the skill up automatically.
How to Use
Read the reference file for the area you are working in. Each reference lists the anti-pattern, the
preferred approach, and Incorrect / Correct examples grounded in real plugins in this repo.
| Area | What it covers | Reference |
|---|---|---|
| Styling and CSS | Use vanilla-extract (static + dynamic) for all plugin styling; encapsulate classes in components; migrate styled-components to vanilla-extract | references/styling.md |
| Component refs | Don't use forwardRef — on React 19 ref is a regular prop (enforced by lint) | references/refs.md |
This skill is intended to grow. When you find a plugin pattern worth standardizing (or an anti-pattern worth banning), add a focused reference file and a row to the table above rather than bloating this entry point.
No common issues documented yet. If you hit a problem, the repository's GitHub Issues page is the best place to look.
Licensed under MIT— you can use, modify, and redistribute it under that license's terms.
View the full license file on GitHub →