Back to flin
flin

Slots and Content Projection

How FLIN's slot system enables component composition -- default slots for children, named slots for structured layouts, and fallback content for empty slots.

Thales & Claude | March 25, 2026 9 min flin
flinslotscontent-projectioncomposition

A Button component renders a button. A Card component renders a card. But what goes inside the card? The Card component does not know. It should not know. The parent that uses decides what content goes inside.

This is content projection -- the ability for a parent component to pass arbitrary content into a child component's template. In React, it is {children}. In Vue and Svelte, it is . In Angular, it is . In FLIN, it is both {props.children} for simple cases and for advanced layouts.

Sessions 108 and 109 built FLIN's slot system with three capabilities: default slots for basic content projection, named slots for multi-region layouts, and fallback content for empty slots.

Default Slots: The Simple Case

The simplest form of content projection passes children from the parent into the child component:

// Card.flin
<div class="card">
    <div class="card-body">
        {props.children}
    </div>
</div>

// Usage

Welcome

This content is projected into the card.

```

{props.children} renders whatever the parent placed between the opening and closing tags. The Card component does not know or care what the content is. It provides the visual wrapper (border, padding, shadow), and the parent provides the content.

This is the same mechanism as React's {children} prop. The difference is that in FLIN, children is automatically available through props.children -- no special import, no type annotation, no destructuring.

Named Slots: Multi-Region Layouts

Default slots handle one content region. But many components need multiple regions. A Modal has a header, a body, and a footer. A Page layout has a sidebar, a main content area, and an optional toolbar.

Named slots let the parent project content into specific regions:

// Modal.flin
<div class="modal-overlay">
    <div class="modal">
        <div class="modal-header">
            <slot name="header">
                <Text weight="bold">Modal</Text>
            </slot>
            <button class="close" click={props.onClose}>&times;</button>
        </div>
        <div class="modal-body">
            <slot />
        </div>
        <div class="modal-footer">
            <slot name="footer">
                <Button click={props.onClose}>Close</Button>
            </slot>
        </div>
    </div>
</div>

// Usage Confirm Deletion

Are you sure you want to delete this item? This action cannot be undone.

```

The syntax: - in the component defines the default (unnamed) slot - defines a named slot - in the parent fills a named slot - Content outside any fills the default slot

How Named Slots Are Resolved

When the renderer encounters a component with slot content, it:

1. Collects all blocks from the parent's children 2. Collects all remaining children as default slot content 3. Renders the component's template 4. Replaces each with the corresponding content 5. Replaces with the default content

// Layout.flin
<div class="layout">
    <header class="layout-header">
        <slot name="header" />
    </header>
    <aside class="layout-sidebar">
        <slot name="sidebar" />
    </aside>
    <main class="layout-main">
        <slot />
    </main>
    <footer class="layout-footer">
        <slot name="footer" />
    </footer>
</div>

// Usage My App

Dashboard Users

Dashboard

Copyright 2026 ZeroSuite ```

This creates a complete page layout with four distinct regions, each filled by the parent. The Layout component provides the CSS Grid or Flexbox structure. The parent provides the content for each region.

Fallback Content: Default Slot Content

When a named slot is not filled by the parent, the component can provide fallback content:

// Card.flin
<div class="card">
    <div class="card-header">
        <slot name="header">
            <Text weight="bold">Card</Text>
        </slot>
    </div>
    <div class="card-body">
        <slot>
            <Text color="muted">No content provided</Text>
        </slot>
    </div>
    <div class="card-footer">
        <slot name="footer">
            <!-- Empty footer by default -->
        </slot>
    </div>
</div>

If the parent writes Hello, the header slot shows the fallback ("Card"), the default slot shows "Hello", and the footer slot shows nothing.

If the parent writes: ``flin Custom Title Custom content here ``

The header slot shows "Custom Title" (overriding the fallback), the default slot shows "Custom content here", and the footer slot shows nothing.

Fallback content makes components usable with minimal props while remaining fully customizable when needed.

Composition Patterns

Wrapper Components

Slots enable "wrapper" components that add behavior without constraining content:

// Collapsible.flin
expanded = props.expanded || false

{expanded ? "Hide" : "Show"}
{if expanded}
{/if}

// Usage

{if results.len > 0}

{/if}

// Usage -- custom item rendering {item.name} {item.email} ```

The let:item directive binds the slot prop to a variable in the parent's scope. The Autocomplete component passes each item to the slot, and the parent decides how to render it. This is the same pattern as React's "render props" and Vue's "scoped slots," but with cleaner syntax.

Implementation: Slot Resolution

Slots are resolved during the rendering phase. The component compiler identifies tags in the template and marks them with their names (or "default" for unnamed slots). The renderer collects slot content from the parent and substitutes:

fn render_slot(
    slot: &SlotDefinition,
    provided_slots: &HashMap<String, Vec<ViewNode>>,
    vm: &mut Vm,
) -> Result<String, RenderError> {
    let slot_name = slot.name.as_deref().unwrap_or("default");

if let Some(content) = provided_slots.get(slot_name) { // Parent provided content for this slot render_nodes(content, vm) } else if !slot.fallback.is_empty() { // Use fallback content render_nodes(&slot.fallback, vm) } else { // Empty slot Ok(String::new()) } } ```

The slot resolution is a simple map lookup. If the parent provided content for the slot name, render it. If not, render the fallback. If there is no fallback, render nothing. The performance cost is one hash map lookup per slot.

Slots vs. Props for Content

A common question: when should you use slots, and when should you use props?

Use props when the content is a single value (text, number, entity): ``flin ``

Use named slots when the content has multiple distinct regions: ``flin Title Body content Actions ``

The rule of thumb: if you are passing a string, use a prop. If you are passing HTML, use a slot.

Slot Forwarding

Components can forward their own slots to child components, enabling deep composition:

// EnhancedCard.flin -- wraps Card with additional features
<Card>
    <slot:header>
        <Stack direction="horizontal" align="center">
            <slot name="icon" />
            <slot name="header">
                <Text weight="bold">{props.title}</Text>
            </slot>
        </Stack>
    </slot:header>

// Usage

Are you sure? This cannot be undone.

```

The EnhancedCard component passes its own slots through to the underlying Card component. The parent's content replaces the EnhancedCard's default footer, which in turn fills the Card's footer slot. Slot forwarding works at any depth -- the resolution algorithm walks the component tree until it finds the final content.

Comparison: Slots Across Frameworks

FrameworkDefault SlotNamed SlotsSlot PropsFallback
React{children}Props with render functionsVia render propsConditional check
Vuev-slot:x="data"fallback
Sveltelet:propfallback
FLIN or {props.children}let:propfallback

FLIN's slot syntax is closest to Svelte's, which is unsurprising given that both prioritize simplicity and HTML-like syntax. The key difference is that FLIN slots work without imports -- the component with slots is discovered automatically, just like any other component.

Why Slots Enable Real Composition

Without slots, components are leaves in the tree. A Button renders a button. A Card renders a card. They cannot contain arbitrary content from the parent. The only way to pass content is through props, which means the child component must know the type and structure of everything it displays.

With slots, components become containers. A Card can contain anything. A Modal can contain any form. A Layout can contain any page. The component provides structure and behavior. The parent provides content. This separation is what makes component libraries reusable -- the same Card component works in an e-commerce store, an admin dashboard, and a content management system because it does not know or care what goes inside it.

The combination of zero-import discovery and slot-based composition means that FLIN components compose as naturally as HTML elements. A

can contain any other element. A can contain any other component. No imports, no registration, no configuration. Just tags inside tags, all the way down.

---

This is Part 91 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO built content projection into a component system.

Series Navigation: - [90] The Component Lifecycle - [91] Slots and Content Projection (you are here) - [92] Attribute Reactivity - [93] Theme Toggle and Dark Mode

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles