Studio Docs

Compose Pricing Tiles From Primitives

A three-tier pricing table (tier name, price, feature list, CTA per tile) built from the same primitives as Heroes, Feature grids, and Testimonials. The middle tier gets a highlighted treatment via a different Card variant.

Prerequisite reading. Design a component library that composes, not sprawls.

Do It With a Skill

compose-marketing-section assembles this from registered primitives; build-repeating-section handles the tile list.

curl -fsSL https://studio-documentation.contentstackapps.com/install.sh | sh

What You’ll Build

A Pricing section: centered H2 heading and muted intro description at the top, followed by a 3-column grid of Cards. Left and right Cards use the outline variant, the middle Card uses the default variant to highlight it as the popular tier. Each Card's header has a tier label, a large price heading, and a period description. The content slot has a stack of checkmark features. The footer has a Button. Outline variants use outline buttons, the highlighted middle uses a default button.

  • Section intro (heading + description, centered)
  • 3-column Grid (Repeater over pricing_tiers)

New to binding inside a Repeater? See the Data Picker + Repeater walkthrough in the Feature Grid recipe, same flow. Inside the Condition Block, the picker scope is the current iteration’s pricing_tier entry.

  • Per-tier Card with tier label + price + period (header), feature list (content), CTA (footer)
  • The “most popular” tier styled distinctly via Card.variant: default. Others variant: outline

Prerequisite: the Content Type Shape

landing_page: with:

Field UIDTypeNotes
pricing_section_titleSingle-line textSection heading
pricing_section_descriptionMulti-line textSection intro description
pricing_tiersReference (multi) to pricing_tierThe list the Grid repeats over

pricing_tier: one entry per tier:

Field UIDTypeNotes
tier_nameSingle-line texte.g. “Starter”, “Pro”, “Enterprise”
tier_taglineSingle-line textShort qualifier: e.g. “Most Popular”, “Free forever”
priceSingle-line texte.g. “$0”, “$29”, “Custom”
periodSingle-line texte.g. “Free forever”, “per user / month”, “Tailored to your team”
featuresList of strings (multiple)Feature bullets
cta_labelSingle-line textButton label
cta_urlLinkButton target
is_highlightedBooleanDrives the Card variant + Button variant per tier

Composition Tree

Section (spacing: spacious, background: none, contentAlign: center)
└── Stack (spacing: loose, alignment: center)
    ├── Stack (spacing: normal, alignment: center)                       ← section intro
    │   ├── Heading (text: "Simple, transparent pricing", level: h2)
    │   └── Description (text: "Pick the tier that fits your team.", emphasis: muted)
    └── Grid (columns: 3, spacing: normal)                                ← Repeater over `pricing_tiers`
        └── Card (variant: <default | outline>, padding: loose)           ← per-tier iteration
            ├── header:
            │   └── Stack (spacing: tight, alignment: left)
            │       ├── Description (text: <tier_tagline>, emphasis: muted | default)
            │       ├── Heading (text: <price>, level: h2)
            │       └── Description (text: <period>, emphasis: muted)
            ├── content:
            │   └── Stack (spacing: tight, alignment: left)               ← Repeater over `features`
            │       └── Description (text: "✓ <feature>", emphasis: default)
            └── footer:
                └── Button (label: <cta_label>, variant: <default | outline>, size: default)

Note. The feature list uses Stack of Description today. If pricing sections become a common pattern across the library, add a dedicated FeatureList atom with items: array<string> + checkmark: boolean. That’s an incremental addition, not a re-registration.

Bindings

Section-level (static):

  • Section.spacing: spacious
  • Grid.columns: 3
  • Card.padding: loose

Repeated per pricing tier (the Grid becomes a Repeater over landing_page.pricing_tiers):

  • Card.variant is Condition-bound: default when pricing_tier.is_highlighted is true, else outline
  • Header Description (tagline) binds to pricing_tier.tier_tagline
  • Header Heading (price) binds to pricing_tier.price
  • Header Description (period) binds to pricing_tier.period
  • Content: inner Stack Repeats over pricing_tier.features, and per-feature Description.text binds to the feature value
  • Footer Button.label binds to pricing_tier.cta_label
  • Footer Button.href binds to pricing_tier.cta_url
  • Footer Button.variant is Condition-bound: default when highlighted, else outline

The highlighted-tier logic is a Condition Block wrapper in Studio, keyed on pricing_tier.is_highlighted. Two branches: highlighted (default variants), non-highlighted (outline variants).

Varieties From the Same Tree

  • 4-tier pricing: Grid.columns: 4 for Freelance / Team / Business / Enterprise ladders.
  • 2-tier pricing: Grid.columns: 2 for Personal vs Business.
  • Compact tile without feature list: omit the content slot’s inner Stack. Just tier + price + CTA.
  • Ghost tiles on a brand background: Card.variant: ghost + Section.background: brand for a “picked from the catalog” feel.
  • Toggle-based billing period: add a Section-level Exposed Prop billingPeriod: monthly | annual. Content bindings resolve to different price and period fields.

Which Layout Props to Expose

  • Good Exposed Props: Grid.columns (2 / 3 / 4 tier ladders), Section.background.
  • Static per Section: Card.padding, atom props.
  • Condition-bound: Card.variant + Button.variant based on the tier’s is_highlighted boolean.

Building This in Studio: Step by Step

  1. Create the pricing_tier Content Type with all fields listed above.
  2. Add a pricing_tiers Reference-multi field to landing_page.
  3. Create 3 pricing tier entries: Starter (highlighted: false), Pro (highlighted: true), Enterprise (highlighted: false).
  4. Open the Sections palette, Create Section, name it Pricing Tiles — 3-tier.
  5. Set the linked schema to landing_page.
  6. Drag Section: spacing: spacious, contentAlign: center.
  7. Drag outer Stack: spacing: loose, alignment: center.
  8. Build the intro: inner Stack + Heading (level: h2, bind to pricing_section_title) + Description (muted, bind to pricing_section_description).
  9. Drop Grid below the intro: columns: 3. Bind list to landing_page.pricing_tiers.
  10. Drop a Condition Block into the Grid iteration, keyed on pricing_tier.is_highlighted.
  11. In the true branch: drop Card (variant: default, padding: loose). Build header + content + footer.
  12. In the false branch: drop Card (variant: outline, padding: loose). Mirror the header + content + footer, with Button variant: outline.
  13. In each Card’s header: Stack + Description (tagline, bind to tier_tagline) + Heading (level: h2, bind to price) + Description (muted, bind to period).
  14. In each Card’s content: drop a Stack (spacing: tight, alignment: left), bind its list to pricing_tier.features (this makes the inner Stack a Repeater), then add a Description (bind to the feature string value, prefixed with "✓ ").
  15. In each Card’s footer: Button, bind label + href, variant matches the branch.
  16. Save the Section. Preview: three tiles render, middle one highlighted.

Testing

  1. Fill a landing_page entry with pricing_section_title, pricing_section_description, and 3 pricing_tier references.
  2. Drop Pricing Tiles — 3-tier on a Template.
  3. Verify: three tiles render, Pro (highlighted: true) uses default Card + default Button. Starter and Enterprise use outline variants.
  4. Toggle is_highlighted on another tier and republish. Highlighting moves without a schema change.