Collapsible
An interactive element that expands and collapses a section of content.
Installation
Included in poetry-ui — available as
poetry_collapsible
the moment you've installed Poetry,
with no per-component step. To own the source and edit it, copy it into your app:
bin/rails g poetry:add collapsible
Default
<div class="w-full max-w-md">
<%= poetry_collapsible do |collapsible| %>
<% collapsible.with_trigger(class: "text-sm font-medium underline-offset-4 hover:underline") { "Show details" } %>
<div class="pt-2 text-sm text-muted-foreground">Hidden until disclosed.</div>
<% end %>
</div>
File tree
<%# Nested collapsibles compose a file tree: each folder is its own
disclosure, and one collapsible's content simply holds the next. The
leading chevron turns a quarter to point down when its folder opens. %>
<div class="w-full max-w-xs text-sm">
<%= poetry_collapsible(open: true) do |root| %>
<% root.with_trigger(class: "flex w-full items-center gap-1.5 rounded-md px-2 py-1 font-medium hover:bg-accent [&[aria-expanded=true]>svg:first-child]:rotate-90") do %>
<%= poetry_icon(name: :"chevron-right", class: "size-4 shrink-0 text-muted-foreground transition-transform") %>
<%= poetry_icon(name: :folder, class: "size-4 shrink-0") %>
app
<% end %>
<div class="ml-3 border-l pl-2">
<div class="flex items-center gap-1.5 rounded-md px-2 py-1 text-muted-foreground">
<%= poetry_icon(name: :file, class: "size-4 shrink-0") %>
application.rb
</div>
<%= poetry_collapsible do |nested| %>
<% nested.with_trigger(class: "flex w-full items-center gap-1.5 rounded-md px-2 py-1 font-medium hover:bg-accent [&[aria-expanded=true]>svg:first-child]:rotate-90") do %>
<%= poetry_icon(name: :"chevron-right", class: "size-4 shrink-0 text-muted-foreground transition-transform") %>
<%= poetry_icon(name: :folder, class: "size-4 shrink-0") %>
components
<% end %>
<div class="ml-3 border-l pl-2">
<div class="flex items-center gap-1.5 rounded-md px-2 py-1 text-muted-foreground">
<%= poetry_icon(name: :file, class: "size-4 shrink-0") %>
button.rb
</div>
<div class="flex items-center gap-1.5 rounded-md px-2 py-1 text-muted-foreground">
<%= poetry_icon(name: :file, class: "size-4 shrink-0") %>
card.rb
</div>
</div>
<% end %>
</div>
<% end %>
</div>
Initially open
<div class="w-full max-w-md">
<%= poetry_collapsible(open: true) do |collapsible| %>
<% collapsible.with_trigger(class: "text-sm font-medium") { "Hide details" } %>
<div class="pt-2 text-sm text-muted-foreground">Server-rendered open.</div>
<% end %>
</div>
Settings panel
Export project
Download everything as a portable archive.
<%# Collapsible as progressive disclosure for configuration: the primary
control stays visible while the rarely touched options sit behind an
"Advanced" disclosure, keeping the panel calm by default. %>
<div class="w-full max-w-md rounded-lg border bg-card p-4">
<div class="space-y-1">
<p class="text-sm font-medium leading-none">Export project</p>
<p class="text-sm text-muted-foreground">Download everything as a portable archive.</p>
</div>
<div class="mt-4">
<%= poetry_collapsible do |collapsible| %>
<% collapsible.with_trigger(class: "flex w-full items-center justify-between text-sm font-medium [&[aria-expanded=true]>svg]:rotate-180") do %>
Advanced options
<%= poetry_icon(name: :"chevron-down", class: "size-4 text-muted-foreground transition-transform") %>
<% end %>
<div class="space-y-3 pt-3 text-sm">
<div class="flex items-center justify-between">
<span>Include attachments</span>
<span class="text-muted-foreground">2.4 GB</span>
</div>
<div class="flex items-center justify-between">
<span>Compression</span>
<span class="text-muted-foreground">Balanced</span>
</div>
<div class="flex items-center justify-between">
<span>Split into parts</span>
<span class="text-muted-foreground">Off</span>
</div>
</div>
<% end %>
</div>
</div>
With icon
<%# A trigger carrying a trailing chevron that flips when the section opens.
The icon rotates off the button's aria-expanded - the same signal the
state controller mirrors - so no extra wiring is needed. %>
<div class="w-full max-w-md">
<%= poetry_collapsible do |collapsible| %>
<% collapsible.with_trigger(class: "flex w-full items-center justify-between rounded-md px-3 py-2 text-sm font-medium hover:bg-accent [&[aria-expanded=true]>svg]:rotate-180") do %>
Shipping and returns
<%= poetry_icon(name: :"chevron-down", class: "size-4 text-muted-foreground transition-transform") %>
<% end %>
<div class="px-3 pt-2 text-sm text-muted-foreground">
Orders ship within two business days and arrive in five to seven. Returns
stay free for thirty days, and we email a prepaid label the moment you
start one.
</div>
<% end %>
</div>
API
Poetry::Ui::Collapsible::Component — options are
constructor keywords (the poetry_* helper forwards them);
slots are composed inside the block. Generated from the gem's source documentation.
| Option | Type | Details | Description |
|---|---|---|---|
| open: | Boolean | defaults to false |
The server-rendered initial state; the trigger toggles it client-side. |
Slots
| Writer | Description |
|---|---|
| with_trigger | The disclosure control - a real button, wired for you (aria-expanded, aria-controls); options merge onto it. |
Styling
Every part carries a stable data-slot attribute — target
[data-slot=…] from your own CSS to restyle it. State rides
data attributes on the parts below. This contract is verified against rendered DOM in CI.
| Part | Description |
|---|---|
| [data-slot=collapsible] | The disclosure root - the state controller flips the pair here |
| [data-slot=collapsible-trigger] | The disclosure button - mirrors aria-expanded |
| [data-slot=collapsible-content] | The disclosure panel - stays in the DOM when closed (hidden) and rides the presence helper on exit |
State attributes
| Part | Attribute | Condition | Values |
|---|---|---|---|
| collapsible | data-open | expanded (server-rendered from open:; the controller flips the pair at runtime) | — |
| collapsible | data-closed | collapsed (the server-rendered default) | — |
| collapsible-trigger | data-panel-open | its content is open (controller-written; absent while closed) | — |
| collapsible-content | data-open | content is open or entering | — |
| collapsible-content | data-closed | content is closed or animating out (hidden lands after the exit finishes) | — |
Wiring
The Stimulus surface each element carries — declared in the component, verified against rendered DOM in CI. Bare actions fire on the element's default event.
| Element | Controller | Wiring |
|---|---|---|
| root | poetry--core--state | registers |
| trigger | poetry--core--state | toggle on click · target trigger |
| content | poetry--core--state | target content |