Splitter
Resizable side-by-side panels with draggable gutters; horizontal or vertical.
Installation
bin/rails g wabi:add splitterExample
Panel A
Panel B
# Splitter fills its parent (Zag sets height:100%), so wrap it in a sized box.
div(class: "h-48 overflow-hidden rounded-md border border-input") do
render Components::UI::Splitter.new(panels: [{ id: "a", minSize: 20 }, { id: "b", minSize: 20 }]) do
render Components::UI::SplitterPanel.new(id: "a", class: "grid place-items-center text-sm") { "Panel A" }
render Components::UI::SplitterResizeTrigger.new(id: "a:b")
render Components::UI::SplitterPanel.new(id: "b", class: "grid place-items-center text-sm") { "Panel B" }
end
end
Vertical orientation
Pass orientation: :vertical to stack panels top-to-bottom; the resize trigger then drags up/down.
Top
Bottom
div(class: "h-64 overflow-hidden rounded-md border border-input") do
render Components::UI::Splitter.new(orientation: :vertical, panels: [{ id: "top", minSize: 20 }, { id: "bottom", minSize: 20 }]) do
render Components::UI::SplitterPanel.new(id: "top", class: "grid place-items-center text-sm") { "Top" }
render Components::UI::SplitterResizeTrigger.new(id: "top:bottom", aria_label: "Resize panels")
render Components::UI::SplitterPanel.new(id: "bottom", class: "grid place-items-center text-sm") { "Bottom" }
end
end
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.