Docs
Components
Accordion

Accordion

A vertically stacked set of interactive headings that each reveal a section of content.

Yes. It adheres to the WAI-ARIA design pattern.
Yes. It comes with default styles that matches the other components' aesthetic.
Yes. It's animated by default, but you can disable it if you prefer.
<%= accordion(type: 'single', collapsible: true, class: 'w-full') do %>
  <%= accordion_item(value: 'item-1') do %>
    <%= accordion_trigger do %>
      Is it accessible?
    <% end %>
    <%= accordion_content do %>
      Yes. It adheres to the WAI-ARIA design pattern.
    <% end %>
  <% end %>
  <%= accordion_item(value: 'item-2') do %>
    <%= accordion_trigger do %>
      Is it styled?
    <% end %>
    <%= accordion_content do %>
      Yes. It comes with default styles that matches the other components' aesthetic.
    <% end %>
  <% end %>
  <%= accordion_item(value: 'item-3') do %>
    <%= accordion_trigger do %>
      Is it animated?
    <% end %>
    <%= accordion_content do %>
      Yes. It's animated by default, but you can disable it if you prefer.
    <% end %>
  <% end %>
<% end %>

Installation

1

Copy and paste the following code into your project.

Create a new file in app/components/ui/accordion_component.rb and paste the following code:
# frozen_string_literal: true

module Ui
  class AccordionComponent < ViewComponent::Base
    def initialize(type: 'single', collapsible: true, class_name: nil, **options)
      super
      @type = type
      @collapsible = collapsible
      @class_name = class_name
      @options = options
    end

    def call
      tag.div(class: accordion_classes, data: accordion_data, **@options) do
        content
      end
    end

    private

    def accordion_classes
      "#{@class_name}"
    end

    def accordion_data
      {
        controller: 'accordion',
        accordion_type_value: @type,
        accordion_collapsible_value: @collapsible
      }
    end
  end

  class AccordionItemComponent < ViewComponent::Base
    def initialize(value:, default_open: false, class_name: nil, **options)
      super
      @value = value
      @default_open = default_open
      @class_name = class_name
      @options = options
    end

    def call
      tag.div(class: item_classes, data: item_data, **@options) do
        content
      end
    end

    private

    def item_classes
      "flex flex-col border-b border-border #{@class_name}"
    end

    def item_data
      {
        accordion_target: 'item',
        value: @value,
        state: @default_open ? 'open' : 'closed',
        accordion_default_open_value: @default_open
      }
    end
  end

  class AccordionTriggerComponent < ViewComponent::Base
    def initialize(class_name: nil, **options)
      super
      @class_name = class_name
      @options = options
    end

    def call
      tag.button(class: trigger_classes, data: trigger_data, **@options) do
        safe_join([
                    content,
                    chevron_icon
                  ])
      end
    end

    private

    def trigger_classes
      "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180 #{@class_name}"
    end

    def trigger_data
      {
        accordion_target: 'trigger',
        action: 'click->accordion#toggle'
      }
    end

    def chevron_icon
      tag.svg(xmlns: 'http://www.w3.org/2000/svg', width: '24', height: '24', viewBox: '0 0 24 24', fill: 'none',
              stroke: 'currentColor', stroke_width: '2', stroke_linecap: 'round', stroke_linejoin: 'round', class: 'h-4 w-4 shrink-0 transition-transform duration-200') do
        tag.polyline(points: '6 9 12 15 18 9')
      end
    end
  end

  class AccordionContentComponent < ViewComponent::Base
    def initialize(class_name: nil, **options)
      super
      @class_name = class_name
      @options = options
    end

    def call
      tag.div(class: content_classes, data: content_data, style: content_style, **@options) do
        tag.div(class: 'pb-4 pt-0') do
          content
        end
      end
    end

    private

    def content_classes
      "overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down #{@class_name}"
    end

    def content_data
      {
        accordion_target: 'content'
      }
    end

    def content_style
      'max-height: 0px;'
    end
  end
end
2

Update tailwind.config.js

Add the following animations to your tailwind.config.js file:
module.exports = {
  theme: {
    extend: {
      keyframes: {
        'accordion-down': {
          from: { height: '0' },
          to: { height: 'var(--radix-accordion-content-height)' },
        },
        'accordion-up': {
          from: { height: 'var(--radix-accordion-content-height)' },
          to: { height: '0' },
        },
      },
      animation: {
        'accordion-down': 'accordion-down 0.2s ease-out',
        'accordion-up': 'accordion-up 0.2s ease-out',
      },
    },
  },
};
Coming soon.

Usage

<%= accordion(type: 'single', collapsible: true, class: 'w-full') do %>
  <%= accordion_item(value: 'item-1') do %>
    <%= accordion_trigger do %>
      Is it accessible?
    <% end %>
    <%= accordion_content do %>
      Yes. It adheres to the WAI-ARIA design pattern.
    <% end %>
  <% end %>
  <%= accordion_item(value: 'item-2') do %>
    <%= accordion_trigger do %>
      Is it styled?
    <% end %>
    <%= accordion_content do %>
      Yes. It comes with default styles that matches the other components' aesthetic.
    <% end %>
  <% end %>
  <%= accordion_item(value: 'item-3') do %>
    <%= accordion_trigger do %>
      Is it animated?
    <% end %>
    <%= accordion_content do %>
      Yes. It's animated by default, but you can disable it if you prefer.
    <% end %>
  <% end %>
<% end %>