Sidebar
Stable Stimulus:senren--sidebar
primary app navigation in dashboards
Usage example #
Copy this ERB into a Rails view after installing the component. The snippet below is the same code used by the live preview above.
app/views/.../sidebar_example.html.erb
<div class="w-full max-w-sm">
<%= render Senren::SidebarComponent.new(brand: "Acme", items: [
{ label: "Dashboard", href: "#", active: true },
{ label: "Projects", href: "#" },
{ label: "Team", href: "#" },
{ label: "Settings", href: "#" }
]) do %>
Workspace navigation built from server-rendered links.
<% end %>
</div>
Install this component #
Copy the official component into your app
This component requires Stimulus. Keep --client so the controller is copied with the ViewComponent.
Terminal
bin/rails senren:add sidebar --client
Create a custom component with the same conventions
Use this when you need an app-specific component that follows Senren's ViewComponent and Stimulus structure. --client is required for this behavior.
Terminal
bin/rails generate senren:component sidebar --client
Dependencies are resolved by senren:add:
button.
At a glance #
Category
Navigation
Class name
Senren::SidebarComponent
Stimulus
senren--sidebar
Variants
default, compact
Depends on
button
Pairs with
app_shell, top_nav
Source #
app/components/senren/sidebar_component.html.erb
<aside <%= tag.attributes(**root_attrs("flex min-h-80 flex-col overflow-hidden rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))] p-3 text-[hsl(var(--senren-card-foreground))] transition-[width] duration-300 ease-in-out", data: { controller: "senren--sidebar" })) %>>
<div class="mb-4 flex items-center justify-between gap-2 px-2">
<div data-senren--sidebar-target="brand" class="truncate font-display text-sm font-semibold tracking-tight"><%= brand %></div>
<button type="button" data-senren--sidebar-target="toggleButton" class="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-md text-[hsl(var(--senren-muted-foreground))] transition-colors hover:bg-[hsl(var(--senren-accent))] hover:text-[hsl(var(--senren-accent-foreground))]" data-action="click->senren--sidebar#toggle" aria-label="Toggle sidebar" aria-expanded="<%= variant != :compact %>">
<span class="sr-only">Toggle sidebar</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M3 6h18" />
<path d="M3 12h18" />
<path d="M3 18h18" />
</svg>
</button>
</div>
<nav aria-label="<%= label %>" class="space-y-1">
<% items.each do |item| %>
<%= link_to item[:href], title: item[:label], data: { "senren--sidebar-target": "link" }, class: "flex items-center gap-2 rounded-md px-3 py-2 text-sm transition-all duration-200 #{item[:active] ? "bg-[hsl(var(--senren-primary))] text-[hsl(var(--senren-primary-foreground))]" : "text-[hsl(var(--senren-muted-foreground))] hover:bg-[hsl(var(--senren-accent))] hover:text-[hsl(var(--senren-accent-foreground))]"}" do %>
<span data-senren--sidebar-target="linkInitial" class="inline-flex h-4 w-0 items-center justify-center overflow-hidden text-xs font-semibold uppercase tracking-wide opacity-0 transition-all duration-200"><%= item[:label].to_s.first&.upcase || "•" %></span>
<span data-senren--sidebar-target="linkLabel" class="max-w-40 truncate opacity-100 transition-all duration-200"><%= item[:label] %></span>
<% end %>
<% end %>
</nav>
<% if content? %>
<div data-senren--sidebar-target="footer" class="mt-auto pt-4 text-xs text-[hsl(var(--senren-muted-foreground))]"><%= content %></div>
<% end %>
</aside>
app/components/senren/sidebar_component.rb
# frozen_string_literal: true
module Senren
class SidebarComponent < BaseComponent
VARIANTS = {
default: 'w-64',
compact: 'w-20'
}.freeze
SIZES = { md: '' }.freeze
def initialize(items: [], brand: 'Senren', variant: :default, label: 'Primary', class_name: nil, **html)
super(variant: variant, size: :md, class_name: class_name, **html)
@items = normalize_items(items)
@brand = brand
@label = label
end
attr_reader :items, :brand, :label
private
def normalize_items(items)
Array(items).map do |item|
if item.is_a?(Hash)
{
label: item[:label] || item['label'],
href: item[:href] || item['href'] || '#',
active: item[:active] || item['active']
}
else
label, href = item
{ label: label, href: href || '#', active: false }
end
end
end
end
end
AI agent rules #
Use for
- +primary app navigation in dashboards
Avoid
- -marketing sites - use top_nav
Accessibility #
- role=navigation with aria-label; collapsed state retains tab order.