Sidebar
A composable, themeable and customizable sidebar component.
Usage
Example
SidebarWrapper do Sidebar(collapsible: :icon) do SidebarHeader do SidebarMenu do SidebarMenuItem do SidebarMenuButton(as: :a, href: "#") do search_icon() span { "Search" } end end SidebarMenuItem do SidebarMenuButton(as: :a, href: "#", active: true) do home_icon() span { "Home" } end end SidebarMenuItem do SidebarMenuButton(as: :a, href: "#") do inbox_icon() span { "Inbox" } SidebarMenuBadge { 4 } end end end end SidebarContent do SidebarGroup do SidebarGroupLabel { "Favorites" } SidebarMenu do FAVORITES.each do |item| SidebarMenuItem do SidebarMenuButton(as: :a, href: "#") do span { item[:emoji] } span { item[:name] } end DropdownMenu(options: { strategy: "fixed", placement: "right-start" }) do SidebarMenuAction( data: { ruby_ui__dropdown_menu_target: "trigger", action: "click->ruby-ui--dropdown-menu#toggle" } ) do ellipsis_icon() span(class: "sr-only") { "More" } end DropdownMenuContent do DropdownMenuItem(href: '#') { "Profile" } DropdownMenuItem(href: '#') { "Billing" } DropdownMenuItem(href: '#') { "Team" } DropdownMenuItem(href: '#') { "Subscription" } end end end end end end SidebarGroup do SidebarGroupLabel { "Workspaces" } SidebarMenu do WORKSPACES.each do |item| SidebarMenuItem do SidebarMenuButton(as: :a, href: "#") do span { item[:emoji] } span { item[:name] } end DropdownMenu() do SidebarMenuAction( data: { ruby_ui__dropdown_menu_target: "trigger", action: "click->ruby-ui--dropdown-menu#toggle" } ) do ellipsis_icon() span(class: "sr-only") { "More" } end DropdownMenuContent do DropdownMenuItem(href: '#') { "Profile" } DropdownMenuItem(href: '#') { "Billing" } DropdownMenuItem(href: '#') { "Team" } DropdownMenuItem(href: '#') { "Subscription" } end end end end end end SidebarGroup(class: "mt-auto") do SidebarGroupContent do SidebarMenu do nav_secondary.each do |item| SidebarMenuItem do SidebarMenuButton(as: :a, href: "#") do item[:icon].call span { item[:label] } end end end end end end end SidebarRail() end SidebarInset do header(class: "flex h-16 shrink-0 items-center gap-2 border-b px-4") do SidebarTrigger(class: "-ml-1") end end end
Inset variant
SidebarWrapper do Sidebar(variant: :inset) do SidebarHeader do SidebarMenu do SidebarMenuItem do SidebarMenuButton(as: :a, href: "#") do search_icon() span { "Search" } end end SidebarMenuItem do SidebarMenuButton(as: :a, href: "#", active: true) do home_icon() span { "Home" } end end SidebarMenuItem do SidebarMenuButton(as: :a, href: "#") do inbox_icon() span { "Inbox" } SidebarMenuBadge { 4 } end end end end SidebarContent do SidebarGroup do SidebarGroupLabel { "Favorites" } SidebarMenu do FAVORITES.each do |item| SidebarMenuItem do SidebarMenuButton(as: :a, href: "#") do span { item[:emoji] } span { item[:name] } end DropdownMenu(options: { strategy: "fixed", placement: "right-start" }) do SidebarMenuAction( data: { ruby_ui__dropdown_menu_target: "trigger", action: "click->ruby-ui--dropdown-menu#toggle" } ) do ellipsis_icon() span(class: "sr-only") { "More" } end DropdownMenuContent do DropdownMenuItem(href: '#') { "Profile" } DropdownMenuItem(href: '#') { "Billing" } DropdownMenuItem(href: '#') { "Team" } DropdownMenuItem(href: '#') { "Subscription" } end end end end end end SidebarGroup do SidebarGroupLabel { "Workspaces" } SidebarMenu do WORKSPACES.each do |item| SidebarMenuItem do SidebarMenuButton(as: :a, href: "#") do span { item[:emoji] } span { item[:name] } end DropdownMenu() do SidebarMenuAction( data: { ruby_ui__dropdown_menu_target: "trigger", action: "click->ruby-ui--dropdown-menu#toggle" } ) do ellipsis_icon() span(class: "sr-only") { "More" } end DropdownMenuContent do DropdownMenuItem(href: '#') { "Profile" } DropdownMenuItem(href: '#') { "Billing" } DropdownMenuItem(href: '#') { "Team" } DropdownMenuItem(href: '#') { "Subscription" } end end end end end end SidebarGroup(class: "mt-auto") do SidebarGroupContent do SidebarMenu do nav_secondary.each do |item| SidebarMenuItem do SidebarMenuButton(as: :a, href: "#") do item[:icon].call span { item[:label] } end end end end end end end SidebarRail() end SidebarInset do header(class: "flex h-16 shrink-0 items-center gap-2 border-b px-4") do SidebarTrigger(class: "-ml-1") end end end
Dialog variant
Dialog(data: {action: "ruby-ui--dialog:connect->ruby-ui--dialog#open"}) do DialogTrigger do Button { "Open Dialog" } end DialogContent(class: "grid overflow-hidden p-0 md:max-h-[500px] md:max-w-[700px] lg:max-w-[800px]") do SidebarWrapper(class: "items-start") do Sidebar(collapsible: :none, class: "hidden md:flex") do SidebarContent do SidebarGroup do SidebarGroupContent do SidebarMenu do SidebarMenuItem do SidebarMenuButton(as: :a, href: "#") do search_icon() span { "Search" } end end SidebarMenuItem do SidebarMenuButton(as: :a, href: "#", active: true) do home_icon() span { "Home" } end end SidebarMenuItem do SidebarMenuButton(as: :a, href: "#") do inbox_icon() span { "Inbox" } end end end end end end end main(class: "flex h-[480px] flex-1 flex-col overflow-hidden") do end end end end
Installation
Using RubyUI CLI
Run the install command
rails g ruby_ui:component Sidebar
Manual installation
1
Add RubyUI::CollapsibleSidebar to app/components/ruby_ui/sidebar/collapsible_sidebar.rb
# frozen_string_literal: true module RubyUI class CollapsibleSidebar < Base def initialize(side: :left, variant: :sidebar, collapsible: :offcanvas, open: true, **attrs) @side = side @variant = variant @collapsible = collapsible @open = open super(**attrs) end def view_template(&) MobileSidebar(side: @side, **attrs, &) div(**mix(sidebar_attrs, attrs)) do div(**gap_element_attrs) div(**content_wrapper_attrs) do div(**content_attrs, &) end end end private def sidebar_attrs { class: "group peer hidden text-sidebar-foreground md:block", data: { state: @open ? "expanded" : "collapsed", collapsible: @open ? "" : @collapsible, variant: @variant, side: @side, collapsible_kind: @collapsible, ruby_ui__sidebar_target: "sidebar" } } end def gap_element_attrs { class: [ "relative w-[var(--sidebar-width)] bg-transparent transition-[width]", "duration-200 ease-linear", "group-data-[collapsible=offcanvas]:w-0", "group-data-[side=right]:rotate-180", variant_classes ] } end def content_wrapper_attrs { class: [ "fixed inset-y-0 z-10 hidden h-svh w-[var(--sidebar-width)]", "transition-[left,right,width] duration-200 ease-linear md:flex", content_wrapper_side_classes, content_wrapper_variant_classes ] } end def content_attrs { class: [ "flex h-full w-full flex-col bg-sidebar", "group-data-[variant=floating]:rounded-lg", "group-data-[variant=floating]:border", "group-data-[variant=floating]:border-sidebar-border", "group-data-[variant=floating]:shadow" ], data: { sidebar: "sidebar" } } end def variant_classes if %i[floating inset].include?(@variant) "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]" else "group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)]" end end def content_wrapper_side_classes return "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" if @side == :left "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]" end def content_wrapper_variant_classes if %i[floating inset].include?(@variant) "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]" else "group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)] group-data-[side=left]:border-r group-data-[side=right]:border-l" end end end end
2
Add RubyUI::MobileSidebar to app/components/ruby_ui/sidebar/mobile_sidebar.rb
# frozen_string_literal: true module RubyUI class MobileSidebar < Base SIDEBAR_WIDTH_MOBILE = "18rem" def initialize(side: :left, **attrs) @side = side super(**attrs) end def view_template(&) Sheet(**attrs) do SheetContent( side: @side, class: "w-[var(--sidebar-width)] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden", style: { "--sidebar-width": SIDEBAR_WIDTH_MOBILE }, data: { sidebar: "sidebar", mobile: "true" } ) do SheetHeader(class: "sr-only") do SheetTitle { "Sidebar" } SheetDescription { "Displays the mobile sidebar." } end div(class: "flex h-full w-full flex-col", &) end end end private def default_attrs { data: { ruby_ui__sidebar_target: "mobileSidebar", action: "ruby--ui-sidebar:open->ruby-ui--sheet#open:self" } } end end end
3
Add RubyUI::NonCollapsibleSidebar to app/components/ruby_ui/sidebar/non_collapsible_sidebar.rb
# frozen_string_literal: true module RubyUI class NonCollapsibleSidebar < Base def view_template(&) div(**attrs, &) end private def default_attrs { class: "flex h-full w-[var(--sidebar-width)] flex-col bg-sidebar text-sidebar-foreground" } end end end
4
Add RubyUI::Sidebar to app/components/ruby_ui/sidebar/sidebar.rb
# frozen_string_literal: true module RubyUI class Sidebar < Base SIDES = %i[left right].freeze VARIANTS = %i[sidebar floating inset].freeze COLLAPSIBLES = %i[offcanvas icon none].freeze def initialize(side: :left, variant: :sidebar, collapsible: :offcanvas, open: true, **attrs) raise ArgumentError, "Invalid side: #{side}." unless SIDES.include?(side.to_sym) raise ArgumentError "Invalid variant: #{variant}." unless VARIANTS.include?(variant.to_sym) raise ArgumentError, "Invalid collapsible: #{collapsible}." unless COLLAPSIBLES.include?(collapsible.to_sym) @side = side.to_sym @variant = variant.to_sym @collapsible = collapsible.to_sym @open = open super(**attrs) end def view_template(&) if @collapsible == :none NonCollapsibleSidebar(**attrs, &) else CollapsibleSidebar(side: @side, variant: @variant, collapsible: @collapsible, open: @open, **attrs, &) end end end end
5
Add RubyUI::SidebarContent to app/components/ruby_ui/sidebar/sidebar_content.rb
# frozen_string_literal: true module RubyUI class SidebarContent < Base def view_template(&) div(**attrs, &) end private def default_attrs { class: "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", data: { sidebar: "content" } } end end end
6
Add RubyUI::SidebarFooter to app/components/ruby_ui/sidebar/sidebar_footer.rb
# frozen_string_literal: true module RubyUI class SidebarFooter < Base def view_template(&) div(**attrs, &) end private def default_attrs { class: "flex flex-col gap-2 p-2", data: { sidebar: "footer" } } end end end
7
Add RubyUI::SidebarGroup to app/components/ruby_ui/sidebar/sidebar_group.rb
# frozen_string_literal: true module RubyUI class SidebarGroup < Base def view_template(&) div(**attrs, &) end private def default_attrs { class: "relative flex w-full min-w-0 flex-col p-2", data: { sidebar: "group" } } end end end
8
Add RubyUI::SidebarGroupAction to app/components/ruby_ui/sidebar/sidebar_group_action.rb
# frozen_string_literal: true module RubyUI class SidebarGroupAction < Base def initialize(as: :button, **attrs) @as = as super(**attrs) end def view_template(&) tag(@as, **attrs, &) end private def default_attrs { class: [ "absolute right-3 top-3.5 flex aspect-square w-5 items-center", "justify-center rounded-md p-0 text-sidebar-foreground", "outline-none ring-sidebar-ring transition-transform", "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", "focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", "after:absolute after:-inset-2 after:md:hidden", "group-data-[collapsible=icon]:hidden" ], data: { sidebar: "group-action" } } end end end
9
Add RubyUI::SidebarGroupContent to app/components/ruby_ui/sidebar/sidebar_group_content.rb
# frozen_string_literal: true module RubyUI class SidebarGroupContent < Base def view_template(&) div(**attrs, &) end private def default_attrs { class: "w-full text-sm", data: { sidebar: "group-content" } } end end end
10
Add RubyUI::SidebarGroupLabel to app/components/ruby_ui/sidebar/sidebar_group_label.rb
# frozen_string_literal: true module RubyUI class SidebarGroupLabel < Base def view_template(&) div(**attrs, &) end private def default_attrs { class: [ "flex h-8 shrink-0 items-center rounded-md px-2 text-xs", "font-medium text-sidebar-foreground/70 outline-none", "ring-sidebar-ring transition-[margin,opacity] duration-200", "ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0" ], data: { sidebar: "group-label" } } end end end
11
Add RubyUI::SidebarHeader to app/components/ruby_ui/sidebar/sidebar_header.rb
# frozen_string_literal: true module RubyUI class SidebarHeader < Base def view_template(&) div(**attrs, &) end private def default_attrs { class: "flex flex-col gap-2 p-2", data: { sidebar: "header" } } end end end
12
Add RubyUI::SidebarInput to app/components/ruby_ui/sidebar/sidebar_input.rb
# frozen_string_literal: true module RubyUI class SidebarInput < Base def view_template(&) Input(**attrs, &) end private def default_attrs { class: "h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring", data: { sidebar: "input" } } end end end
13
Add RubyUI::SidebarInset to app/components/ruby_ui/sidebar/sidebar_inset.rb
# frozen_string_literal: true module RubyUI class SidebarInset < Base def view_template(&) main(**attrs, &) end private def default_attrs { class: [ "relative flex w-full flex-1 flex-col bg-background", "md:peer-data-[variant=inset]:m-2", "md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2", "md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl", "md:peer-data-[variant=inset]:shadow" ] } end end end
14
Add RubyUI::SidebarMenu to app/components/ruby_ui/sidebar/sidebar_menu.rb
# frozen_string_literal: true module RubyUI class SidebarMenu < Base def view_template(&) ul(**attrs, &) end private def default_attrs { class: "flex w-full min-w-0 flex-col gap-1", data: { sidebar: "menu" } } end end end
15
Add RubyUI::SidebarMenuAction to app/components/ruby_ui/sidebar/sidebar_menu_action.rb
# frozen_string_literal: true module RubyUI class SidebarMenuAction < Base def initialize(as: :button, show_on_hover: false, **attrs) @as = as super(**attrs) end def view_template(&) tag(@as, **attrs, &) end private def default_attrs { class: [ "absolute right-1 top-1.5 flex aspect-square w-5 items-center", "justify-center rounded-md p-0 text-sidebar-foreground outline-none", "ring-sidebar-ring transition-transform hover:bg-sidebar-accent", "hover:text-sidebar-accent-foreground focus-visible:ring-2", "peer-hover/menu-button:text-sidebar-accent-foreground", "[&>svg]:size-4 [&>svg]:shrink-0", "after:absolute after:-inset-2 after:md:hidden", "peer-data-[size=sm]/menu-button:top-1", "peer-data-[size=default]/menu-button:top-1.5", "peer-data-[size=lg]/menu-button:top-2.5", "group-data-[collapsible=icon]:hidden", show_on_hover_classes ], data: { sidebar: "menu-action" } } end def show_on_hover_classes return unless @show_on_hover [ "group-focus-within/menu-item:opacity-100", "group-hover/menu-item:opacity-100 data-[state=open]:opacity-100", "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0" ].join(" ") end end end
16
Add RubyUI::SidebarMenuBadge to app/components/ruby_ui/sidebar/sidebar_menu_badge.rb
# frozen_string_literal: true module RubyUI class SidebarMenuBadge < Base def view_template(&) div(**attrs, &) end private def default_attrs { class: [ "pointer-events-none absolute right-1 flex h-5 min-w-5 select-none", "items-center justify-center rounded-md px-1 text-xs font-medium", "tabular-nums text-sidebar-foreground", "peer-hover/menu-button:text-sidebar-accent-foreground", "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground", "peer-data-[size=sm]/menu-button:top-1", "peer-data-[size=default]/menu-button:top-1.5", "peer-data-[size=lg]/menu-button:top-2.5", "group-data-[collapsible=icon]:hidden" ], data: { sidebar: "menu-badge" } } end end end
17
Add RubyUI::SidebarMenuButton to app/components/ruby_ui/sidebar/sidebar_menu_button.rb
# frozen_string_literal: true module RubyUI class SidebarMenuButton < Base VARIANT_CLASSES = { default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", outline: "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]" }.freeze SIZE_CLASSES = { default: "h-8 text-sm", sm: "h-7 text-xs", lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0" }.freeze def initialize(as: :button, variant: :default, size: :default, active: false, **attrs) raise ArgumentError, "Invalid variant: #{variant}" unless VARIANT_CLASSES.key?(variant) raise ArgumentError, "Invalid size: #{size}" unless SIZE_CLASSES.key?(size) @as = as @variant = variant @size = size @active = active super(**attrs) end def view_template(&) tag(@as, **attrs, &) end private def default_attrs { class: [ "peer/menu-button flex w-full items-center gap-2 overflow-hidden", "rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring", "transition-[width,height,padding] hover:bg-sidebar-accent", "hover:text-sidebar-accent-foreground focus-visible:ring-2", "active:bg-sidebar-accent active:text-sidebar-accent-foreground", "disabled:pointer-events-none disabled:opacity-50", "group-has-[[data-sidebar=menu-action]]/menu-item:pr-8", "aria-disabled:pointer-events-none aria-disabled:opacity-50", "data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium", "data-[active=true]:text-sidebar-accent-foreground", "data-[state=open]:hover:bg-sidebar-accent", "data-[state=open]:hover:text-sidebar-accent-foreground", "group-data-[collapsible=icon]:!size-8", "group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate", "[&>svg]:size-4 [&>svg]:shrink-0", VARIANT_CLASSES[@variant], SIZE_CLASSES[@size] ], data: { sidebar: "menu-button", size: @size, active: @active.to_s } } end end end
18
Add RubyUI::SidebarMenuItem to app/components/ruby_ui/sidebar/sidebar_menu_item.rb
# frozen_string_literal: true module RubyUI class SidebarMenuItem < Base def view_template(&) ul(**attrs, &) end private def default_attrs { class: "group/menu-item relative", data: { sidebar: "menu-item" } } end end end
19
Add RubyUI::SidebarMenuSkeleton to app/components/ruby_ui/sidebar/sidebar_menu_skeleton.rb
# frozen_string_literal: true module RubyUI class SidebarMenuSkeleton < Base def initialize(show_icon: false, **attrs) @show_icon = show_icon super(**attrs) end def view_template(&) div(**attrs) do Skeleton(class: "size-4 rounded-md", data: {sidebar: "menu-skeleton-icon"}) if @show_icon Skeleton( class: "h-4 max-w-[var(--skeleton-width)] flex-1", data: {sidebar: "menu-skeleton-text"}, style: {"--skeleton-width" => "#{skeleton_width}%"} ) end end private def default_attrs { class: "flex h-8 items-center gap-2 rounded-md px-2", data: { sidebar: "menu-skeleton" } } end def skeleton_width @_skeleton_width ||= rand(50..89) end end end
20
Add RubyUI::SidebarMenuSub to app/components/ruby_ui/sidebar/sidebar_menu_sub.rb
# frozen_string_literal: true module RubyUI class SidebarMenuSub < Base def view_template(&) ul(**attrs, &) end private def default_attrs { class: [ "mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l", "border-sidebar-border px-2.5 py-0.5", "group-data-[collapsible=icon]:hidden" ], data: { sidebar: "menu-sub" } } end end end
21
Add RubyUI::SidebarMenuSubButton to app/components/ruby_ui/sidebar/sidebar_menu_sub_button.rb
# frozen_string_literal: true module RubyUI class SidebarMenuSubButton < Base SIZE_CLASSES = { sm: "text-xs", md: "text-sm" }.freeze def initialize(as: :button, size: :md, active: false, **attrs) raise ArgumentError, "Invalid size: #{size}" unless SIZE_CLASSES.key?(size) @as = as @size = size @active = active super(**attrs) end def view_template(&) tag(@as, **attrs, &) end private def default_attrs { class: [ "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden", "rounded-md px-2 text-sidebar-foreground outline-none", "ring-sidebar-ring hover:bg-sidebar-accent", "hover:text-sidebar-accent-foreground focus-visible:ring-2", "active:bg-sidebar-accent active:text-sidebar-accent-foreground", "disabled:pointer-events-none disabled:opacity-50", "aria-disabled:pointer-events-none aria-disabled:opacity-50", "[&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", "[&>svg]:text-sidebar-accent-foreground", "data-[active=true]:bg-sidebar-accent", "data-[active=true]:text-sidebar-accent-foreground", "group-data-[collapsible=icon]:hidden", SIZE_CLASSES[@size] ], data: { sidebar: "menu-sub-button", size: @size, active: @active.to_s } } end end end
22
Add RubyUI::SidebarMenuSubItem to app/components/ruby_ui/sidebar/sidebar_menu_sub_item.rb
# frozen_string_literal: true module RubyUI class SidebarMenuSubItem < Base def view_template(&) li(**attrs, &) end end end
23
Add RubyUI::SidebarRail to app/components/ruby_ui/sidebar/sidebar_rail.rb
# frozen_string_literal: true module RubyUI class SidebarRail < Base def view_template(&) button(**attrs, &) end private def default_attrs { class: [ "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all", "ease-linear after:absolute after:inset-y-0 after:left-1/2", "after:w-[2px] hover:after:bg-sidebar-border", "group-data-[side=left]:-right-4 group-data-[side=right]:left-0", "sm:flex [[data-side=left]_&]:cursor-w-resize", "[[data-side=right]_&]:cursor-e-resize", "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize", "[[data-side=right][data-state=collapsed]_&]:cursor-w-resize", "group-data-[collapsible=offcanvas]:translate-x-0", "group-data-[collapsible=offcanvas]:after:left-full", "group-data-[collapsible=offcanvas]:hover:bg-sidebar", "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2" ], data: { sidebar: "rail", tabindex: "-1", action: "click->ruby-ui--sidebar#toggle" } } end end end
24
Add RubyUI::SidebarSeparator to app/components/ruby_ui/sidebar/sidebar_separator.rb
# frozen_string_literal: true module RubyUI class SidebarSeparator < Base def view_template(&) Separator(**attrs, &) end private def default_attrs { class: "mx-2 w-auto bg-sidebar-border", data: { sidebar: "separator" } } end end end
25
Add RubyUI::SidebarTrigger to app/components/ruby_ui/sidebar/sidebar_trigger.rb
# frozen_string_literal: true module RubyUI class SidebarTrigger < Base def view_template(&) Button(variant: :ghost, size: :icon, **attrs) do panel_left_icon span(class: "sr-only") { "Toggle Sidebar" } end end private def default_attrs { class: "h-7 w-7 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", data: { sidebar: "trigger", action: "click->ruby-ui--sidebar#toggle" } } end def panel_left_icon 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: "lucide lucide-panel-left" ) do |s| s.rect(width: "18", height: "18", x: "3", y: "3", rx: "2") s.path(d: "M9 3v18") end end end end
26
Add RubyUI::SidebarWrapper to app/components/ruby_ui/sidebar/sidebar_wrapper.rb
# frozen_string_literal: true module RubyUI class SidebarWrapper < Base SIDEBAR_WIDTH = "16rem" SIDEBAR_WIDTH_ICON = "3rem" def view_template(&) div(**attrs, &) end private def default_attrs { class: "group/sidebar-wrapper [&:has([data-variant=inset])]:bg-sidebar flex min-h-svh w-full", style: "--sidebar-width: #{SIDEBAR_WIDTH}; --sidebar-width-icon: #{SIDEBAR_WIDTH_ICON};", data: { controller: "ruby-ui--sidebar" } } end end end
27
Add sidebar_controller.js to app/javascript/controllers/ruby_ui/sidebar_controller.js
import { Controller } from "@hotwired/stimulus"; const SIDEBAR_COOKIE_NAME = "sidebar_state"; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; const State = { EXPANDED: "expanded", COLLAPSED: "collapsed", }; const MOBILE_BREAKPOINT = 768; export default class extends Controller { static targets = ["sidebar", "mobileSidebar"]; sidebarTargetConnected() { const { state, collapsibleKind } = this.sidebarTarget.dataset; this.open = state === State.EXPANDED; this.collapsibleKind = collapsibleKind; } toggle(e) { e.preventDefault(); if (this.#isMobile()) { this.#openMobileSidebar(); return; } this.open = !this.open; this.onToggle(); } onToggle() { this.#updateSidebarState(); this.#persistSidebarState(); } #updateSidebarState() { if (!this.hasSidebarTarget) { return; } const { dataset } = this.sidebarTarget; dataset.state = this.open ? State.EXPANDED : State.COLLAPSED; dataset.collapsible = this.open ? "" : this.collapsibleKind; } #persistSidebarState() { document.cookie = `${SIDEBAR_COOKIE_NAME}=${this.open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; } #isMobile() { return window.innerWidth < MOBILE_BREAKPOINT; } #openMobileSidebar() { if (!this.hasMobileSidebarTarget) { return; } this.mobileSidebarTarget.dispatchEvent( new CustomEvent("ruby--ui-sidebar:open"), ); } }
28
Update the Stimulus controllers manifest file
Importmap!
rake stimulus:manifest:update
Components
| Component | Built using | Source |
|---|---|---|
CollapsibleSidebar | Phlex | |
MobileSidebar | Phlex | |
NonCollapsibleSidebar | Phlex | |
Sidebar | Phlex | |
SidebarContent | Phlex | |
SidebarFooter | Phlex | |
SidebarGroup | Phlex | |
SidebarGroupAction | Phlex | |
SidebarGroupContent | Phlex | |
SidebarGroupLabel | Phlex | |
SidebarHeader | Phlex | |
SidebarInput | Phlex | |
SidebarInset | Phlex | |
SidebarMenu | Phlex | |
SidebarMenuAction | Phlex | |
SidebarMenuBadge | Phlex | |
SidebarMenuButton | Phlex | |
SidebarMenuItem | Phlex | |
SidebarMenuSkeleton | Phlex | |
SidebarMenuSub | Phlex | |
SidebarMenuSubButton | Phlex | |
SidebarMenuSubItem | Phlex | |
SidebarRail | Phlex | |
SidebarSeparator | Phlex | |
SidebarTrigger | Phlex | |
SidebarWrapper | Phlex | |
SidebarController | Stimulus JS |