Slots

Slots allow a component to treat the JSX children of the component as a form of input and project these children into the component's DOM tree.

This concept has different names in different frameworks:

  • In Angular is called Content Projection
  • In React, it's the children of the props
  • In Web components it's <slot> as well

The main API to achieve this is the <Slot> component, exported in @builder.io/qwik:

import { Slot, component$ } from '@builder.io/qwik';
 
const Button = component$(() => {
  return (
    <button>
      Content: <Slot />
    </button>
  );
});
 
export default component$(() => {
  return (
    <Button>
      This goes inside {'<Button>'} component marked by{`<Slot>`}
    </Button>
  );
});

The <Slot> component is a placeholder for the children of the component. The <Slot> component will be replaced by the children of the component during rendering.

Note: Slots in Qwik are declarative, allowing Qwik to render parents and children in isolation. Because slots are declarative, the children can NOT be read, or transformed by the components.

Why? Because Qwik needs to be able to render parent/children components independently from each other. With an imperative (children) approach, the child component can modify the children in countless ways. If a child component relied on children, it would be forced to re-render whenever a parent component would re-render to reapply the imperative transformation to the children. The extra rendering goes explicitly against the goals of Qwik components rendering in isolation.

Named slots

The Slot component can be used multiple times in the same component, as long as it has a different name property:

import { Slot, component$, useStylesScoped$ } from '@builder.io/qwik';
import CSS from './index.css?inline';
 
const Tab = component$(() => {
  useStylesScoped$(CSS);
  return (
    <section>
      <h2>
        <Slot name="title" />
      </h2>
      <div>
        <Slot /> {/* default slot */}
        <div>
          <Slot name="footer" />
        </div>
      </div>
    </section>
  );
});
 
export default component$(() => {
  return (
    <Tab>
      <div q:slot="title">Qwik</div>
      <div>A resumable framework for building instant web applications</div>
      <span q:slot="footer">made with ❤️ by </span>
      <a q:slot="footer" href="https://builder.io">
        builder.io
      </a>
    </Tab>
  );
});

Now, when consuming the <Tab> component, we can pass children and specify in which slot they should be placed, using the q:slot attribute:

Notice that:

  • If q:slot is not specified or it's an empty string, the content will be projected into the default <Slot>, i.e. the <Slot> without a name property.
  • Multiple q:slot="footer" attributes coalesce items together in the content projection.

Unprojected content

Qwik keeps all content around, even if not projected. This is because the content could be projected in the future. When projected content does not match any <Slot> component, the content is moved into an inert <q:template> element.

import { Slot, component$, useSignal } from '@builder.io/qwik';
 
const Accordion = component$(() => {
  const isOpen = useSignal(false);
  return (
    <div>
      <h1 onClick$={() => (isOpen.value = !isOpen.value)}>
        {isOpen.value ? '▼' : '▶︎'}
      </h1>
      {isOpen.value && <Slot />}
    </div>
  );
});
 
export default component$(() => {
  return (
    <Accordion>
      I am pre-rendered on the Server and hidden until needed.
    </Accordion>
  );
});

Results in:

<div>
  <h1>▶︎</h1>
</div>
<q:template q:slot hidden aria-hidden="true">
  I am pre-rendered on the Server and hidden until needed.
</q:template>

Notice that the un-projected content is moved into an inert <q:template>. This is done just in case the Accordion component re-renders and inserts the <Slot>. In that case, we avoid having to re-render the parent component just to generate the projected content. By persisting the un-projected content when the parent is initially rendered, the rendering of the two components can stay independent.

Invalid projection

The q:slot attribute must be a direct child of a component.

import { component$ } from '@builder.io/qwik';
 
export const Project = component$(() => { ... })
 
export const MyApp = component$(() => {
  return (
    <Project>
      <span q:slot="title">ok, direct child of Project</span>
      <div>
        <span q:slot="title">Error, not a direct child of Project</span>
      </div>
    </Project>
  );
});

Advanced Example

An example of a collapsible component that conditionally projects editable content.

import { Slot, component$, useSignal } from '@builder.io/qwik';
 
export const Collapsible = component$(() => {
  const isOpen = useSignal(true);
 
  return (
    <div>
      <h1 onClick$={() => (isOpen.value = !isOpen.value)}>
        {isOpen.value ? '▼' : '▶︎'}
        <Slot name="title" />
      </h1>
      {isOpen.value && <Slot />}
    </div>
  );
});
 
export default component$(() => {
  const title = useSignal('Qwik');
  const description = useSignal(
    'A resumable framework for building instant web applications'
  );
  return (
    <>
      <label>Title</label>
      <input bind:value={title} type="text" />
      <label>Description</label>
      <textarea bind:value={description} cols={50} />
      <hr />
      <Collapsible>
        <span q:slot="title">{title}</span>
        {description}
      </Collapsible>
    </>
  );
});

The Collapsible component will always display the title, but the body of the text will only display if store.isOpen is true.

Additionally, the title and description of the projected content are editable.

  • The parent component needs to be able to change content without forcing the Collapsible component to re-render.
  • The child component needs to change what is projected without having the parent component re-render. In our case, Collapsible should be able to show/hide the default q:slot without downloading and re-rendering the parent component.

In order for the two components to have an independent lifecycle, the projection needs to be declarative. In this way, either the parent or child can change what is projected or how it is projected without forcing the re-rendering of the other.

Projection vs children

All frameworks need a way for a component to wrap its complex content conditionally. This problem is solved in many different ways, but there are two predominant approaches:

  • projection: Projection is a declarative way of describing how the content gets from the parent template to where it needs to be projected.
  • children: children refers to vDOM approaches that treat content just like another input.

The two approaches can best be described as declarative vs imperative. They both come with their set of advantages and disadvantages.

Qwik uses the declarative projection approach. The reason for this is that Qwik needs to be able to render parent/children components independently from each other. With an imperative (children) approach, the child component can modify the children in countless ways. If a child component relied on children, it would be forced to re-render whenever a parent component would re-render to reapply the imperative transformation to the children. The extra rendering goes explicitly against the goals of Qwik components rendering in isolation.

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • RATIU5
  • manucorporat
  • forresst
  • adamdbradley
  • cunzaizhuyi
  • zanettin
  • lbensaad
  • gabrielgrant
  • mhevery