Tabs
WAI-ARIA APG-compliant tabbed navigation. Selecting a tab shows its panel — everything else (roles, roving tabindex, keyboard nav, optional deep-linking) is wired by a single Stimulus controller reading server-rendered markup.
Key Features
- Four visual variants —
:underline,:pills,:boxed,:spaced(our flagship design) - Full WAI-ARIA APG tabs pattern — explicit
aria-selectedon every tab, roving tabindex, wrap-around arrow navigation - Horizontal + vertical orientation —
tablist_position: :endflips sides visually without reordering the DOM - Icons and badges in triggers — a
badge:shortcut for one counter, or abadges:array for several - Per-tab color override —
color:onrui_tabssets the default;with_tab(color:)overrides a single tab - Optional deep-linking —
deep_link: truereads/writeslocation.hashwithout extra history entries - Reduced-motion aware — the fade-in panel reveal is fully suppressed under
prefers-reduced-motion - Pro tier — one Stimulus controller (
rui--tabs), server-rendered structure and styling
Basic Usage
Pass a block and populate it with with_tab calls. The first tab is active by default — mark a different one with active: true to change the initial selection.
A flexible online sale format that allows buyers more time to review and bid.
A flat commission on the hammer price — no listing fees, no hidden charges.
<%= rui_tabs(id: "pricing") do |t| %>
<% t.with_tab(id: "overview", label: "Overview") do %>
Overview panel content
<% end %>
<% t.with_tab(id: "fees", label: "Fees") do %>
Fees panel content
<% end %>
<% end %>
The Spaced Variant
Standalone rounded blocks with a gap between them — not connected. The active tab is a solid, dark color: block with white text; inactive tabs are a soft tint of the same color. A hairline divider under the row is on by default for this variant (off by default for the other three). Panel content can nest another rui_tabs — here, Become a User holds a smaller, differently-colored subtab row.
A flexible online sale format that allows buyers more time to review and bid before the countdown closes lot by lot.
Bidding opens Monday 9am and closes Thursday from 2pm, with lots closing at staggered 30-second intervals.
Register as a Vendor, Buyer, or Agency — each role has its own registration flow.
List stock for sale — set a reserve, choose a sale date, and let buyers compete.
Register to bid — verified buyers get outbid alerts and max-bid automation.
Selling agents manage listings on behalf of vendors from a single dashboard.
A flat commission on the hammer price — no listing fees, no hidden charges.
<%= rui_tabs(id: "listing-type", variant: :spaced, color: :primary) do |t| %>
<% t.with_tab(id: "overview", label: "Overview", icon: "layout-grid") do %>
Overview panel content
<% end %>
<% t.with_tab(id: "become-a-user", label: "Become a User", icon: "user-plus") do %>
<%# Nested subtabs — smaller, different color, same API %>
<%= rui_tabs(id: "become-a-user-role", variant: :spaced, size: :sm, color: :secondary) do |st| %>
<% st.with_tab(id: "vendor", label: "Vendor") { "Vendor content" } %>
<% st.with_tab(id: "buyer", label: "Buyer") { "Buyer content" } %>
<% st.with_tab(id: "agency", label: "Agency") { "Agency content" } %>
<% end %>
<% end %>
<% t.with_tab(id: "fees", label: "Fees", icon: "coins") do %>
Fees panel content
<% end %>
<% end %>
Variants
Four visual variants, all sharing the same API.
:underline (default)
Connected row, transparent bg. Active tab gets a 2px bottom border.
Details panel content.
Reviews panel content.
:pills
Rounded-full triggers. Active gets a solid color background.
Details panel content.
Reviews panel content.
:boxed
Tablist has a bottom border; the active tab merges into the panel.
Details panel content.
Reviews panel content.
:spaced
Standalone rounded blocks. Active = solid dark bg; inactive = soft tint.
Details panel content.
Reviews panel content.
<%= rui_tabs(id: "t1", variant: :underline) do |t| %>
<%= rui_tabs(id: "t2", variant: :pills) do |t| %>
<%= rui_tabs(id: "t3", variant: :boxed) do |t| %>
<%= rui_tabs(id: "t4", variant: :spaced) do |t| %>
Tabs Options
Tabs-level options control variant, color, size, orientation, and behavior.
rui_tabs(
id:, # required — prefix for every tab/panel DOM id
variant: :underline, # :underline | :pills | :boxed | :spaced
color: :primary, # semantic color, resolved via ColorBuilderHelper
size: :base, # :sm | :base | :lg
orientation: :horizontal, # :horizontal | :vertical
tablist_position: :start, # vertical only — :start (left) | :end (right)
divider: nil, # nil = variant default (:spaced -> true, others -> false)
mobile_layout: :row, # horizontal only — :row | :grid (2-col grid below md)
full_width: false, # horizontal only — tabs stretch equally (flex-1)
activation: :automatic, # :automatic (arrow focus selects) | :manual (Enter/Space selects)
deep_link: false, # read/write location.hash on switch
animation: :fade, # :fade | :none — suppressed under prefers-reduced-motion
panels: true, # false renders the tablist only (external panels)
active_shade: nil, # :pills/:spaced only — 500/600/700/800/900/950, nil = color's own default
active_icon_color: nil, # tint the ACTIVE tab's icon (nil = matches label, unchanged)
active_icon_shade: 300, # 100/200/300/400/500 — ignored unless active_icon_color: is set
active_bg_color: nil, # :underline/:boxed only — light fill for the ACTIVE trigger, nil = variant default
active_bg_shade: 100, # 50/100/200/300/400 — ignored unless active_bg_color: is set
inactive_bg_color: nil, # :underline/:boxed only — light fill for INACTIVE triggers, nil = variant default
inactive_bg_shade: 100 # 50/100/200/300/400 — ignored unless inactive_bg_color: is set
) do |t|
# ...tabs
end
Active Shade & Icon Color (v0.55.0)
active_shade: and active_icon_color:/active_icon_shade: are :pills/:spaced-only escape hatches. active_shade: overrides the active trigger's solid bg/hover/border/focus-ring shade — normally fixed per color category (900 for :primary-style colors) with no way to dial it down. active_icon_color: tints only the active tab's icon a different color than its (unchanged) label text; active_icon_shade: (default 300) controls how light that tint is. Both are nil by default, so existing :pills/:spaced tabs render byte-identical output.
active_shade: 600 pulls the active tab's solid background down from the default 900 to 600.
Sale Dates panel content.
Fees panel content.
What's controlling what: active_shade: 600 is the shade control — it darkens/lightens the active tab's solid background (and its hover/border/focus-ring) without touching inactive tabs or dark mode. active_icon_color: :sky is the icon-tint control — it colors only the active tab's icon (sky-300, via the default active_icon_shade: 300) while the label text stays white. Switch tabs above to see both apply only to whichever tab is active.
<%= rui_tabs(id: "commercial", variant: :spaced, color: :primary, active_shade: 600, active_icon_color: :sky, full_width: true) do |t| %>
<% t.with_tab(id: "overview", label: "Overview", icon: "layout-grid", active: true) do %>
Overview panel content
<% end %>
<% t.with_tab(id: "sale-dates", label: "Sale Dates", icon: "calendar") do %>
Sale Dates panel content
<% end %>
<% t.with_tab(id: "fees", label: "Fees", icon: "coins") do %>
Fees panel content
<% end %>
<% end %>
Active & Inactive Background Fill (v0.56.2 / v0.56.4)
active_bg_color:/active_bg_shade: and inactive_bg_color:/inactive_bg_shade: are :underline/:boxed-only escape hatches — both variants render no background on their active OR inactive triggers by default (border/text only on :underline, plain white/zinc-900 active fill on :boxed). Each pair gives its own trigger state a light color fill, independent of color:. All four are nil/default by default, so existing :underline/:boxed tabs render byte-identical output. active_bg_color:/active_bg_shade: shipped in v0.56.2; inactive_bg_color:/inactive_bg_shade: is the v0.56.4 mirror for the inactive state. The two compose freely on the same tabs instance.
active_bg_color: :primary gives this active tab a light primary fill instead of the default no-background :underline look.
inactive_bg_color: :zinc gives every inactive tab a light zinc fill.
Fees panel content.
What's controlling what: active_bg_color: :primary tints only the currently active tab; inactive_bg_color: :zinc tints every other tab. Each reads a completely independent trigger-state class string, so switching tabs never lets one fill bleed into or get eaten by the other.
<%= rui_tabs(id: "pricing", variant: :underline, color: :primary, active_bg_color: :primary, active_bg_shade: 100, inactive_bg_color: :zinc, inactive_bg_shade: 50) do |t| %>
<% t.with_tab(id: "overview", label: "Overview", active: true) do %>
Overview panel content
<% end %>
<% t.with_tab(id: "fees", label: "Fees") do %>
Fees panel content
<% end %>
<% end %>
Sizes
Three sizes — sm, base (default), lg — scale trigger padding, text, icon, and badge size together.
:sm
:base (default)
:lg
<%= rui_tabs(id: "t", size: :sm) do |t| %>
<%= rui_tabs(id: "t", size: :base) do |t| %>
<%= rui_tabs(id: "t", size: :lg) do |t| %>
Vertical Orientation
orientation: :vertical turns the tablist into a column (shrink-0); panels take the remaining width (flex-1). The tablist carries aria-orientation="vertical", and ArrowDown/ArrowUp move focus instead of ArrowRight/ArrowLeft.
Update your name, email, and profile photo.
Choose which emails and alerts you want to receive.
Manage your plan, payment method, and invoices.
<%= rui_tabs(id: "settings", orientation: :vertical, tablist_position: :start) do |t| %>
<% t.with_tab(id: "profile", label: "Profile", icon: "settings") { "..." } %>
<% t.with_tab(id: "notifications", label: "Notifications", icon: "bell") { "..." } %>
<% t.with_tab(id: "billing", label: "Billing", icon: "credit-card") { "..." } %>
<% end %>
Tablist Position (Vertical)
tablist_position: :end flips which side the tablist sits on with flex-row-reverse, not by reordering the DOM — keyboard focus order always matches DOM order regardless of which side the tablist visually sits on.
Update your name, email, and profile photo.
Choose which emails and alerts you want to receive.
Manage your plan, payment method, and invoices.
<%= rui_tabs(id: "settings", orientation: :vertical, tablist_position: :end) do |t| %>
<%# same tabs, tablist rendered on the right %>
<% end %>
Icons
icon: accepts any Lucide icon name. icon_position: :start (default) or :end places it before or after the label — :left/:right are accepted as aliases and normalized.
icon_position: :start
icon_position: :end
<% t.with_tab(id: "grid", label: "Grid", icon: "layout-grid", icon_position: :start) %>
<% t.with_tab(id: "list", label: "List", icon: "list", icon_position: :end) %>
Badges
badge:/badge_color: is a shortcut for a single trailing counter badge. For more than one — or a leading badge — use the badges: array. Render order: leading badges → icon (:start) → label → icon (:end) → trailing badges → the badge: shortcut (always last).
Shortcut counter badge
Leading + trailing badges
<%# Shortcut — single trailing counter badge %>
<% t.with_tab(id: "inbox", label: "Inbox", badge: 5, badge_color: :danger) %>
<%# Multiple badges — leading + trailing %>
<% t.with_tab(
id: "inbox",
label: "Inbox",
badges: [
{ text: "New", color: :accent, position: :leading },
{ text: 5, color: :danger } # position: defaults to :trailing
]
) %>
Disabled Tab
disabled: true renders aria-disabled="true", applies muted/opacity styling, and is skipped entirely during arrow-key traversal (Home/End skip it too).
Overview panel content.
You won't reach this — the tab is disabled.
Settings panel content.
<% t.with_tab(id: "archived", label: "Archived", disabled: true) %>
Per-Tab Color Override
color: on rui_tabs sets the default for every tab; color: on an individual with_tab overrides it for that one tab only.
Uses the tabs-level color: :primary.
Also :primary.
Overridden to :danger for this tab only.
<%= rui_tabs(id: "t", color: :primary) do |t| %>
<% t.with_tab(id: "overview", label: "Overview") %> <%# :primary %>
<% t.with_tab(id: "alerts", label: "Alerts", color: :danger) %> <%# overridden %>
<% end %>
Mobile Layout (2x2 Grid)
mobile_layout: :grid switches the tablist to a 2-column grid below the md breakpoint, and back to a row from md up (horizontal only). The breakpoint reads actual browser viewport width, not this container's width — resize your browser (or open dev tools' responsive mode) below 768px to see the grid.
Overview panel.
Sale Dates panel.
Become a User panel.
Fees panel.
<%= rui_tabs(id: "listing-type", variant: :spaced, mobile_layout: :grid) do |t| %>
<%# 4 tabs render as a 2x2 grid below md, a single row from md up %>
<% end %>
Nested Tabs
Panel content is arbitrary HTML/ERB/components — nested rui_tabs inside a panel works out of the box; each nested instance gets its own Stimulus controller scope. See the Spaced Variant demo above for the full Become a User example (spaced outer tabs, small spaced inner subtabs in a different color).
<%= rui_tabs(id: "outer", variant: :spaced, color: :primary) do |t| %>
<% t.with_tab(id: "become-a-user", label: "Become a User", icon: "user-plus") do %>
<%# Nested subtabs — smaller, different color, same API %>
<%= rui_tabs(id: "outer-inner", variant: :spaced, size: :sm, color: :secondary) do |st| %>
<% st.with_tab(id: "vendor", label: "Vendor") { "Vendor content" } %>
<% st.with_tab(id: "buyer", label: "Buyer") { "Buyer content" } %>
<% st.with_tab(id: "agency", label: "Agency") { "Agency content" } %>
<% end %>
<% end %>
<% end %>
Deep Linking
deep_link: true reads location.hash on connect (e.g. #fees) and activates the matching tab if the raw id: matches. On every switch it writes the hash back via history.replaceState — no extra back-button history entries, no browser auto-scroll-to-anchor. It also listens for hashchange, so external links into a specific tab work both on load and while the page is already open.
Try switching tabs, then check the URL bar for the hash.
This panel's hash is #fees.
<%= rui_tabs(id: "pricing", deep_link: true) do |t| %>
<% t.with_tab(id: "overview", label: "Overview") { "..." } %>
<% t.with_tab(id: "fees", label: "Fees") { "..." } %>
<% end %>
Manual Activation
activation: :automatic (default) selects a tab as soon as arrow-key focus reaches it. activation: :manual requires an explicit Enter/Space to activate a focused tab — arrow keys only move focus. Manual mode suits panels that are expensive to render or fetch, where you don't want every arrow press to trigger a switch.
Arrow to a tab, then press Enter or Space to activate it.
Fees panel content.
Settings panel content.
<%= rui_tabs(id: "t", activation: :manual) do |t| %>
<%# arrow keys move focus only; Enter/Space activates %>
<% end %>
External Panels (panels: false)
panels: false renders the tablist only — role="tab", aria-selected, roving tabindex, and full keyboard navigation still work out of the box for the triggers themselves.
Verified against v0.52.0 source: when panels: false, aria-controls is omitted from every trigger entirely (there's no built-in panel to point at), and the Stimulus controller's panel targets are scoped to elements inside the tabs root — external panels placed outside it are not auto-toggled. Wire visibility yourself: add aria-controls and a hidden-attribute toggle by hand, or listen for the tabs:change custom event (detail: { tab: <raw id> }) that the controller dispatches on every switch and drive your own panels from it.
Step 1 content, wired up and toggled by hand.
Step 2 content.
<%= rui_tabs(id: "wizard", panels: false) do |t| %>
<% t.with_tab(id: "step-1", label: "Step 1") %>
<% t.with_tab(id: "step-2", label: "Step 2") %>
<% end %>
<div id="wizard-panel-step-1" role="tabpanel" aria-labelledby="wizard-tab-step-1" tabindex="0" data-wizard-panel="step-1">
Step 1 content.
</div>
<div id="wizard-panel-step-2" role="tabpanel" aria-labelledby="wizard-tab-step-2" tabindex="0" data-wizard-panel="step-2" hidden>
Step 2 content.
</div>
<%# aria-controls isn't rendered when panels: false, so drive visibility
from the "tabs:change" event the controller dispatches on every switch %>
<script>
document.getElementById("wizard").addEventListener("tabs:change", (event) => {
document.querySelectorAll("[data-wizard-panel]").forEach((panel) => {
panel.hidden = panel.dataset.wizardPanel !== event.detail.tab
})
})
</script>
Lazy Panel Content
No dedicated lazy-mount API. Put a turbo_frame_tag inside the panel block instead — this is the intended recipe for large or fetched-on-demand panel content.
<% t.with_tab(id: "activity", label: "Activity") do %>
<%= turbo_frame_tag "activity-frame", src: activity_path, loading: :lazy do %>
Loading…
<% end %>
<% end %>
Accessibility
Tabs is our accessibility showcase — the full WAI-ARIA APG tabs pattern is implemented, not approximated.
ARIA contract
| Element | Attribute | Notes |
|---|---|---|
| tablist | role="tablist" | On the wrapping element around every trigger. |
| tablist | aria-orientation="vertical" | Present only when orientation: :vertical; horizontal is the implicit default (omitted). |
| tab | <button type="button" role="tab"> | Never a link — no href:-triggered tabs. |
| tab | aria-selected="true"|"false" | Set explicitly on every tab, including "false" — never omitted. |
| tab | aria-controls | Points at the matching panel's id ("#{id}-panel-#{tab_id}") when panels: true (default). Omitted entirely when panels: false — see External Panels below. |
| tab | id | Unique per tab ("#{id}-tab-#{tab_id}"); referenced by the panel's aria-labelledby. |
| tab | tabindex="0"|"-1" | Roving tabindex — only the active tab is 0; Tab key moves straight from the active tab into its panel. |
| tab | aria-disabled="true" | Set when disabled: true; the tab is also skipped entirely during arrow-key traversal. |
| panel | role="tabpanel" | One per tab, rendered in a wrapper below (horizontal) or beside (vertical) the tablist. |
| panel | aria-labelledby | Points back at the owning tab's id. |
| panel | tabindex="0" | Always present — panels host arbitrary content, so we can't inspect the block server-side to know if it already has a focusable element. |
| panel | hidden | Inactive panels are hidden via the HTML hidden attribute, not a display utility class — no-JS degrades gracefully to the first panel visible. |
Keyboard support — horizontal
| Key | Action |
|---|---|
| Tab | Move focus onto the active tab (roving tabindex); a second Tab moves into the active panel. |
| → Arrow Right | Move focus to the next enabled tab (wraps to the first at the end). |
| ← Arrow Left | Move focus to the previous enabled tab (wraps to the last at the start). |
| Home | Jump to the first enabled tab. |
| End | Jump to the last enabled tab. |
| Enter / Space | Activate the focused tab (only needed in activation: :manual — automatic mode activates on arrow focus). |
Up/Down are never bound in horizontal orientation, so vertical scrolling stays native.
Keyboard support — vertical
| Key | Action |
|---|---|
| Tab | Move focus onto the active tab (roving tabindex); a second Tab moves into the active panel. |
| ↓ Arrow Down | Move focus to the next enabled tab (wraps to the first at the end). |
| ↑ Arrow Up | Move focus to the previous enabled tab (wraps to the last at the start). |
| Home | Jump to the first enabled tab. |
| End | Jump to the last enabled tab. |
| Enter / Space | Activate the focused tab (activation: :manual only). |
Left/Right are never bound in vertical orientation. Which arrow keys move focus is decided in the rendered data-action per orientation — the Stimulus controller never branches on orientation at runtime, so a horizontal tablist can never receive an Up/Down binding and vice versa.
Automatic vs. manual activation
:automatic (default)
Arrow-key focus immediately selects the tab and shows its panel. Matches the APG's "automatic activation" recommendation — best when switching panels is cheap.
:manual
Arrow keys only move focus; the user must press Enter or Space to activate. Use this when activating a tab is expensive (a fetch, a heavy render) so arrowing through tabs doesn't trigger it repeatedly.
Focus visibility
Triggers always ship a visible focus-visible ring (ring-2/ring-offset-0, 700 light / 400 dark) — the same recipe Combobox and Select use. We shipped an invisible focus ring once; never again.
Reduced motion
animation: :fade's opacity transition is fully skipped under prefers-reduced-motion: reduce — the panel still shows instantly on switch, just without the fade. animation: :none disables the transition unconditionally.
API Reference
rui_tabs
WAI-ARIA APG-compliant tabs. Selecting a tab shows its panel; one Stimulus controller (rui--tabs) wires roles, roving tabindex, keyboard nav, and optional deep-linking against server-rendered markup.
| Parameter | Type | Default | Description |
|---|---|---|---|
| id* | String | — | Required — prefix for every tab/panel DOM id ("#{id}-tab-#{tab_id}" / "#{id}-panel-#{tab_id}") |
Appearance
Visual styling options
| Parameter | Type | Default | Description |
|---|---|---|---|
| variant | Symbol |
:underline
|
Visual style
:underline
:pills
:boxed
:spaced
|
| color | Symbol |
:primary
|
Semantic color, resolved via ColorBuilderHelper — sets the default for every tab |
| size | Symbol |
:base
|
Trigger size — scales padding, text, icon, and badge size together
:sm
:base
:lg
|
| divider | Boolean |
nil (variant default)
|
Hairline between tablist and panels. nil resolves to the variant default (:spaced -> true, others -> false) |
| class | String | — | Additional root classes, appended (not tailwind_merge'd — no conflicting-class resolution) |
Orientation & Layout
Horizontal vs. vertical layout, and responsive/full-width behavior
| Parameter | Type | Default | Description |
|---|---|---|---|
| orientation | Symbol |
:horizontal
|
Tablist direction
:horizontal
:vertical
|
| tablist_position | Symbol |
:start
|
Vertical only — which side the tablist sits on, flipped via flex-row-reverse (not DOM reorder)
:start
:end
|
| mobile_layout | Symbol |
:row
|
Horizontal only — :grid renders a 2-column grid below md, a row from md up
:row
:grid
|
| full_width | Boolean |
false
|
Horizontal only — tabs stretch equally via flex-1 |
Active Tab Styling (v0.55.0)
:pills/:spaced only — override the active trigger's solid shade and tint its icon independently of the label
| Parameter | Type | Default | Description |
|---|---|---|---|
| active_shade | Integer |
nil (color's own category default)
|
Override the active trigger's solid bg/hover/border/focus-ring shade — normally fixed at 900 for :primary-style colors, 700 for standard colors, 400 for the bright/:warning family. An invalid value warns (dev/test) and falls back to 900. Focus ring stays capped at 900 regardless of the override. Dark mode is unaffected.
500
600
700
800
900
950
|
| active_icon_color | Symbol | — | Tint the ACTIVE tab's icon a different color than its label text (e.g. a light accent on a dark :spaced solid background). nil (default) means the icon matches the label, unchanged from pre-0.55.0 behavior. Inactive tabs are never affected. |
| active_icon_shade | Integer |
300
|
Shade for active_icon_color:. Ignored unless active_icon_color: is set.
100
200
300
400
500
|
Active & Inactive Background Fill (v0.56.2 / v0.56.4)
:underline/:boxed only — give the active or inactive triggers a light color fill, independent of color:
| Parameter | Type | Default | Description |
|---|---|---|---|
| active_bg_color | Symbol |
nil (variant default)
|
Light color fill for the ACTIVE trigger. nil leaves :underline (no background) and :boxed (plain white/dark:zinc-900) exactly as before. Ignored on :pills/:spaced, which already render their own solid active background. |
| active_bg_shade | Integer |
100
|
Shade for active_bg_color:. Ignored unless active_bg_color: is set.
50
100
200
300
400
|
| inactive_bg_color | Symbol |
nil (variant default)
|
Light color fill for every INACTIVE trigger — the mirror image of active_bg_color:. nil leaves :underline/:boxed exactly as before. Ignored on :spaced (already soft-tinted) and :pills (established transparent-until-hover convention). |
| inactive_bg_shade | Integer |
100
|
Shade for inactive_bg_color:. Ignored unless inactive_bg_color: is set.
50
100
200
300
400
|
Behavior
Activation mode, deep-linking, animation, and panel rendering
| Parameter | Type | Default | Description |
|---|---|---|---|
| activation | Symbol |
:automatic
|
arrow focus selects (:automatic) or requires Enter/Space (:manual)
:automatic
:manual
|
| deep_link | Boolean |
false
|
Read location.hash on connect and write it back (history.replaceState) on every switch; listens for hashchange |
| animation | Symbol |
:fade
|
Panel reveal transition — fully suppressed under prefers-reduced-motion
:fade
:none
|
| panels | Boolean |
true
|
false renders the tablist only — aria-controls is omitted (no built-in panel to point at); wire your own external panel visibility, e.g. via the tabs:change event |
Tab Options (with_tab)
Options for t.with_tab() — one trigger + panel pair
| Parameter | Type | Default | Description |
|---|---|---|---|
| id* | String | — | Required — becomes the tab/panel DOM id suffix |
| label* | String | — | Required — trigger label text |
| icon | String/Symbol | — | Lucide icon name rendered in the trigger |
| icon_position | Symbol |
:start
|
:left/:right accepted as aliases and normalized to :start/:end
:start
:end
|
| active | Boolean |
false
|
Exactly one tab ends up active — the first tab with active: true wins; if none is flagged, the first tab in render order is used |
| disabled | Boolean |
false
|
aria-disabled + skipped entirely during arrow-key traversal (including Home/End) |
| color | Symbol |
nil (inherits tabs-level color:)
|
Per-tab override of the tabs-level color: |
| badge | String/Integer | — | Shortcut — a single trailing counter badge, always rendered last |
| badge_color | Symbol |
:muted
|
Color for the badge: shortcut |
Multiple Badges (badges:)
Array fallback on with_tab for more than one badge, or a leading badge — the block already captures panel content, so a nested renders_many slot isn't available here (see USAGE.md for why)
| Key | Type | Default | Description |
|---|---|---|---|
| text* | String/Integer | — | Badge text — text: or value: both accepted |
| color | Symbol |
:muted
|
Badge color |
| position | Symbol |
:trailing
|
Render before or after the icon+label
:leading
:trailing
|
Slots
Content slots for customizing component parts
| Slot | Description |
|---|---|
| with_tab | One trigger + panel pair. t.with_tab(id:, label:, icon:, icon_position:, active:, disabled:, color:, badge:, badge_color:, badges:) { panel content }. Panel content is arbitrary HTML/ERB/components — nested rui_tabs is supported. |