DashboardGroup

Full-screen sidebar + topbar layout shell for dashboard applications.

Quick Start

Create a dashboard layout by composing the four layout components inside your application layout file:

<!DOCTYPE html>
<html lang="en">
  <head>
    <%= kiso_theme_script %>
    <%= stylesheet_link_tag "tailwind" %>
    <%= javascript_importmap_tags %>
  </head>
  <body>
    <%= kui(:dashboard_group) do %>
      <%= kui(:dashboard_navbar) do %>
        <%= kui(:dashboard_sidebar, :toggle) %>
        <%= kui(:dashboard_sidebar, :collapse) %>
        <%= yield :topbar %>
      <% end %>

      <%= kui(:dashboard_sidebar) do %>
        <%= kui(:nav) do %>
          <%= kui(:nav, :section, title: "Main") do %>
            <%= kui(:nav, :item, href: "/", icon: "layout-dashboard", active: true) { "Dashboard" } %>
          <% end %>
        <% end %>
        <%= yield :sidebar %>
      <% end %>

      <%= kui(:dashboard_panel) do %>
        <%= yield %>
      <% end %>
    <% end %>
  </body>
</html>

Components

Component Element Purpose
kui(:dashboard_group) <div> Root grid container, manages sidebar state. layout: :sidebar (default) or :navbar
kui(:dashboard_navbar) <header> Topbar (panel column only in :sidebar layout, full-width in :navbar layout)
kui(:dashboard_sidebar, :toggle) <button> Mobile-only hamburger toggle (lg:hidden)
kui(:dashboard_sidebar, :collapse) <button> Desktop-only collapse button (hidden lg:flex)
kui(:dashboard_sidebar) <aside> Collapsible sidebar navigation area
kui(:dashboard_sidebar, :header) <div> Top section of sidebar (logo, collapse button)
kui(:dashboard_sidebar, :footer) <div> Bottom section of sidebar (sign-out, color mode)
kui(:dashboard_toolbar) <div> Secondary action bar with :left and :right sub-parts
kui(:dashboard_panel) <main> Main content area
kui(:nav) <nav> Navigation wrapper with :section and :item sub-parts

Locals

DashboardGroup

Local Type Default
sidebar_open: Boolean | nil nil (reads from cookie)
layout: Symbol :sidebar
css_classes: String ""
**component_options Hash {}

The layout: prop controls the grid structure:

  • :sidebar (default) — sidebar spans full viewport height, navbar spans the panel column only
  • :navbar — navbar spans full width across both columns, sidebar occupies the second row

DashboardNavbar, DashboardSidebar, DashboardPanel

Local Type Default
css_classes: String ""
**component_options Hash {}

DashboardSidebar :collapse

Local Type Default
open_icon: String | nil nil (uses panel_left_close icon)
closed_icon: String | nil nil (uses panel_left_open icon)
css_classes: String ""
**component_options Hash {}

DashboardNavbar :toggle

Local Type Default
css_classes: String ""
**component_options Hash {}

Anatomy

Default layout (:sidebar) — sidebar spans full height, navbar in panel column:

DashboardGroup (grid root, kiso--sidebar controller)
├── DashboardSidebar (collapsible aside, spans both rows)
│   └── sidebar-inner (auto-rendered scroll container)
│       ├── DashboardSidebar :header (optional, top section)
│       ├── ... sidebar content ...
│       └── DashboardSidebar :footer (optional, bottom section)
├── DashboardNavbar (topbar, panel column only)
│   └── DashboardNavbar :toggle (hamburger button)
├── DashboardPanel (main content)
└── scrim (auto-rendered mobile overlay)

Alternate layout (:navbar) — navbar spans full width:

DashboardGroup (grid root, kiso--sidebar controller)
├── DashboardNavbar (topbar, spans both columns)
│   └── DashboardNavbar :toggle (hamburger button)
├── DashboardSidebar (collapsible aside)
│   └── sidebar-inner (auto-rendered scroll container)
│       ├── DashboardSidebar :header (optional, top section)
│       ├── ... sidebar content ...
│       └── DashboardSidebar :footer (optional, bottom section)
├── DashboardPanel (main content)
└── scrim (auto-rendered mobile overlay)

The sidebar inner wrapper and scrim are rendered automatically by dashboard_group and dashboard_sidebar. You only compose the five public components.

Usage

By default, dashboard_group reads the sidebar_open cookie to restore the user’s preference. Pass sidebar_open: explicitly to override:

<%= kui(:dashboard_group, sidebar_open: true) do %>
  ...
<% end %>

The kiso--sidebar Stimulus controller persists the sidebar state to a one-year cookie on every toggle, so the next page load restores it server-side without JavaScript.

Topbar Content

Place your logo, search bar, user menu, and other topbar elements inside dashboard_navbar. The sidebar toggle and collapse buttons should come first:

<%= kui(:dashboard_navbar) do %>
  <%= kui(:dashboard_sidebar, :toggle) %>
  <%= kui(:dashboard_sidebar, :collapse) %>
  <div class="flex-1"></div>
  <%= kui(:color_mode_button) %>
<% end %>

Custom Toggle Icons

The toggle button renders a default icon (menu for the hamburger) but accepts a block to replace it with any content:

<%= kui(:dashboard_sidebar, :toggle) do %>
  <%= kiso_icon("align-justify", class: "size-4") %>
<% end %>

The collapse button has two state-dependent icons (open/closed) that swap via CSS. Override them per-instance with open_icon: and closed_icon::

<%= kui(:dashboard_sidebar, :collapse,
    open_icon: kiso_icon("chevron-left", class: "size-4"),
    closed_icon: kiso_icon("chevron-right", class: "size-4")) %>

To change default icons globally (without passing props every time), override them in your initializer:

# config/initializers/kiso.rb
Kiso.configure do |config|
  config.icons[:menu] = "align-justify"             # toggle
  config.icons[:panel_left_close] = "chevron-left"   # collapse (open state)
  config.icons[:panel_left_open] = "chevron-right"   # collapse (closed state)
end

Place navigation links, grouped menus, and any sidebar content inside dashboard_sidebar:

<%= kui(:dashboard_sidebar) do %>
  <nav class="flex flex-col gap-1 p-4">
    <%= link_to "Home", root_path, class: "..." %>
    <%= link_to "Settings", settings_path, class: "..." %>
  </nav>
<% end %>

Active Nav Item

Use Rails’ controller_name helper to highlight the current page’s nav item automatically:

<%= kui(:nav, :item,
      href: dashboard_path,
      icon: "layout-dashboard",
      active: controller_name == "dashboard") { "Dashboard" } %>
<%= kui(:nav, :item,
      href: settings_path,
      icon: "settings",
      active: controller_name == "settings") { "Settings" } %>

For items that should highlight across multiple controllers:

<%= kui(:nav, :item,
      href: settings_path,
      icon: "settings",
      active: controller_name.in?(%%w[settings billing integrations])) { "Settings" } %>

controller_name is a built-in Rails helper that returns the current controller as a lowercase string (e.g. "dashboard", "settings"). It evaluates on every render, so the correct item highlights automatically with no extra setup.

Kiso ships two custom Tailwind variants for showing/hiding content based on whether the sidebar is open or closed. These work on any element inside dashboard_group — not just the navbar:

<div class="kui-sidebar-open:lg:hidden flex items-center gap-2">
  <%= image_tag "logo.svg", class: "h-6 w-6" %>
  <span class="font-semibold">MyApp</span>
</div>

<div class="hidden kui-sidebar-closed:lg:block">
  <%= image_tag "icon.svg", class: "h-6 w-6" %>
</div>

<p class="hidden kui-sidebar-closed:block text-sm text-muted-foreground">
  Open the sidebar for full navigation
</p>

The variants compose with all other Tailwind modifiers — kui-sidebar-open:lg:hidden means “hide at the lg breakpoint and above when the sidebar is open.” On mobile the sidebar is an overlay, so the variant typically pairs with a breakpoint to only affect desktop behavior.

Variant Matches when
kui-sidebar-open: Sidebar is expanded (data-sidebar-open="true")
kui-sidebar-closed: Sidebar is collapsed (data-sidebar-open="false")

Apps like Linear and Notion place the collapse button in the sidebar header instead of the navbar. In this pattern, the navbar toggle (hamburger) should only appear on mobile or when the sidebar is closed on desktop — otherwise you’d have two buttons that do the same thing:

<%= kui(:dashboard_navbar, css_classes: "lg:hidden") do %>
  <%= kui(:dashboard_sidebar, :toggle) %>
<% end %>

<%= kui(:dashboard_sidebar) do %>
  <%= kui(:dashboard_sidebar, :header) do %>
    <div class="flex items-center gap-1.5 flex-1 min-w-0">
      <%= image_tag "logo.svg", class: "h-6 w-6 shrink-0" %>
      <span class="font-semibold text-sm truncate">MyApp</span>
    </div>
    <%= kui(:dashboard_sidebar, :collapse) %>
  <% end %>
  <%= kui(:nav) do %>
  <% end %>
<% end %>

Key classes:

  • lg:hidden on the navbar — hides the entire navbar row on desktop. On mobile the sidebar is an overlay, so the navbar hamburger toggle is still needed.
  • Collapse button in sidebar header — the collapse button moves into the sidebar header alongside the logo, so users can collapse from within the sidebar itself.

If you want the navbar to remain visible on desktop (e.g., for breadcrumbs or user actions) but just hide the toggle, use kui-sidebar-open:lg:hidden on the toggle instead:

<%= kui(:dashboard_navbar) do %>
  <%= kui(:dashboard_sidebar, :toggle, css_classes: "kui-sidebar-open:lg:hidden") %>
<% end %>

Page-Specific Sidebar Content

Use Rails’ content_for to inject per-page content into the sidebar from any view. Define a yield point in your layout:

<%= kui(:dashboard_sidebar) do %>
  <%= kui(:nav) do %>
  <% end %>
  <% if content_for?(:sidebar) %>
    <%= yield :sidebar %>
  <% end %>
  <%= kui(:dashboard_sidebar, :footer) do %>
    <%= kui(:color_mode_button) %>
  <% end %>
<% end %>

Then populate it from any view:

<% content_for :sidebar do %>
  <div class="p-4">
    <h3 class="text-sm font-medium text-foreground">Participants</h3>
  </div>
<% end %>

When your sidebar header shows a logo, hide the duplicate navbar logo on desktop using the kui-sidebar-open: variant:

<%= kui(:dashboard_navbar) do %>
  <%= kui(:dashboard_sidebar, :toggle) %>
  <div class="kui-sidebar-open:lg:hidden flex items-center gap-2">
    <%= image_tag "logo.svg", class: "h-6 w-6" %>
    <span class="font-semibold">MyApp</span>
  </div>
  <div class="flex-1"></div>
  <%= kui(:color_mode_button) %>
<% end %>

The logo appears in the navbar when the sidebar is collapsed (giving users a brand anchor), and hides when the sidebar opens (since the sidebar header already shows it). On mobile the sidebar is an overlay, so the navbar logo stays visible.

Custom CSS Tokens

Override layout tokens in your app’s Tailwind CSS:

@theme {
  --sidebar-width: 18rem;
  --topbar-height: 4rem;
}

CSS Tokens

Token Default Purpose
--sidebar-width 16rem Sidebar width when open
--topbar-height 3.5rem Navbar height
--sidebar-duration 220ms Open/close animation duration
--sidebar-background white / zinc-950 Sidebar background (light/dark)
--sidebar-foreground zinc-900 / zinc-100 Sidebar text color
--sidebar-border zinc-200 / zinc-800 Sidebar right border

Theme

DashboardGroup = ClassVariants.build(
  base: "grid h-dvh overflow-hidden bg-background text-foreground antialiased"
)

DashboardNavbar = ClassVariants.build(
  base: "col-span-full flex items-center gap-3 px-4 border-b border-border
         bg-background shrink-0 z-(--z-topbar)"
)

DashboardNavbarToggle = ClassVariants.build(
  base: "flex items-center justify-center w-8 h-8 rounded-md
         text-foreground/50 hover:text-foreground hover:bg-accent
         transition-colors duration-150 shrink-0"
)

DashboardSidebar = ClassVariants.build(
  base: "overflow-hidden border-r"
)

DashboardPanel = ClassVariants.build(
  base: "min-w-0 overflow-y-auto bg-background"
)

Responsive Behavior

On desktop (768px+), the sidebar is an inline grid column that smoothly animates between open and collapsed states. The panel content reflows to fill the available space.

On mobile (below 768px), the sidebar column is always 0. When opened, the sidebar becomes a fixed full-width overlay that slides in from the left. A semi-transparent scrim appears behind it. Tapping the scrim closes the sidebar.

Accessibility

Attribute Element Value
data-slot group "dashboard-group"
data-slot navbar "dashboard-navbar"
data-slot toggle "dashboard-navbar-toggle"
data-slot sidebar "dashboard-sidebar"
data-slot panel "dashboard-panel"
aria-label toggle "Toggle sidebar"
aria-expanded toggle synced with sidebar state
aria-controls toggle "dashboard-sidebar"
aria-label sidebar "Sidebar navigation"
aria-hidden scrim "true"