Sidebar

A composable, collapsible navigation panel wired to a Stimulus controller. Build app shells with a persistent left (or right) drawer that collapses off-canvas.

Installation

bin/rails g shadcnrb:component sidebar

Note

Because this docs page is itself rendered inside a sidebar layout, embedding a full sidebar_wrapper in the preview would create a nested layout conflict. Instead, the individual building blocks are shown below. For a full working demo, see the sidebar playground.

Usage — individual pieces

Child parts are reached via the proxy yielded by sui.sidebar_wrapper do |s|. For isolated demos outside a wrapper, sui.sidebar_proxy returns the same proxy without rendering the shell. menu_button mirrors Rails link_to — pass a name + path and the active state is auto-detected via current_page?.

<!-- default -->
<%= s.menu_button "Dashboard", "#" %>
<!-- active: true -->
<%= s.menu_button "Active item", "#", active: true %>
<!-- icon: :home -->
<%= s.menu_button "Home", "#", icon: :home %>

For anything beyond a plain link — button_to with method: :delete, Turbo options, nested icons — pass a block. The wrapper renders a styled <div role="button"> and your Rails helper fills the row.

Platform

<!-- menu_button (block form with button_to) -->
<div class="w-64 bg-sidebar rounded-md p-2">
  <%= s.menu_button do %>
    <%= button_to "Sign out", "#", method: :delete %>
  <% end %>
</div>
<!-- group_label -->
<div class="w-48 bg-sidebar rounded-md">
  <%= s.group_label "Platform" %>
</div>
<!-- separator -->
<div class="w-48 bg-sidebar rounded-md py-2">
  <%= s.separator %>
</div>

Sub-menu flyout

A collapsible group loses its menu_sub when the rail collapses to icons. s.collapsible flyout: true keeps the nested pages reachable: on the icon rail, hovering (or focusing) the menu button opens the same list as a panel to its right, headed by the button's label. Expanded, the group behaves as before. The click is inert while the flyout is on, so the inline open state survives a collapse and expand. Try it collapsed in the playground or the Nav + Sidebar block.

s.collapsible open: true, flyout: true do |c|
  s.menu_item do
    s.menu_button "Docs", collapsible: true, icon: :folder do
      c.chevron
    end
  end

  c.content do
    s.menu_sub do
      s.menu_sub_item { s.menu_sub_button "Introduction", "#" }
      s.menu_sub_item { s.menu_sub_button "Installation", "#" }
    end
  end
end

Full structure (code only)

Assemble these methods to build a complete sidebar shell. The sidebar_wrapper owns the Stimulus controller; s.trigger and s.rail both dispatch the toggle action.

sui.sidebar_wrapper do |s|
  s.sidebar do
    s.header do
      tag.span "My App", class: "font-semibold text-sm px-2"
    end
    s.content do
      s.group do
        s.group_label "Platform"
        s.group_content do
          s.menu do
            s.menu_item do
              s.menu_button "Dashboard", root_path
            end
            s.menu_item do
              s.menu_button "Settings", settings_path
            end
          end
        end
      end
      s.separator
      s.group do
        s.group_label "Account"
        s.group_content do
          s.menu do
            s.menu_item do
              s.menu_button "Profile", profile_path
            end
          end
        end
      end
    end
    s.footer do
      tag.span "Footer content"
    end
    s.rail
  end
  s.inset do
    tag.header(class: "flex items-center gap-2 p-4") do
      s.trigger
    end
    tag.div(class: "p-4") do
      tag.p "Main content area"
    end
  end
end

Persisting on the server

The cookie is the default store. To keep the choice with the user's account, pass the stored value to the helper and listen for the change event — see State persistence.

API

methodsignaturedescription
sui.sidebar_wrapper(state:, bounded:, **opts, &block)Outermost div. Mounts the Stimulus sidebar controller and sets CSS custom properties for width. Renders state: (:expanded / :collapsed), else the sidebar_state cookie, so a collapsed sidebar never flashes expanded. Each desktop toggle dispatches shadcnrb--sidebar--component:change with { state }. Yields a proxy s.
sui.sidebar_proxy—Returns a bare proxy for rendering individual pieces outside a wrapper (e.g. docs previews).
s.sidebar(side:, variant:, collapsible:, **opts, &block)The sidebar panel itself. side: is :left (default) or :right.
s.header(**opts, &block)Flex column at the top of the sidebar, typically for a logo or app name.
s.content(**opts, &block)Scrollable flex column that fills the remaining vertical space.
s.group(**opts, &block)A labelled section inside sidebar content, padded on all sides.
s.group_label(name = nil, **opts, &block)Small muted heading above a group of links.
s.group_content(**opts, &block)Wrapper div for the group's menu list.
s.menu(**opts, &block)Unordered list (ul) that holds menu items with gap spacing.
s.menu_item(**opts, &block)List item (li) wrapper for a single menu button.
s.menu_button(name, options, active: nil, size: :default, icon: nil, **html_opts, &block)An anchor styled as a sidebar row. Mirrors link_to. Auto-sets active via current_page?; pass active: true/false to override. size: is :sm, :default, :md, or :lg.
s.collapsible(open:, flyout: false, content:, **opts, &block)sui.collapsible for a sub-menu. With flyout: true the content also opens on hover as a panel to the right of its menu_button while the rail is collapsed to icons — the same menu_sub, written once, headed by the button's label. On the rail the click is inert and the inline open state is kept.
s.separator(**opts)Horizontal rule styled with sidebar border colour, with side margins.
s.footer(**opts, &block)Flex column pinned to the bottom of the sidebar.
s.inset(**opts, &block)The main content area (main) that sits beside the sidebar, grows to fill remaining space.
s.trigger(**opts)Icon button that dispatches shadcnrb--sidebar--component#toggle on click to expand/collapse the sidebar.
s.rail(**opts)Narrow invisible hit-area along the sidebar edge — also toggles on click, visible as a faint line on hover.