Tabs
Stable Stimulus:senren--tabs
switching between related views in the same context
Usage example #
Copy this ERB into a Rails view after installing the component. The snippet below is the same code used by the live preview above.
app/views/.../tabs_example.html.erb
<div class="w-full max-w-lg">
<%= render Senren::TabsComponent.new(items: [
{ id: "overview", label: "Overview", content: "Track project health, activity, and pending work in one place." },
{ id: "activity", label: "Activity", content: "Recent updates and team decisions appear here." },
{ id: "settings", label: "Settings", content: "Tune notifications, visibility, and access rules." }
]) %>
</div>
Install this component #
Copy the official component into your app
This component requires Stimulus. Keep --client so the controller is copied with the ViewComponent.
Terminal
bin/rails senren:add tabs --client
Create a custom component with the same conventions
Use this when you need an app-specific component that follows Senren's ViewComponent and Stimulus structure. --client is required for this behavior.
Terminal
bin/rails generate senren:component tabs --client
At a glance #
Category
Navigation
Class name
Senren::TabsComponent
Stimulus
senren--tabs
Variants
default, underline
Depends on
—
Pairs with
card
Source #
app/components/senren/tabs_component.html.erb
<div <%= tag.attributes(**root_attrs("w-full", data: { controller: "senren--tabs" })) %>>
<div role="tablist" aria-label="<%= label %>" class="<%= self.class::VARIANTS[variant] %> flex flex-wrap items-center gap-1">
<% items.each do |item| %>
<% selected = active_item?(item) %>
<button type="button" role="tab" id="<%= item[:id] %>-tab" aria-selected="<%= selected %>" aria-controls="<%= item[:id] %>-panel" tabindex="<%= selected ? 0 : -1 %>" data-senren--tabs-target="tab" data-panel-id="<%= item[:id] %>" data-state="<%= selected ? "active" : "inactive" %>" data-action="click->senren--tabs#select keydown->senren--tabs#onKey" class="cursor-pointer rounded-[calc(var(--senren-radius)-2px)] px-3 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--senren-ring))] text-[hsl(var(--senren-muted-foreground))] data-[state=active]:bg-[hsl(var(--senren-background))] data-[state=active]:text-[hsl(var(--senren-foreground))] data-[state=active]:shadow-sm data-[state=inactive]:hover:text-[hsl(var(--senren-foreground))]">
<%= item[:label] %>
</button>
<% end %>
</div>
<div class="mt-4">
<% items.each do |item| %>
<% selected = active_item?(item) %>
<section id="<%= item[:id] %>-panel" role="tabpanel" aria-labelledby="<%= item[:id] %>-tab" data-senren--tabs-target="panel" data-panel-id="<%= item[:id] %>" data-state="<%= selected ? "active" : "inactive" %>" <%= "hidden" unless selected %> class="rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))] p-4 text-sm text-[hsl(var(--senren-card-foreground))]">
<%= item[:content].presence || content %>
</section>
<% end %>
</div>
</div>
app/components/senren/tabs_component.rb
# frozen_string_literal: true
module Senren
class TabsComponent < BaseComponent
VARIANTS = {
default: 'rounded-(--senren-radius) bg-[hsl(var(--senren-muted))] p-1',
underline: 'border-b border-[hsl(var(--senren-border))]'
}.freeze
SIZES = { md: '' }.freeze
def initialize(items: [], variant: :default, active: nil, label: 'Tabs', class_name: nil, **html)
super(variant: variant, size: :md, class_name: class_name, **html)
@items = normalize_items(items)
@active = active&.to_s || @items.first&.fetch(:id, nil)
@label = label
end
attr_reader :items, :active, :label
def active_item?(item)
item[:id].to_s == active.to_s
end
private
def normalize_items(items)
Array(items).map.with_index do |item, index|
source = item.is_a?(Hash) ? item : { label: item.to_s }
label = source[:label] || source['label'] || "Tab #{index + 1}"
id = (source[:id] || source['id'] || label.to_s.parameterize).to_s
{ id: id, label: label, content: source[:content] || source['content'] }
end
end
end
end
AI agent rules #
Use for
- +switching between related views in the same context
Avoid
- -non-related sections - use a separate page
Accessibility #
- role=tablist with arrow-key navigation and aria-controls.