Accordion
A vertically stacked set of interactive headings that each reveal a section of content.
Usage
Example
Built on native <details> and <summary>: no JavaScript. One item opens at a time.
What is RubyUI?
Can I use it with Rails?
Does it need JavaScript?
Accordion(class: "max-w-lg") do
AccordionItem do
AccordionTrigger { "What is RubyUI?" }
AccordionContent { "RubyUI is a UI component library for Ruby devs who want to build better, faster." }
end
AccordionItem do
AccordionTrigger { "Can I use it with Rails?" }
AccordionContent { "Yes. RubyUI is pure Ruby and works great with Rails. It's a Ruby gem that you can install into your Rails app." }
end
AccordionItem do
AccordionTrigger { "Does it need JavaScript?" }
AccordionContent { "No. The browser opens and closes each item, and find-in-page reveals a closed item that holds the match." }
end
end
Multiple
Pass multiple: true to let several items stay open.
Notifications
Privacy
Billing
Accordion(multiple: true, class: "max-w-lg") do
AccordionItem(open: true) do
AccordionTrigger { "Notifications" }
AccordionContent { "Choose how you want to be notified about updates." }
end
AccordionItem(open: true) do
AccordionTrigger { "Privacy" }
AccordionContent { "Control who can see your profile and activity." }
end
AccordionItem do
AccordionTrigger { "Billing" }
AccordionContent { "Manage your plan and payment methods." }
end
end
Disabled
A disabled item can't be clicked and leaves the tab order. Pass disabled: true to the Accordion to disable every item. Without JavaScript, a screen reader can still open it.
Available
Unavailable
Accordion(class: "max-w-lg") do
AccordionItem do
AccordionTrigger { "Available" }
AccordionContent { "This item opens as usual." }
end
AccordionItem(disabled: true) do
AccordionTrigger { "Unavailable" }
AccordionContent { "This item is disabled." }
end
end
Heading level
Each trigger is a heading (h3 by default). Pick the level that fits the page outline.
Shipping
Returns
Accordion(class: "max-w-lg") do
AccordionItem do
AccordionTrigger(level: 2) { "Shipping" }
AccordionContent { "Orders ship within two business days." }
end
AccordionItem do
AccordionTrigger(level: 2) { "Returns" }
AccordionContent { "Returns are accepted within 30 days." }
end
end
Installation
Using RubyUI CLI
Run the install command
rails g ruby_ui:component Accordion
Manual installation
1
Add RubyUI::Accordion to app/components/ruby_ui/accordion/accordion.rb
# frozen_string_literal: true
require "securerandom"
module RubyUI
# Groups AccordionItems. Single-open by default: every item shares the same
# <details name>, so the browser closes the others when one opens.
class Accordion < Base
def initialize(multiple: false, disabled: false, **attrs)
@multiple = multiple
@disabled = disabled
super(**attrs)
end
def view_template(&)
previous = context[:ruby_ui_accordion]
context[:ruby_ui_accordion] = {
name: (@multiple ? nil : "ruby-ui-accordion-#{SecureRandom.hex(4)}"),
disabled: @disabled
}
div(**attrs, &)
ensure
context[:ruby_ui_accordion] = previous
end
private
def default_attrs
{
data: {slot: "accordion"},
class: "flex w-full flex-col"
}
end
end
end
2
Add RubyUI::AccordionContent to app/components/ruby_ui/accordion/accordion_content.rb
# frozen_string_literal: true
module RubyUI
# The panel of an AccordionItem. Attributes and classes go to the inner
# element, like shadcn's AccordionContent.
class AccordionContent < Base
def view_template(&)
div(data: {slot: "accordion-content"}, class: "text-sm") do
div(**attrs, &)
end
end
private
def default_attrs
{
class: "pt-0 pb-4 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4"
}
end
end
end
3
Add RubyUI::AccordionDocs to app/components/ruby_ui/accordion/accordion_docs.rb
# frozen_string_literal: true
class Views::Docs::Accordion < Views::Base
def view_template
div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
component = "Accordion"
render Docs::Header.new(title: component,
description: "A vertically stacked set of interactive headings that each reveal a section of content.")
Heading(level: 2) { "Usage" }
render Docs::VisualCodeExample.new(title: "Example", description: "Built on native <details> and <summary>: no JavaScript. One item opens at a time.", context: self) do
<<~RUBY
Accordion(class: "max-w-lg") do
AccordionItem do
AccordionTrigger { "What is RubyUI?" }
AccordionContent { "RubyUI is a UI component library for Ruby devs who want to build better, faster." }
end
AccordionItem do
AccordionTrigger { "Can I use it with Rails?" }
AccordionContent { "Yes. RubyUI is pure Ruby and works great with Rails. It's a Ruby gem that you can install into your Rails app." }
end
AccordionItem do
AccordionTrigger { "Does it need JavaScript?" }
AccordionContent { "No. The browser opens and closes each item, and find-in-page reveals a closed item that holds the match." }
end
end
RUBY
end
render Docs::VisualCodeExample.new(title: "Multiple", description: "Pass multiple: true to let several items stay open.", context: self) do
<<~RUBY
Accordion(multiple: true, class: "max-w-lg") do
AccordionItem(open: true) do
AccordionTrigger { "Notifications" }
AccordionContent { "Choose how you want to be notified about updates." }
end
AccordionItem(open: true) do
AccordionTrigger { "Privacy" }
AccordionContent { "Control who can see your profile and activity." }
end
AccordionItem do
AccordionTrigger { "Billing" }
AccordionContent { "Manage your plan and payment methods." }
end
end
RUBY
end
render Docs::VisualCodeExample.new(title: "Disabled", description: "A disabled item can't be clicked and leaves the tab order. Pass disabled: true to the Accordion to disable every item. Without JavaScript, a screen reader can still open it.", context: self) do
<<~RUBY
Accordion(class: "max-w-lg") do
AccordionItem do
AccordionTrigger { "Available" }
AccordionContent { "This item opens as usual." }
end
AccordionItem(disabled: true) do
AccordionTrigger { "Unavailable" }
AccordionContent { "This item is disabled." }
end
end
RUBY
end
render Docs::VisualCodeExample.new(title: "Heading level", description: "Each trigger is a heading (h3 by default). Pick the level that fits the page outline.", context: self) do
<<~RUBY
Accordion(class: "max-w-lg") do
AccordionItem do
AccordionTrigger(level: 2) { "Shipping" }
AccordionContent { "Orders ship within two business days." }
end
AccordionItem do
AccordionTrigger(level: 2) { "Returns" }
AccordionContent { "Returns are accepted within 30 days." }
end
end
RUBY
end
render Components::ComponentSetup::Tabs.new(component_name: component)
render Docs::ComponentsTable.new(component_files(component))
end
end
end
4
Add RubyUI::AccordionIcon to app/components/ruby_ui/accordion/accordion_icon.rb
# frozen_string_literal: true
module RubyUI
# Chevron down while closed, chevron up while open (lucide icons, swapped
# with CSS on the parent <details open>). Pass a block to render your own.
class AccordionIcon < Base
CHEVRONS = {down: "m6 9 6 6 6-6", up: "m18 15-6-6-6 6"}.freeze
def view_template(&block)
if block
span(**attrs, &block)
else
chevron(:down, "group-open/accordion-item:hidden")
chevron(:up, "hidden group-open/accordion-item:inline")
end
end
private
def chevron(direction, visibility)
svg(
**mix(attrs, class: visibility),
xmlns: "http://www.w3.org/2000/svg",
viewbox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
stroke_width: "2",
stroke_linecap: "round",
stroke_linejoin: "round"
) { |s| s.path(d: CHEVRONS[direction]) }
end
def default_attrs
{
aria: {hidden: "true"},
data: {slot: "accordion-trigger-icon"},
class: "pointer-events-none ml-auto size-4 shrink-0 text-muted-foreground"
}
end
end
end
5
Add RubyUI::AccordionItem to app/components/ruby_ui/accordion/accordion_item.rb
# frozen_string_literal: true
module RubyUI
# A native <details>. Open/close, the single-open group (name) and
# find-in-page come from the browser; the height animates on
# ::details-content where supported.
class AccordionItem < Base
def initialize(open: false, disabled: nil, **attrs)
@open = open
@disabled = disabled
super(**attrs)
end
def view_template(&)
group = context[:ruby_ui_accordion] || {}
disabled = @disabled.nil? ? group[:disabled] : @disabled
previous = context[:ruby_ui_accordion_item]
context[:ruby_ui_accordion_item] = {disabled: disabled}
item_attrs = attrs.dup
item_attrs[:name] ||= group[:name] if group[:name]
item_attrs[:open] = true if @open
details(**item_attrs, &)
ensure
context[:ruby_ui_accordion_item] = previous
end
private
def default_attrs
{
data: {slot: "accordion-item"},
class: [
"group/accordion-item not-last:border-b [interpolate-size:allow-keywords]",
"details-content:h-0 details-content:overflow-hidden open:details-content:h-auto",
"details-content:transition-[height,content-visibility] details-content:transition-discrete",
"details-content:duration-200 details-content:ease-out"
].join(" ")
}
end
end
end
6
Add RubyUI::AccordionTrigger to app/components/ruby_ui/accordion/accordion_trigger.rb
# frozen_string_literal: true
module RubyUI
# The <summary> of an AccordionItem. Wraps its content in a heading
# (h3 by default) and appends the chevron (icon: false to render your own).
class AccordionTrigger < Base
LEVELS = (1..6)
def initialize(level: 3, icon: true, **attrs)
raise ArgumentError, "level must be 1..6, got #{level.inspect}" unless level.is_a?(Integer) && LEVELS.cover?(level)
@level = level
@icon = icon
super(**attrs)
end
def view_template(&block)
item = context[:ruby_ui_accordion_item] || {}
trigger_attrs = item[:disabled] ? mix(attrs, aria: {disabled: "true"}, tabindex: "-1") : attrs
summary(**trigger_attrs) do
send(:"h#{@level}", &block)
render AccordionIcon.new if @icon
end
end
private
def default_attrs
{
data: {slot: "accordion-trigger"},
class: [
"group/accordion-trigger relative flex flex-1 cursor-pointer list-none items-start justify-between",
"rounded-md border border-transparent py-4 text-left text-sm font-medium transition-all outline-none",
"hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
"aria-disabled:pointer-events-none aria-disabled:opacity-50 [&::-webkit-details-marker]:hidden"
].join(" ")
}
end
end
end
Components
| Component | Built using | Source |
|---|---|---|
Accordion | Phlex | |
AccordionContent | Phlex | |
AccordionDocs | Phlex | |
AccordionIcon | Phlex | |
AccordionItem | Phlex | |
AccordionTrigger | Phlex |