Sidebar
UI::SidebarA collapsible application sidebar with groups and menus.
# VelvetKit
## Sidebar (`UI::Sidebar`)
A collapsible application sidebar with groups and menus.
Add it: `bin/rails generate velvetkit:add sidebar`
Also adds: `icon`, `spinner`, `button`
Stimulus: `ui--sidebar`
### Arguments
```ruby
UI::Sidebar.new(collapsible: :none, variant: :sidebar, side: :left, state: :expanded, **system_args)
```
Slots: `with_sidebar_header`, `with_sidebar_content`, `with_sidebar_footer`, `with_sidebar_group`, `with_label`, `with_menu`, `with_menu_item`, `with_menu_button`
### Example: Default
```erb
<% brand = render("ui/sidebar_preview/brand") %>
<% main = capture do %>
<p class="text-muted-foreground">Main content area next to the sidebar.</p>
<% end %>
<%= render "ui/sidebar_preview/shell",
sidebar_id: "preview-sidebar-default",
brand: brand,
main: main,
groups: [
{ label: "Applications", items: [
{ icon: "layout-dashboard", text: "Dashboard", active: true, icon_class: "stroke-orange-500" },
{ icon: "users", text: "Team", icon_class: "stroke-purple-500" },
{ icon: "file-text", text: "Reports" }
] }
] %>
```
### Example: With Groups
```erb
<% brand = render("ui/sidebar_preview/brand") %>
<% main = capture do %>
<p class="text-muted-foreground">Two labelled groups sharing one sidebar.</p>
<% end %>
<%= render "ui/sidebar_preview/shell",
sidebar_id: "preview-sidebar-groups",
brand: brand,
main: main,
groups: [
{ label: "Navigation", items: [
{ icon: "home", text: "Home", active: true },
{ icon: "layout-dashboard", text: "Dashboard" },
{ icon: "mail", text: "Messages" }
] },
{ label: "Settings", items: [
{ icon: "settings", text: "General" },
{ icon: "shield", text: "Security" },
{ icon: "bell", text: "Notifications" }
] }
] %>
```
### Example: With Dropdown Header
```erb
<% brand = capture do %>
<%= render(UI::Menu.new) do |menu| %>
<% menu.with_trigger(variant: :ghost, class: "w-full justify-start px-2 h-auto py-1.5 group-data-[collapsible=icon]:size-8 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:p-0") do %>
<img src="/velvetkit/mark-light.svg" alt="" class="size-8 max-w-none shrink-0 object-contain dark:hidden"><img src="/velvetkit/mark-dark.svg" alt="" class="hidden size-8 max-w-none shrink-0 object-contain dark:block">
<div class="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
<span class="truncate font-medium">VelvetKit</span>
<span class="truncate text-xs text-muted-foreground">Workspace</span>
</div>
<%= render(UI::Icon.new(name: "chevrons-up-down", size: :sm, class: "ml-auto group-data-[collapsible=icon]:hidden")) %>
<% end %>
<% menu.with_content(side: :right, align: :start, class: "w-56") do |content| %>
<% content.with_label("Workspaces") %>
<% content.with_item("VelvetKit") %>
<% content.with_item("Marketing site") %>
<% content.with_item("Internal tools") %>
<% end %>
<% end %>
<% end %>
<% main = capture do %>
<p class="text-muted-foreground">Sidebar with a dropdown menu inside the header for context switching.</p>
<% end %>
<%= render "ui/sidebar_preview/shell",
sidebar_id: "preview-sidebar-dropdown",
brand: brand,
main: main,
groups: [
{ label: "Applications", items: [
{ icon: "layout-dashboard", text: "Dashboard", active: true, icon_class: "stroke-orange-500" },
{ icon: "users", text: "Team", icon_class: "stroke-purple-500" }
] }
] %>
```
### Example: Collapsible
```erb
<% brand = render("ui/sidebar_preview/brand") %>
<% main = capture do %>
<div class="space-y-6">
<div>
<h2 class="text-lg font-semibold mb-2">Collapsible Sidebar</h2>
<p class="text-sm text-muted-foreground">Click the toggle button to collapse the sidebar to icon-only mode. State is persisted in localStorage.</p>
</div>
<%= render(UI::Tabs.new(default_value: "overview")) do |tabs| %>
<% tabs.with_list do |list| %>
<% list.with_trigger(value: "overview") { "Overview" } %>
<% list.with_trigger(value: "analytics") { "Analytics" } %>
<% list.with_trigger(value: "reports") { "Reports" } %>
<% end %>
<% tabs.with_panel(value: "overview") do %>
<div class="space-y-3">
<p class="text-sm text-muted-foreground">Project overview with key metrics and recent activity.</p>
<%= render(UI::Menu.new) do |menu| %>
<% menu.with_trigger(variant: :outline, size: :sm) { "Actions" } %>
<% menu.with_content do |content| %>
<% content.with_label("Options") %>
<% content.with_item("Edit project") %>
<% content.with_item("Share") %>
<% content.with_divider %>
<% content.with_item("Delete", variant: :destructive) %>
<% end %>
<% end %>
</div>
<% end %>
<% tabs.with_panel(value: "analytics") do %>
<p class="text-sm text-muted-foreground">Traffic and conversion analytics over the last 30 days.</p>
<% end %>
<% tabs.with_panel(value: "reports") do %>
<p class="text-sm text-muted-foreground">Generated reports and scheduled exports.</p>
<% end %>
<% end %>
</div>
<% end %>
<%= render "ui/sidebar_preview/shell",
sidebar_id: "preview-sidebar-collapsible",
collapsible: :icon,
brand: brand,
main: main,
groups: [
{ label: "Platform", items: [
{ icon: "layout-dashboard", text: "Dashboard", active: true },
{ icon: "bar-chart-3", text: "Analytics" },
{ icon: "folder-kanban", text: "Projects" }
] },
{ label: "Account", items: [
{ icon: "user", text: "Profile" },
{ icon: "credit-card", text: "Billing" }
] }
] %>
```
bin/rails generate velvetkit:add sidebar
- Stimulus
- ui--sidebar
- Installs
- Nothing
Examples
Default
default.html.erb
<% brand = render("ui/sidebar_preview/brand") %>
<% main = capture do %>
<p class="text-muted-foreground">Main content area next to the sidebar.</p>
<% end %>
<%= render "ui/sidebar_preview/shell",
sidebar_id: "preview-sidebar-default",
brand: brand,
main: main,
groups: [
{ label: "Applications", items: [
{ icon: "layout-dashboard", text: "Dashboard", active: true, icon_class: "stroke-orange-500" },
{ icon: "users", text: "Team", icon_class: "stroke-purple-500" },
{ icon: "file-text", text: "Reports" }
] }
] %>
With Groups
with_groups.html.erb
<% brand = render("ui/sidebar_preview/brand") %>
<% main = capture do %>
<p class="text-muted-foreground">Two labelled groups sharing one sidebar.</p>
<% end %>
<%= render "ui/sidebar_preview/shell",
sidebar_id: "preview-sidebar-groups",
brand: brand,
main: main,
groups: [
{ label: "Navigation", items: [
{ icon: "home", text: "Home", active: true },
{ icon: "layout-dashboard", text: "Dashboard" },
{ icon: "mail", text: "Messages" }
] },
{ label: "Settings", items: [
{ icon: "settings", text: "General" },
{ icon: "shield", text: "Security" },
{ icon: "bell", text: "Notifications" }
] }
] %>
With Dropdown Header
with_dropdown_header.html.erb
<% brand = capture do %>
<%= render(UI::Menu.new) do |menu| %>
<% menu.with_trigger(variant: :ghost, class: "w-full justify-start px-2 h-auto py-1.5 group-data-[collapsible=icon]:size-8 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:p-0") do %>
<img src="/velvetkit/mark-light.svg" alt="" class="size-8 max-w-none shrink-0 object-contain dark:hidden"><img src="/velvetkit/mark-dark.svg" alt="" class="hidden size-8 max-w-none shrink-0 object-contain dark:block">
<div class="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
<span class="truncate font-medium">VelvetKit</span>
<span class="truncate text-xs text-muted-foreground">Workspace</span>
</div>
<%= render(UI::Icon.new(name: "chevrons-up-down", size: :sm, class: "ml-auto group-data-[collapsible=icon]:hidden")) %>
<% end %>
<% menu.with_content(side: :right, align: :start, class: "w-56") do |content| %>
<% content.with_label("Workspaces") %>
<% content.with_item("VelvetKit") %>
<% content.with_item("Marketing site") %>
<% content.with_item("Internal tools") %>
<% end %>
<% end %>
<% end %>
<% main = capture do %>
<p class="text-muted-foreground">Sidebar with a dropdown menu inside the header for context switching.</p>
<% end %>
<%= render "ui/sidebar_preview/shell",
sidebar_id: "preview-sidebar-dropdown",
brand: brand,
main: main,
groups: [
{ label: "Applications", items: [
{ icon: "layout-dashboard", text: "Dashboard", active: true, icon_class: "stroke-orange-500" },
{ icon: "users", text: "Team", icon_class: "stroke-purple-500" }
] }
] %>
Collapsible
collapsible.html.erb
<% brand = render("ui/sidebar_preview/brand") %>
<% main = capture do %>
<div class="space-y-6">
<div>
<h2 class="text-lg font-semibold mb-2">Collapsible Sidebar</h2>
<p class="text-sm text-muted-foreground">Click the toggle button to collapse the sidebar to icon-only mode. State is persisted in localStorage.</p>
</div>
<%= render(UI::Tabs.new(default_value: "overview")) do |tabs| %>
<% tabs.with_list do |list| %>
<% list.with_trigger(value: "overview") { "Overview" } %>
<% list.with_trigger(value: "analytics") { "Analytics" } %>
<% list.with_trigger(value: "reports") { "Reports" } %>
<% end %>
<% tabs.with_panel(value: "overview") do %>
<div class="space-y-3">
<p class="text-sm text-muted-foreground">Project overview with key metrics and recent activity.</p>
<%= render(UI::Menu.new) do |menu| %>
<% menu.with_trigger(variant: :outline, size: :sm) { "Actions" } %>
<% menu.with_content do |content| %>
<% content.with_label("Options") %>
<% content.with_item("Edit project") %>
<% content.with_item("Share") %>
<% content.with_divider %>
<% content.with_item("Delete", variant: :destructive) %>
<% end %>
<% end %>
</div>
<% end %>
<% tabs.with_panel(value: "analytics") do %>
<p class="text-sm text-muted-foreground">Traffic and conversion analytics over the last 30 days.</p>
<% end %>
<% tabs.with_panel(value: "reports") do %>
<p class="text-sm text-muted-foreground">Generated reports and scheduled exports.</p>
<% end %>
<% end %>
</div>
<% end %>
<%= render "ui/sidebar_preview/shell",
sidebar_id: "preview-sidebar-collapsible",
collapsible: :icon,
brand: brand,
main: main,
groups: [
{ label: "Platform", items: [
{ icon: "layout-dashboard", text: "Dashboard", active: true },
{ icon: "bar-chart-3", text: "Analytics" },
{ icon: "folder-kanban", text: "Projects" }
] },
{ label: "Account", items: [
{ icon: "user", text: "Profile" },
{ icon: "credit-card", text: "Billing" }
] }
] %>
API
Arguments
UI::Sidebar.new(collapsible: :none, variant: :sidebar, side: :left, state: :expanded, **system_args)
Slots:
with_sidebar_header
with_sidebar_content
with_sidebar_footer
with_sidebar_group
with_label
with_menu
with_menu_item
with_menu_button