Senren UI
Components / Sidebar

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.