Slots & Composition

How Vue/React slots and children map to Kiso's yield and sub-part composition.

Default slot

Vue’s <slot>, React’s {children}, and Kiso’s yield all do the same thing — render whatever the caller puts inside the component:

Vue

<Card>
  Hello
</Card>

React

<Card>
  Hello
</Card>

Kiso

<%%= kui(:card) do %>
  Hello
<%% end %>

Named slots → sub-part composition

Vue has named slots (<template #header>). React uses compound components (<Card.Header>). Kiso uses sub-part partials:

Vue

<Card>
  <template #header>
    <CardTitle>Title</CardTitle>
  </template>
  Content here
  <template #footer>
    <Button>Save</Button>
  </template>
</Card>

React (shadcn)

<Card>
  <CardHeader>
    <CardTitle>Title</CardTitle>
  </CardHeader>
  <CardContent>
    Content here
  </CardContent>
  <CardFooter>
    <Button>Save</Button>
  </CardFooter>
</Card>

Kiso

<%%= kui(:card) do %>
  <%%= kui(:card, :header) do %>
    <%%= kui(:card, :title) { "Title" } %>
  <%% end %>
  <%%= kui(:card, :content) do %>
    Content here
  <%% end %>
  <%%= kui(:card, :footer) do %>
    <%%= kui(:button) { "Save" } %>
  <%% end %>
<%% end %>

kui(:card, :header) renders the card/_header.html.erb partial. Each sub-part is its own file with its own locals — just like how shadcn decomposes Card into CardHeader, CardTitle, CardContent, and CardFooter.

Why composition instead of content_for?

Rails does have content_for / yield :name, which looks like named slots:

<% # This works but has a gotcha %>
<%= kui(:card) do %>
  <% content_for :header, "Title" %>
  Body content
<% end %>

The problem: content_for is page-global. If you render two Cards on the same page, their :header content bleeds into each other. Sub-part composition doesn’t have this problem — each kui(:card, :header) call is isolated.

This is the same reason shadcn and Nuxt UI use compound components instead of Vue’s named slots for complex layouts.

Choosing between yield and sub-parts

Use yield (default slot) for simple components where the caller provides a single block of content:

<%= kui(:badge, color: :success) { "Active" } %>

<%= kui(:button, variant: :solid) do %>
  Save changes
<% end %>

Use sub-parts for structured components with multiple content areas:

<%= kui(:alert, color: :info) do %>
  <%= kui(:alert, :title) { "Heads up!" } %>
  <%= kui(:alert, :description) { "This is important." } %>
<% end %>

Each component’s docs page lists its available sub-parts.