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
Sidebar State
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
Sidebar Content
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.
Sidebar State Variants
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") |
Sidebar Header Collapse Pattern
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:hiddenon 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 %>
Navbar Logo with Sidebar State
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" |