Slot data

View as Markdown
Last updated September 14, 2026

A slot prop is a placeholder an author fills by dropping components into it (see Section Slots). By default a slot renders whatever was dropped: the dropped components get no data from the component that owns the slot.

<Slot> fixes that. Wrap a slot's value in <Slot data={...}> and the components an author drops inside can bind that data through the component_props source, with the nearest slot winning over data provided higher up.

When to use it

Reach for <Slot data={...}> whenever both of these are true:

  1. A component exposes a slot, a spot you want authors to fill with whatever component fits, not one you hard-wire.
  2. The owning component holds data that the dropped component needs.

That combination shows up in two ways, and the reasoning is the same for both:

  • Designing fresh. You're building a component and you already know one spot should be an open, reusable slot: you can't predict what authors will drop there, but you know it'll need data the component holds.
  • Opening up an existing spot. A component had a fixed child that received props directly. You make it a slot so authors can swap what goes there. The data that child received still has to reach whatever replaces it.

Either way, the moment a spot is a slot, props can't reach it. The component doesn't know what the author will drop. <Slot data={...}> bridges that: it carries the owner's data to whatever fills the slot, where the dropped component can bind it.

Example: a Hero banner with a CTA. The banner is meant to be reusable: authors drop any CTA into it, a button, a link, or a promo card. But the banner owns ctaDestination (where the CTA should link), and the dropped component needs it. Since the CTA is a slot, the banner can't pass it as a prop, so it carries it through the slot:

// The banner owns `ctaDestination`; the slot carries it to whatever CTA is dropped.
<Slot data={{ destination: ctaDestination }}>{ctaSlot}</Slot>

The rule in one line: when a slot needs data the owner holds, wrap the slot in <Slot data={...}>.

Click to enlarge

The props

interface SlotProps {
  data?: Record<string, unknown>;   // values exposed to the slot's children as component_props
  children: React.ReactNode;        // the slot value (the ReactNode for the slot prop)
}

<Slot> is render-time only: it lives in your component code, nothing is stored in the composition. It's opt-in: rendering the bare slot value ({ctaSlot}) still works, it carries no data.

For the full type-level reference, see <Slot> reference.

How it works

<Slot data={...}> merges data into the component_props data source for everything rendered inside it, a shallow merge over any component_props inherited from further up. Each top-level key of data becomes a bindable field.

In the canvas, an author selects a component dropped into the slot, opens the binding chip on a prop, and the Data Picker's Component Default Data source lists the slot's keys (grouped under a Component Props node). They bind the prop to the slot's destination, and at render time the dropped component receives the banner's ctaDestination.

Click to enlarge

The picker lists Destination because the Hero wrapped its slot in <Slot data={{ destination: ctaDestination }}>. Without the wrapper the same source reads "No data available for selected data source".

The dropped component must sit directly in the component's slot prop. Slot keys reach the picker when the author drops onto the slot-typed prop itself. Put a Section Slot between the component's slot prop and the dropped component (the section-level pattern) and the Component source comes back empty: the render-time merge still happens, but the editor never receives the keys, so there is nothing to bind to.

Nearest slot wins

data merges over component_props inherited from outside the slot, the same nearest-wins model as Repeater context. If a key exists both in the global data (the data prop on <StudioComponent />) and in a slot's data, the slot's value wins for children inside that slot. Nested slots stack: the deepest (nearest) slot overrides the ones above it.

Worked example: Hero banner

A banner with a headline and a CTA slot. The banner owns the CTA's destination. The slot carries it to whatever the author drops in.

import { Slot, type SlotProps, type StudioAttributes } from "@contentstack/studio-react";

interface HeroBannerProps extends StudioAttributes {
  headline?: string;
  ctaDestination?: string;   // where the CTA should link — the banner owns this
  ctaSlot?: SlotProps["children"];
}

export function HeroBanner(props: HeroBannerProps) {
  const { headline, ctaDestination, ctaSlot } = props;
  return (
    <section className="hero">
      <h1>{headline}</h1>

      {/* `ctaSlot` is an open slot — authors drop any CTA. The banner owns
          `ctaDestination`, so it carries it through the slot to whatever they drop. */}
      <Slot data={{ destination: ctaDestination }}>{ctaSlot}</Slot>
    </section>
  );
}

Register the component with ctaSlot as a slot prop (see Component schema). An author drops any CTA component into ctaSlot, binds its link to the slot's destination (under Component Default Data), and it renders the banner's ctaDestination, even though the banner never passes props to that component directly.

Common pitfalls

PitfallSymptomFix
Rendering the bare slot valueDropped components can't bind the owner's dataWrap the slot value in <Slot data={...}>
A Section Slot sits between the component's slot prop and the dropped componentThe picker's Component source is empty: keys are never reported to the editorDrop onto the component's slot prop directly. Slot data does not carry through a Section Slot
Expecting a key to appear without selecting a childComponent Default Data source looks emptySelect a component inside the slot first. The keys are scoped to the slot
Same key in global data and slot dataConfusion over which value rendersNearest wins: the slot's value applies to children inside it
Passing non-serializable values in dataBinding resolves oddlyKeep data plain: strings, numbers, arrays, objects
Binding via the wrong sourceField not foundBind through the Component Default Data source, not a CMS or external-data source

See also