Skip to main content
Solar’s slot system makes component composition a contract, not a convention. When a parent component declares a slot prop, it can specify exactly which component’s output is allowed to fill that slot. Pass a raw createElement vnode, the wrong component, or a plain value — Solar throws a ContractError with a structured message that names the required component. This means composition mistakes surface immediately at call time, not silently at render time.

Defining a slot prop

Use type: 'slot' in your prop schema to declare that a prop expects a rendered component vnode. Add accepts to restrict the slot to the output of a specific named component. The Card component below declares an action slot that only accepts a vnode produced by Button:
const Card = defineComponent({
  name: 'Card',
  props: {
    title:  { type: 'string', required: true },
    action: { type: 'slot',   accepts: 'Button', required: true },
  },
  render({ title, action }) {
    return createElement('div', { class: 'card' },
      createElement('h3', {}, title),
      action,
    )
  },
})
Inside render, you use the slot value directly as a child. Because Solar has already validated it at the call boundary, you know it is a legitimate vnode from the expected component — no additional checks needed.

Using a slot

To fill a slot, call the required component and pass its return value as the slot prop. The vnode it returns carries an internal _source tag that Solar checks against accepts.
// ✅ valid — Button() returns a vnode tagged with _source: 'Button'
Card({
  title:  'Hello',
  action: Button({ label: 'Go', onClick: () => {} }),
})

// ❌ throws ContractError — createElement returns a vnode with no _source tag
Card({
  title:  'Hello',
  action: createElement('button', {}, 'Go'),
})
The second call throws because a plain createElement vnode has no _source property. The ContractError it produces will tell you exactly which component to use instead.

How slot validation works

When defineComponent wraps your render function, it stamps every vnode the component returns with a _source property set to the component’s name:
// Inside defineComponent — runs after render() returns
if (vnode && typeof vnode === 'object') vnode._source = name
The slot validator inside validateProps then checks two things in order:
  1. Is the value an object (and not an array)? If not, it throws — a string or number cannot be a vnode.
  2. If accepts is set, does value._source equal accepts? If not, it throws, reporting whether the vnode has no _source at all (plain createElement) or the wrong _source (a different component).
This means only the output of the exact named component can satisfy a typed slot — there is no way to accidentally satisfy it with a structurally similar vnode from a different component.

Untyped slots

If you omit accepts, Solar accepts any rendered component vnode for that slot. The vnode still must be an object (not a plain value), but it can come from any registered or unregistered component.
props: {
  icon: { type: 'slot' },  // accepts any component vnode
}
Use untyped slots for genuinely polymorphic positions — a layout component that accepts any content block, for example. Use typed slots (with accepts) whenever your component has a real contract about what goes in that position.
When a slot validation fails, the ContractError’s fix field names the exact component you need to pass. For example: "Pass a vnode produced by the Button component". This field is intentionally formatted as a complete instruction so that an AI agent can read it and correct the call without parsing the error message string.