Senren UI
Components / Accordion

Accordion

Stable Stimulus: senren--accordion

FAQ

Add the gem, run senren:install, then copy components with senren:add.

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/.../accordion_example.html.erb
<div class="w-full max-w-lg">
  <%= render Senren::AccordionComponent.new(items: [
    { id: "install", title: "How do I install Senren?", content: "Add the gem, run senren:install, then copy components with senren:add." },
    { id: "runtime", title: "Does Senren use React?", content: "No. Senren is Rails-native: ViewComponent, Turbo, Stimulus, and Tailwind." },
    { id: "ownership", title: "Who owns copied code?", content: "The host app owns copied components and can edit them directly." }
  ]) %>
</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 accordion --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 accordion --client

At a glance #

Category Navigation
Class name Senren::AccordionComponent
Stimulus senren--accordion
Variants single, multiple
Depends on
Pairs with card

Source #

app/components/senren/accordion_component.html.erb
<div <%= tag.attributes(**root_attrs("divide-y divide-[hsl(var(--senren-border))] rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-background))]", data: { controller: "senren--accordion", "senren--accordion-multiple-value": multiple? })) %>>
  <% items.each_with_index do |item, index| %>
    <% expanded = open_item?(item, index) %>
    <section>
      <h3>
        <button type="button" class="flex w-full cursor-pointer items-center justify-between gap-4 px-4 py-3 text-left text-sm font-medium text-[hsl(var(--senren-foreground))] transition-colors hover:bg-[hsl(var(--senren-muted)/0.5)]" aria-expanded="<%= expanded %>" aria-controls="<%= item[:id] %>-panel" data-senren--accordion-target="trigger" data-panel-id="<%= item[:id] %>" data-action="click->senren--accordion#toggle">
          <span><%= item[:title] %></span>
          <span aria-hidden="true" class="text-[hsl(var(--senren-muted-foreground))]">+</span>
        </button>
      </h3>
      <div id="<%= item[:id] %>-panel" data-senren--accordion-target="panel" data-panel-id="<%= item[:id] %>" <%= "hidden" unless expanded %> class="px-4 pb-4 text-sm leading-6 text-[hsl(var(--senren-muted-foreground))]">
        <%= item[:content].presence || content %>
      </div>
    </section>
  <% end %>
</div>
app/components/senren/accordion_component.rb
module Senren
  class AccordionComponent < BaseComponent
    VARIANTS = { single: '', multiple: '' }.freeze
    SIZES = { md: '' }.freeze

    def initialize(items: [], variant: :single, open: nil, class_name: nil, **html)
      super(variant: variant, size: :md, class_name: class_name, **html)
      @items = normalize_items(items)
      @open = Array(open).map(&:to_s)
    end

    attr_reader :items, :open

    def multiple? = variant == :multiple

    def open_item?(item, index)
      open.include?(item[:id]) || (open.empty? && index.zero?)
    end

    private

    def normalize_items(items)
      Array(items).map.with_index do |item, index|
        source = item.is_a?(Hash) ? item : { title: item.to_s }
        title = source[:title] || source['title'] || "Section #{index + 1}"
        id = (source[:id] || source['id'] || title.to_s.parameterize).to_s
        { id: id, title: title, content: source[:content] || source['content'] }
      end
    end
  end
end

AI agent rules #

Use for

  • +FAQ
  • +collapsible content sections

Avoid

  • -primary navigation

Accessibility #

  • Buttons toggle aria-expanded; panels use aria-hidden.