Studio Docs

When to Use

Decide the SHAPE of a Studio component library (which atomic and layout primitives to register) BEFORE any registerComponent call is written. Applies the atom + layout taxonomy from docs/getting-started/composable-primitives.md to the user’s design system so authors compose sections on the fly without visual drift.

Use when the user says “which components should I register”, “design our Studio library”, “what should the component library look like”, “we’re starting fresh, what do we register”, “atomic + layout primitives for Studio”, or when someone brings a design system (Tailwind + shadcn, Chakra, MUI, a custom kit) and asks how it maps to Studio. Do NOT use to write registration calls for one component (use register-component). Do NOT use to break one over-large existing component into pieces (use decompose-jsx-to-atomics). Do NOT use for per-page decomposition of a specific design (use decompose-design).

Auth preflight: settle the credential before the first API call. Resolve it OAuth-first per authenticate-cma: CS_OAUTH_ACCESS_TOKEN, else the Contentstack MCP’s stored session. Never ask the user for a session authtoken. If nothing resolves, or a refresh fails with 400 invalid_refresh_token, hand them ! CONTENTSTACK_REGION=<code> npx @contentstack/mcp --auth (it needs a TTY and a browser, so it cannot be run for them) and wait. 403 error_code 316 is a valid credential aimed at another org: fix the org or the api_key, do not re-authenticate.

Design a Studio Component Library

Context

The trap is registering either (a) one giant Hero with 20 props, or (b) ten differently-shaped Heros. Both fragment the design system: (a) hides variety behind a monolith authors can’t extend without engineering. (b) lets every Hero pick its own spacing and type scale. The alternative is a small set of atomic and layout primitives, the 10-primitive pattern in docs/getting-started/composable-primitives.md.

The Four-Question Decision Framework

This skill walks the four-question framework from the pattern doc. The framework is an open validator. It works on any input (React component, Figma frame, verbal description) regardless of what’s already registered:

  • Q1. Classify: Atom, Layout, or Compound? Look at the input in isolation. A component that carries a single content unit is an Atom. One that arranges children via slots is a Layout. One that does both content AND arrangement is a Compound (refuse and decompose recursively). This is a validator question. It tells you what shape the component actually is.
  • Q2. Does the library already have it? Only after Q1 confirms atom or layout, check reuse. On a fresh project the answer is trivially “no” for the first primitives. In an existing library, check the registry. With legacy monoliths, check the atom/layout registry, not the monolith list.
  • Q3. Which props? For each proposed prop, does it vary per-instance AND represent functional/content intent (not visual polish)?
  • Q4. Which are Exposed? For each schema prop, is it author-understandable AND DS-portable?

Halt if any answer is ambiguous. Never guess. Ask the user to disambiguate before proceeding.

This skill’s job is to produce that primitive list for the user’s specific design system BEFORE any registration is written. Get it right up front and the registration work that follows is mechanical.

The Rule of Thumb: Expose Only When the Value

  1. Varies per instance, AND
  2. Represents functional or content intent, not visual polish.

Concretely:

  • Good: level: h1 | h2 | h3 on Heading (semantic hierarchy, so DS tokens set size and weight)
  • Good: variant: default | secondary | ghost on Button (intent maps to color + border tokens)
  • Good: text on Heading, href on Button, src on Image (content)
  • Avoid: size: sm | md | lg | xl on Heading (level already decided the size)
  • Avoid: color: hex on anything (drift risk: use emphasis: default | muted | inverse instead)
  • Avoid: borderRadius, fontWeight, letterSpacing, boxShadow, padding: 32 (design-system concerns)

Task

Walk the user through a five-step decomposition of their design system into a primitive list ready to hand to register-component.

Step 1: Confirm the design system

Ask (only if not stated):

  • Which UI library are you on? (Tailwind + shadcn/ui, Chakra, MUI, custom Radix, custom tokens)
  • Where are your semantic tokens defined? (CSS variables, Tailwind config, theme file)
  • Does the DS already expose semantic variant names (primary / secondary / default) or raw color tokens?

Do NOT invent tokens. Every semantic option in a primitive’s schema must map to an existing DS token. If a token doesn’t exist for a proposed option, flag it and offer to defer that primitive until the DS adds the token.

Step 1b. The split that makes a library reusable: content binds, design stays static

Measured on a 58-section production Studio site: 300 bound props vs 1803 static. 86% of every prop set is design, not content.

An atom therefore has few content props and many design props, and they are wired differently:

Content propsDesign props
Examplestext, title, href, media, altsize, color, align, variant, tag, gap, padding_*
Wired asbound to a CT fieldstatic value set on the canvas
Who decidesthe author, per entrythe composer, per placement
Count per atom1 to 34 to 13

Real signatures from that site:

heading      content: text                         design: tag, color, align, variant, size
body-string  content: text                         design: size, color, align, variant
cta-button   content: title, href, open_in_new_tab design: style, icon_position, full_width, align, variant
media        content: media, alt                   design: fill, aspect_ratio, fit

Never model a design prop as a CT field. gap, align and variant as content fields produce a schema an author cannot understand and a section that drifts visually per entry. If it changes how the thing looks rather than what it says, it is a static value.

The corollary is what makes the library reusable: the same four atoms compose every section, differing only in their static design values. heading is used 23× across that site, cta-button 33×, stack 66×, not one variant per section.

Step 2: List the four content atoms

Every marketing surface needs the same four content-only atoms:

AtomPropsNotes
Headingtext, level: h1..h4, `emphasis: defaultinverse`
Descriptiontext, `emphasis: defaultmuted
Buttonlabel, href, variant, size, iconVariants + sizes mirror shadcn’s cva (or the DS’s equivalent) exactly.
Imagesrc, alt, `aspect: auto1:1

For each atom, confirm the DS has tokens for every semantic option. If the DS uses different variant names (e.g. emphasis values named primary/secondary/accent on Chakra), keep the DS’s names. The schema should read like the DS.

Body copy needs one atom per rich-text shape, not one Description. The production reference registers four, because the render path differs per storage format. See register-json-rte:

AtomForProp type
body-stringplain single-paragraph copyplaintext
body-multilineplain multi-paragraph copyplaintext
body-json-rteJSON RTEjson_rte
body-html-rteHTML RTE / Markdownjson_rte (or any on an SDK without the widened binding)

Add these atoms when the designs call for them: each is a real atom on the reference site, and each is one an agent otherwise inlines into a bigger component: eyebrow (kicker above a heading), icon, breadcrumb, cta-row (a group of buttons), numbered-item / step-number (ordered lists), share-buttons, form-embed, and shape / accents (decorative, no content props at all).

Step 3: List the six layout primitives

LayoutPropsSlotsNotes
Sectionspacing: compact/comfortable/spacious, background: none/surface/muted/brand/inverse, contentAlignchildrenOuter padded wrapper. Spacing is semantic, not px.
BackgroundMediaimage, overlay: none/subtle/strong, focalPoint, height: standard/tall/fullchildrenBackground image behind foreground content.
SplitRow`ratio: 50-5060-4040-60
Stackspacing: tight/normal/loose, alignmentchildrenVertical grouping, the workhorse.
Grid`columns: 234, spacing`
Cardvariant: default/outline/ghost, paddingheader, content, footerCompound container, mirrors shadcn Card.

Two more the production reference relies on, both easy to omit and expensive to retrofit:

LayoutPropsSlotsWhy it earns a place
BoxnonechildrenA bare slot wrapper, 49 uses on the reference site, second only to Stack. It gives a grouping seam with no styling opinion, so a slot can be filled without inheriting spacing. Without it, composers reach for Stack and inherit gap they then have to cancel.
Columns (named slots)split, align, gap, stack_on_mobileleft, center, rightDistinct from Grid: named slots let a composer target a specific column, and let a template author swap one side only. Grid’s single children slot cannot express that.

Stack carries the spacing vocabulary. On the reference site: direction, gap, align, align_mobile, justify, max_width, margin_x/y, padding_x/y, width, height. Every one is static. It is used 66×. Treat it as the workhorse and give it the full vocabulary up front, because adding a spacing prop later means editing every placement.

Name by layer, with one prefix. The reference uses alpha- throughout: alpha-atom-heading, alpha-stack, alpha-section-shell, alpha-card-shell. A 60-primitive palette is unnavigable otherwise. The prefix groups them, and -atom- marks the layer at a glance. Pick any prefix. Apply it to every primitive.

Group them in the palette too: the name is not enough. Each registration declares the group it appears under, and the reference uses exactly two:

const ATOMS_GROUP  = 'Alpha Atoms';
const LAYOUT_GROUP = 'Alpha Layout';
// …
{ type: 'alpha-atom-eyebrow', sections: [ATOMS_GROUP], thumbnailUrl: T.text, … }

Decide the group names here, at library-design time. sections + thumbnailUrl mechanics belong to register-component § Palette group + thumbnail.

Design props are choice with fixed options: this is what enforces the split

Step 1b says design stays static. choice is the mechanism that makes it stick. Every design prop on the reference declares a closed option list and a default, so an author picks from a menu and cannot type a raw value:

props: {
  text:        { type: 'string', defaultValue: 'New Feature' },              // content
  color:       { type: 'choice', options: ['brand','default','inverse','muted'], defaultValue: 'brand' },
  variant:     { type: 'choice', options: ['text','badge'],  defaultValue: 'badge' },
  badge_color: { type: 'choice', options: ['green'],         defaultValue: 'green' },
}

One content prop, three design props, closed options on each. A string design prop (color: '#6c5ce7', gap: '24px') re-opens exactly the drift the library exists to prevent.

Give every prop a defaultValue. It is what lets a composition omit design props entirely and still render. The authored section then carries only its content bindings, and the primitive supplies the rest.

Confirm the DS has:

  • Padding tokens for spacing: compact/comfortable/spacious at both Section and Card levels.
  • Background theme tokens for background: none/surface/muted/brand/inverse.
  • Gap tokens for Stack.spacing and Grid.spacing.
  • Card border/shadow tokens for Card.variant.
  • Overlay opacity tokens for BackgroundMedia.overlay.
  • Min-height tokens for BackgroundMedia.height.

If any is missing, list it as a DS gap. The primitive can still register but the token must land before the option is enabled.

Step 4: Sanity-check against target section types

Ask which section types the user needs from day one. Common set:

  • Hero: needs Section + BackgroundMedia + SplitRow + Stack + all 4 atoms
  • Feature Grid: needs Section + Stack + Grid + Card + all 4 atoms
  • Testimonial row: needs Section + Grid + Card + SplitRow + Stack + all 4 atoms
  • Pricing tiles: needs Section + Stack + Grid + Card + all 4 atoms (+ a Condition Block for tier highlighting)
  • CTA band: needs Section + Stack + all 4 atoms
  • Logo strip: needs Section + Grid + Image

Confirm every section on the user’s list can be built from the 10-primitive set. If any needs an atom or layout not on the list (e.g. a Rating atom for testimonials, a Tabs layout for a comparison table), note it as a future addition. Do NOT add it to the initial registration. Grow the library additively.

Step 5: Emit the decomposition sheet

Give the user a plain-text summary they can paste into a PR description:

Studio component library — initial 10 primitives

ATOMS (content-only)
  Heading      · props: text, level, emphasis
  Description  · props: text, emphasis
  Button       · props: label, href, variant, size, icon    (mirrors shadcn Button)
  Image        · props: src, alt, aspect, fit

LAYOUTS (composition)
  Section          · props: spacing, background, contentAlign, children
  BackgroundMedia  · props: image, imageAlt, overlay, focalPoint, height, children
  SplitRow         · props: ratio, verticalAlign, reverseOnMobile, leftSlot, rightSlot
  Stack            · props: spacing, alignment, children
  Grid             · props: columns, spacing, children
  Card             · props: variant, padding, header, content, footer    (mirrors shadcn Card compound)

DESIGN-SYSTEM TOKEN CHECK
  All semantic options map to existing DS tokens — no gaps
  OR
  Gaps: <list — token that must land before shipping the primitive>

TARGET SECTION TYPES COVERED
  Hero (5 variants) · Feature Grid · Testimonial · Pricing · CTA band · Logo strip

NOT INCLUDED (deferred, add additively)
  <list any section types that would need a new atom or layout>

NEXT STEP
  Run register-component for each of the 10 primitives, in one PR.

Inputs Needed From the User

  • Design system name: Tailwind + shadcn / Chakra / MUI / custom
  • Where tokens live: CSS vars / Tailwind config / theme file
  • Target section types for day one: Hero, Feature Grid, Testimonial, Pricing, other

If any is missing, ask before decomposing. Do NOT invent DS tokens or section types.

Acceptance

This skill succeeds only when ALL of the following are true:

  • Every atomic component carries its CSLP tags: studioAttributes spread on the root element and wrap: false in its register entry and each bindable prop’s $-twin spread on the element rendering it. Verified in the DOM, not assumed: the root and each bound text/image element must show data-cslp. Without this the atom renders correctly but is invisible to Visual Editor and Live Preview. See register-component § The studioAttributes contract and § The $-twin contract.

  • User confirms the design system + where tokens live.

  • All 10 primitives listed with their prop schemas.

  • Every semantic option is either mapped to a DS token or explicitly flagged as a gap.

  • User’s target section list is confirmed buildable from the 10 primitives OR future additions are flagged.

  • Decomposition sheet emitted for the PR description.

If the user asks for a primitive that doesn’t fit the atom/layout taxonomy (e.g. “register a Hero”), stop and explain the rule of thumb. Do not silently register the monolith.

What “done” Looks Like: a Production Reference

Measured on a live 58-section Studio site (contentstack.com 2025), for calibration:

SignalValueReads as
Sections58one per marketing pattern, not per page
Nodes per section9.1 avg, depth 3.8genuinely decomposed, a 1-node section means the component swallowed the page
Props static vs bound86% / 14%design lives on the canvas, content in the CT
Reuse of the top atomcta-button 33×, heading 23×, stack 66×the same primitives everywhere, varied by static values
Studio primitives32 condition blocks, 17 section slots, 15 repeaterspolymorphism and swap points are used, not avoided

If a proposed library would produce sections averaging 2 to 3 nodes, or would bind design props to CT fields, it is not decomposed enough. Go back to Step 1b.

Common Pitfalls

PitfallWhy it bitesFix
Modeling a design prop (gap, align, variant, size) as a CT fieldProduces a schema authors can’t reason about, and lets a section drift visually per entry. 86% of props on the production reference are static for this reasonIf it changes how the thing looks rather than what it says, it is a static value set on the canvas
One Description atom for all body copyThe four rich-text shapes render through different paths. A single plain-text atom shows JSON RTE or Markdown as literal charactersRegister body-string / body-multiline / body-json-rte / body-html-rte per register-json-rte
Skipping a bare Box because Stack “already wraps things”Stack carries spacing, so every grouping inherits gap the composer must then cancel. The reference uses Box 49×Register a props-free slot wrapper alongside Stack
Unprefixed primitive namesA 60-entry palette with no grouping is unnavigable in the component pickerOne prefix for every primitive, with the layer in the name (x-atom-heading, x-stack)
Exposing size on HeadingLayout drift: every H1 picks its own sizelevel drives size via DS tokens. size never appears
color: hex on any atomColor drift across the libraryemphasis: default/muted/inverse maps to DS tokens
Padding in pixels on SectionSpacing scale fragmentsspacing: compact/comfortable/spacious maps to Tailwind padding tokens
Registering Hero as an atomMonolithic Section: new variant needs a code changeRegister the 4 atoms + 4 layouts. Compose Heros on canvas
A Rating atom before you need itLibrary bloat, unused primitivesGrow additively. Add only when a section actually needs it
Semantic option without a DS tokenSilent visual failure at renderAdd the token before enabling the option in the schema

See Also

  • docs/getting-started/composable-primitives.md: foundational pattern doc.
  • register-component: the mechanical registration once the shape is decided.
  • decompose-jsx-to-atomics: for breaking one existing over-large component into its parts.
  • decompose-design: for taking a single design and mapping it to the primitive set + a Content Type shape.
  • import-design-tokens: get the DS tokens into Studio’s Design Panel before registration.