Tabs
A tablist of triggers that switch between content panels.
Installation
Included in poetry-ui — available as
poetry_tabs
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 tabs
Default
Overview
Analytics
Reports
Settings
<%# Upstream's tabs-demo: four sections, each panel a REAL Card
(title + description + muted content line). %>
<% sections = [
["Overview", "overview",
"View your key metrics and recent project activity. Track progress across all your active projects.",
"You have 12 active projects and 3 pending tasks."],
["Analytics", "analytics",
"Track performance and user engagement metrics. Monitor trends and identify growth opportunities.",
"Page views are up 25% compared to last month."],
["Reports", "reports",
"Generate and download your detailed reports. Export data in multiple formats for analysis.",
"You have 5 reports ready and available to export."],
["Settings", "settings",
"Manage your account preferences and options. Customize your experience to fit your needs.",
"Configure notifications, security, and themes."]
] %>
<%= poetry_tabs(label: "Project sections", class: "w-[400px]") do |tabs| %>
<% sections.each do |title, value, description, line| %>
<% tabs.with_tab(title, value: value) do %>
<%= poetry_card do |card| %>
<% card.with_title { title } %>
<% card.with_description { description } %>
<span class="text-sm text-muted-foreground"><%= line %></span>
<% end %>
<% end %>
<% end %>
<% end %>
Disabled
<%# Upstream's tabs-disabled: a live tab beside a disabled one - the
disabled trigger renders disabled + data-disabled and drops out of
the roving-focus collection. List-only like upstream (panel: false). %>
<%= poetry_tabs(label: "Disabled tab demo") do |tabs| %>
<% tabs.with_tab("Home", value: "home", panel: false) %>
<% tabs.with_tab("Disabled", value: "settings", disabled: true, panel: false) %>
<% end %>
Line
<%# Upstream's tabs-line: the list alone carries the demo - three line
triggers, no panels (panel: false is the declared list-only shape). %>
<%= poetry_tabs(variant: :line, label: "Docs sections") do |tabs| %>
<% tabs.with_tab("Overview", value: "overview", panel: false) %>
<% tabs.with_tab("Analytics", value: "analytics", panel: false) %>
<% tabs.with_tab("Reports", value: "reports", panel: false) %>
<% end %>
Vertical
<%= poetry_tabs(orientation: :vertical, label: "Project areas", class: "w-full max-w-md") do |tabs| %>
<% tabs.with_tab("General", value: "general") { "Project defaults." } %>
<% tabs.with_tab("Members", value: "members") { "Who has access." } %>
<% tabs.with_tab("Billing", value: "billing", disabled: true) { "Plan and invoices." } %>
<% end %>
With icon
<%# The tab title accepts markup - wrap a poetry_icon and the label in an
inline-flex span so the trigger shows an icon before its text. %>
<%= poetry_tabs(label: "Account settings", class: "w-full max-w-md") do |tabs| %>
<% tabs.with_tab(tag.span(class: "inline-flex items-center gap-1.5") { safe_join([poetry_icon(name: :user), "Profile"]) }, value: "profile") do %>
Update your name, avatar, and contact details.
<% end %>
<% tabs.with_tab(tag.span(class: "inline-flex items-center gap-1.5") { safe_join([poetry_icon(name: :"credit-card"), "Billing"]) }, value: "billing") do %>
Review your plan, invoices, and payment method.
<% end %>
<% tabs.with_tab(tag.span(class: "inline-flex items-center gap-1.5") { safe_join([poetry_icon(name: :inbox), "Notifications"]) }, value: "notifications") do %>
Choose which alerts land in your inbox.
<% end %>
<% end %>
API
Poetry::Ui::Tabs::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 |
|---|---|---|---|
| default: | String | The value of the server-rendered active tab; defaults to the first enabled tab. Raises when it matches no tab. | |
| label: | String | The tablist's accessible name - recommended when a page has several tab sets. | |
| orientation: | Symbol | one of: horizontal, vertical; defaults to :horizontal |
The tab axis; :vertical stacks the triggers and flips the arrow keys. |
| variant: | Symbol | one of: default, line; defaults to :default |
The list's visual treatment: :default a filled capsule, :line an underline indicator. |
Slots
| Writer | Description |
|---|---|
| with_tab | Declares one tab: the title, its value:, and the panel as the block (defer: swaps in a lazy turbo-frame panel; panel: false declares a list-only tab). Omitting all three raises. |
Methods
| Method | Description |
|---|---|
| #webmcp_tool_definition(definition) | The rendered instance knows its tab values: the payload's schema carries them as the enum, so an agent can only ask for a tab that exists (the class-level projection stays a plain string). |
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=tabs] | Root wrapper - the orientation rides here and flips the flex direction |
| [data-slot=tabs-list] | The role=tablist row of triggers - the roving-focus keyboard group and the visual variant ride here |
| [data-slot=tabs-trigger] | One role=tab button per tab |
| [data-slot=tabs-content] | One role=tabpanel per tab - only the active panel is visible |
State attributes
| Part | Attribute | Condition | Values |
|---|---|---|---|
| tabs | data-orientation | the tab axis (matches aria-orientation on the list) | horizontal · vertical |
| tabs-list | data-variant | the list treatment - default (filled capsule) or line (underline indicator) | — |
| tabs-trigger | data-active | the selected tab (the controller moves it with aria-selected on activation) | — |
| tabs-trigger | data-disabled | tab is disabled - also filters it from the roving-focus collection | — |
| tabs-trigger | data-value | the tab's value - the key the controller matches panels against | — |
| tabs-content | data-hidden | panel is inactive (paired with the hidden property - the controller flips both) | — |
| tabs-content | data-value | the owning tab's value | — |
Agent tools
What an agent may do to a rendered instance once the call opts in
(webmcp: "name") — declared beside the wiring, projected
to the registry, registered with the browser's document.modelContext
by poetry-agent.
| Tool | Parameters | Does | Dispatches |
|---|---|---|---|
| set_value mutating | value (string, required) | Activate the tab whose value matches and show its panel. | poetry--core--tabs#setValue |
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--tabs | registers |
| list | poetry--core--roving-focus | registers · value orientation · value loop · keydown on keydown |
| list | poetry--core--tabs | focusActivate on poetry--core--roving-focus:entry |
| trigger | poetry--core--tabs | activate on click |