The Transient Root Pattern: Integrating Ember 6.8 and Storybook 10

By | December 11, 2025

Making Connections

Integrating a robust application framework with a documentation tool often involves reconciling different philosophies on how code should be built and rendered. I recently worked on integrating an Ember UI library with Storybook 10, and I encountered a specific architectural challenge. I actually tried and failed to combine the two many times before a peer suggested a bleeding edge feature that was just released might be my path to success.

My goal was straightforward: combine the standards based rendering of Ember 6.8 with the documentation environment of Storybook. While leaf components (like buttons) were easy to render, composite components, such as Accordions or Menus that rely on nested leaf components, required a different approach.

Here is how I solved the composite component problem using a pattern I call the Transient Root.

The Architecture

To keep the system maintainable, I established a “Russian Doll” architecture that separates the build concerns from the documentation display:

  1. ui-components: The core V2 Addon containing the source code.
  2. sample-site: A standard Ember application. It consumes the addon and acts as the compiler and bundler.
  3. storybook: A static HTML/Vite site that consumes the output of the sample site to render components.

In this setup, Storybook acts strictly as a viewer. It doesn’t compile Ember templates itself; it injects a runtime built by the sample-site.

The Challenge: renderComponent and composite components

My integration relies on the renderComponent API introduced in Ember (RFC #1099). This API allows me to mount a component instance to a DOM element, which works perfectly for simple components:

await renderComponent(MyButton, {
  element: document.getElementById('storybook-root'),
  args: { label: 'Click Me' }
});

However, composite components are designed to be “Headless,” yielding context to their children via blocks.

<Accordion as |item|>
  <item.Trigger>Open Me</item.Trigger>
  <item.Content>Content</item.Content>
</Accordion>

The limitation I faced is that renderComponent accepts a component definition and arguments, but it does not support passing composite components. When I attempted to render the Accordion directly, it rendered an empty shell because there was no mechanism to inject the required child components via the API.

The Solution: The Transient Root

Since renderComponent can only render a component instance, I realized I had to shift the responsibility of creating the composite components back to the Ember build. I call this a Transient Root Component, only real value is in this testing solution.

Instead of trying to construct the blocks inside the JavaScript story file, I create a wrapper component in the sample-site that already contains the necessary template structure.

1. Create the Wrapper

In the sample-site, I define a .gts component specifically for the demonstration.

// app/components/demos/accordion-demo.gts
import Accordion from '@ember-ui/components/accordion';

export default class AccordionDemo extends Component {
  <template>
    <Accordion @type={{@type}} as |a|>
      <a.Item @value="item-1">
        <a.Trigger>Item 1</a.Trigger>
        <a.Content>
          This content is standard Ember template code.
        </a.Content>
      </a.Item>
    </Accordion>
  </template>
}

2. Export to Runtime

I export this component via the storybook-entry.ts file, ensuring it is included in the pre-built runtime bundle that Storybook consumes.

3. Render the Wrapper

In the Storybook file, I instruct the adapter to render AccordionDemo rather than Accordion. I can still pass args (like @type) to the wrapper, which forwards them to the actual component.

// accordion.stories.ts
export const Default = {
  render: () => createEmberStory({
    component: 'AccordionDemo', // The wrapper acts as the root
    args: { type: 'single' }
  })
};

It Works, Next Problem

This approach avoids the need for complex workarounds like runtime string compilation or virtual block mocking. By using the Transient Root pattern, I achieve Environment Parity: the code running in Storybook is standard Glimmer template code, compiled by the same build pipeline used in production.

This ensures that my documentation accurately reflects the behavior of the library while respecting the strict API boundaries of Ember 6.8.

Leave a Reply

Your email address will not be published. Required fields are marked *

Comment moderation is enabled. Your comment may take some time to appear.

This site uses Akismet to reduce spam. Learn how your comment data is processed.