ComponentsThemes
Palette
Default
Stone
Rose
Blue
Green
Violet
Yellow
Orange
6

← Components

Toggle Group

Group of toggle buttons with single or multiple selection.

Installation

bin/rails g wabi:add toggle_group
bin/importmap pin @zag-js/toggle-group
bin/importmap pin @zag-js/vanilla

Pin @zag-js/toggle-group and @zag-js/vanilla at version 1.41+ using the +esm jsdelivr URLs — bin/importmap pin only downloads the main entry and leaves submodule imports unresolved.

Example

Source

app/components/ui/toggle_group.rb

# frozen_string_literal: true

require "json"
require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class ToggleGroup < Wabi::Base
      def initialize(type: :single, value: nil, name: nil, disabled: false, **attrs)
        @type     = type
        @value    = Array(value).compact
        @name     = name
        @disabled = disabled
        @attrs    = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        div(
          **@attrs,
          role: "group",
          data: {
            controller: "wabi--toggle-group",
            "wabi--toggle-group-multiple-value": (@type == :multiple).to_s,
            "wabi--toggle-group-value-value":    @value.to_json,
            "wabi--toggle-group-name-value":     @name,
            "wabi--toggle-group-disabled-value": @disabled.to_s,
          },
          class: merge_class("inline-flex items-center gap-1 rounded-md", user_class),
          &block
        )
      end
    end
  end
end

app/components/ui/toggle_group_item.rb

# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class ToggleGroupItem < Wabi::Base
      ITEM_CLASS = "inline-flex items-center justify-center rounded-md text-sm font-medium " \
                   "h-9 px-2.5 transition-colors motion-reduce:transition-none " \
                   "hover:bg-muted hover:text-muted-foreground " \
                   "focus-visible:outline-none focus-visible:ring-2 " \
                   "focus-visible:ring-ring focus-visible:ring-offset-2 " \
                   "disabled:pointer-events-none disabled:opacity-50 " \
                   "data-[state=on]:bg-accent data-[state=on]:text-accent-foreground"

      def initialize(value:, disabled: false, **attrs)
        @value    = value
        @disabled = disabled
        @attrs    = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        button(
          type: "button",
          **@attrs,
          "data-state": "off",
          data: {
            "wabi--toggle-group-target": "item",
            "wabi-value": @value,
            "wabi-disabled": @disabled.to_s,
          },
          class: merge_class(ITEM_CLASS, user_class)
        ) do
          yield if block_given?
        end
      end
    end
  end
end

Accessibility

  • role="group" wrapper + role="button" per item.
  • Arrow keys move focus between items. Space toggles the focused item.
  • type: :single enforces one selection; type: :multiple allows many.
  • Hidden <input type="hidden"> emitted as `name` (single) or `name[]` (multiple) for form submission.