ComponentsThemes
Palette
Default
Stone
Rose
Blue
Green
Violet
Yellow
Orange
6

← Components

Splitter

Resizable side-by-side panels with draggable gutters; horizontal or vertical.

Installation

bin/rails g wabi:add splitter

Example

Vertical orientation

Pass orientation: :vertical to stack panels top-to-bottom; the resize trigger then drags up/down.

Source

app/components/ui/splitter.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 Splitter < Wabi::Base
      # panels: array of hashes, e.g. [{ id: "a", minSize: 20 }, { id: "b" }]
      # default_size: optional array of numbers (sizes per panel, summing to 100)
      def initialize(panels:, orientation: :horizontal, default_size: nil, **attrs)
        @panels       = panels
        @orientation  = orientation
        @default_size = default_size
        @attrs        = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        data = {
          controller: "wabi--splitter",
          "wabi--splitter-panels-value":      @panels.to_json,
          "wabi--splitter-orientation-value": @orientation.to_s,
        }
        data["wabi--splitter-default-size-value"] = @default_size.to_json if @default_size

        # Zag's getRootProps sets display:flex, flex-direction, width/height:100%
        # and overflow:hidden inline, so the root needs no layout classes — the
        # Splitter fills its (sized) parent. user_class is for cosmetics (border,
        # rounded) that Zag does not override.
        div(**@attrs, data: data, class: merge_class(user_class)) do
          yield if block_given?
        end
      end
    end
  end
end

app/components/ui/splitter_panel.rb

# frozen_string_literal: true

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

module Components
  module UI
    class SplitterPanel < Wabi::Base
      def initialize(id:, **attrs)
        @id    = id
        @attrs = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        div(
          data: { "wabi--splitter-target": "panel", "wabi-id": @id.to_s },
          class: merge_class("overflow-auto", user_class)
        ) do
          yield if block_given?
        end
      end
    end
  end
end

app/components/ui/splitter_resize_trigger.rb

# frozen_string_literal: true

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

module Components
  module UI
    class SplitterResizeTrigger < Wabi::Base
      # id is the "<beforePanelId>:<afterPanelId>" pair Zag expects, e.g. "a:b".
      # aria_label: optional accessible name for the resize handle (e.g. "Resize sidebar").
      # The controller reads data-wabi-label at runtime and forwards it as aria-label,
      # falling back to "Resize panels" when nil.
      def initialize(id:, aria_label: nil, **attrs)
        @id         = id
        @aria_label = aria_label
        @attrs      = attrs
      end

      def view_template
        user_class = @attrs.delete(:class)
        data = { "wabi--splitter-target": "resizeTrigger", "wabi-id": @id.to_s }
        data[:"wabi-label"] = @aria_label if @aria_label
        div(
          data: data,
          class: merge_class(
            "group relative flex items-center justify-center bg-border transition-colors motion-reduce:transition-none " \
            "hover:bg-primary/40 data-[focus]:bg-primary/40 " \
            "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 " \
            "data-[orientation=horizontal]:w-1.5 data-[orientation=horizontal]:cursor-col-resize " \
            "data-[orientation=vertical]:h-1.5 data-[orientation=vertical]:cursor-row-resize",
            user_class
          )
        )
      end
    end
  end
end

Accessibility

  • Each gutter is role="separator" with aria-valuenow; arrow keys resize, Home/End jump to min/max.
  • Set min/max sizes per panel via the panels: config.