Tutorial: Build Reusable Panels

Four steps build one settings page with three collapsible panels that share a header and toggle mechanism but show completely different content each. By the end, one small component definition drives all three, plus a fourth piece that renders data it doesn't own.

Sandbox Examples: every example on this page runs in an isolated iframe, so it can't interfere with this documentation site's own components. Each is also a complete, self-contained file. Open it directly to read every line.

Step 1: One Component, Two Instances

Writing data-component="panel" a second time really does give you a second, fully independent instance. One wildflower.component('panel', {...}) registration; every element that names it gets its own state.

<div data-component="panel" data-title="Profile">...</div>
<div data-component="panel" data-title="Notifications">...</div>
Live Example Two panels, one definition, independent state Open Full Example ↗
Toggle "Profile" closed. "Notifications" stays open. That's the whole mechanism. No special tag syntax, no registry to configure. If you've been reaching for JSX-style frameworks, this is the same thing <Panel/><Panel/> gives you. It just doesn't look like a stamp the way a repeated component tag does, because it's an attribute, not a tag. It behaves identically anyway.

Step 2: Different Content Per Instance

The panel's body was hardcoded text in step 1. Real panels need real content, different per instance. A data-slot-container in the component's own layout marks where content goes; a data-slot at each usage site supplies it. The panel never sees an input, a checkbox, or a button, just a hole labeled "body."

<div class="panel-body" data-show="expanded">
    <div data-slot-container="body"></div>
</div>

<!-- at the usage site, a sibling of the component element -->
<div data-slot="body">
    <label>Name <input id="name-input" type="text"></label>
</div>
Live Example Three panels, three different bodies, one component Open Full Example ↗
The component definition at the bottom of the file is identical to step 1's. Nothing changed there. Everything new happened at the three usage sites. See Slots for named slots, fallback content, and reactive slot content.

Step 3: Multiple Named Slots

Slots aren't all-or-nothing. Add a second named container (a badge next to the title) and every instance decides independently whether to fill it. The component definition doesn't grow to accommodate the new slot; it just names it in its own layout.

<span class="panel-header-left">
    <span data-bind="title"></span>
    <span data-slot-container="badge"></span>
</span>

<!-- Notifications fills it: -->
<div data-slot="badge"><span class="badge">3</span></div>
<!-- Profile and Danger Zone don't. Nothing to opt out of. -->
Live Example An optional second slot, filled by one instance out of three Open Full Example ↗
Only "Notifications" shows a badge. An unfilled data-slot-container is simply empty. There's no error, no warning, no default to override.

Step 4: When the Child Owns the Data

Slots project static content the parent already has fully formed. Sometimes it's the other way around. The child owns a list of data it computes or fetches itself, and the parent wants to control how each item looks, without the child knowing anything about icons, classes, or layout. That's Configurable Templates. A parent declares a named <template data-item-template="...">; any descendant can render its own list through it with data-use-template.

<!-- settings-page (parent) declares the template, anywhere in its
     own markup, not necessarily right next to where it's used -->
<template data-item-template="logEntry">
    <div class="log-entry" data-bind-class="type === 'warning' ? 'warn' : ''">
        <span data-bind="icon"></span>
        <span data-bind="message"></span>
    </div>
</template>

<!-- activity-feed (child) renders ITS OWN data through it -->
<div data-component="activity-feed">
    <div data-list="entries">
        <template data-use-template="logEntry"></template>
    </div>
</div>
Why the class expression is inline. A named computed referenced from a list item (data-bind-class="entryClass") resolves against whichever component actually renders the list (activity-feed here), not the component that defined the template. An inline expression like type === 'warning' ? 'warn' : '' evaluates directly against the item's own fields, so it works no matter which component's markup the template lives in. That's what lets settings-page decide what "warning" looks like without activity-feed ever needing to expose a class name.
Live Example A child's own data, styled by the parent's template Open Full Example ↗
activity-feed's three log entries are hardcoded here for the example, but they could just as easily come from a store or a data-query. It decides what happened, and settings-page decides how a warning-type entry looks different from an info-type one. Neither component could do the other's job without the split.

What You Built

One component definition ended up driving three independent, differently-configured instances. Two named slots, one always filled and one optional, controlled what each panel showed. A fourth component rendered its own data through a template it never had to know existed, with no duplicated markup and no parallel state fields typed out three times.

Where to go deeper: