Sidebar
A composable, themeable sidebar component.
Sidebars are one of the most complex components to build. They are central to any application and often contain a lot of moving parts. KomoUI's sidebar exposes Sidebar and SidebarInset as siblings inside SidebarProvider, and the provider arranges them as a Row on desktop or as a ModalNavigationDrawer on mobile (viewport < SidebarDefaults.MobileBreakpoint, default 768.dp).
Structure
A Sidebar component is composed of the following parts:
SidebarProvider— Owns the open/collapsed state, detects the viewport, and orchestrates desktop vs mobile layout.Sidebar— The sidebar container; sibling ofSidebarInset.SidebarInset— The main content area beside the sidebar; hosts the mobile drawer.SidebarHeader/SidebarFooter— Sticky top and bottom sections.SidebarContent— Scrollable middle section.SidebarGroup— A labelled section of menu items insideSidebarContent.SidebarMenu/SidebarMenuItem/SidebarMenuButton— The navigation rows.SidebarTrigger— A ghost icon button that toggles the sidebar.SidebarRail— Optional thin clickable strip on the sidebar's outer edge.

Usage
Create Page
@Composable
fun DashboardPage(nav: NavHostController) {
Column { Text(text = "Welcome to the Dashboard!") }
}
@Composable
fun ProjectPage(nav: NavHostController) {
Column { Text(text = "Welcome to the Project!") }
}
@Composable
fun TaskPage(nav: NavHostController) {
Column { Text(text = "Welcome to the Task!") }
}Create Navigation
@Composable
fun SidebarNavigation(sidebarNavHost: NavHostController) {
NavHost(
sidebarNavHost,
modifier = Modifier,
startDestination = SidebarRoute.Dashboard.path,
) {
composable(SidebarRoute.Dashboard.path) { DashboardPage(sidebarNavHost) }
composable(SidebarRoute.Project.path) { ProjectPage(sidebarNavHost) }
composable(SidebarRoute.Task.path) { TaskPage(sidebarNavHost) }
}
}Create AppSidebar
Compose Sidebar from SidebarHeader, SidebarContent (with one or more SidebarGroups), and an optional SidebarFooter. Add SidebarRail() to render an outer-edge click rail.
import com.komoui.components.sidebar.Sidebar
import com.komoui.components.sidebar.SidebarContent
import com.komoui.components.sidebar.SidebarFooter
import com.komoui.components.sidebar.SidebarGroup
import com.komoui.components.sidebar.SidebarGroupContent
import com.komoui.components.sidebar.SidebarGroupLabel
import com.komoui.components.sidebar.SidebarHeader
import com.komoui.components.sidebar.SidebarMenu
import com.komoui.components.sidebar.SidebarMenuButton
import com.komoui.components.sidebar.SidebarRail
@Composable
fun AppSidebar(
sidebarNav: NavHostController,
selectedMenu: String,
onMenuClick: (String) -> Unit,
) {
val menus = listOf(
Content("Dashboard", SidebarRoute.Dashboard.path, Icons.Default.Home),
Content("Projects", SidebarRoute.Project.path, Icons.Default.Folder),
Content("Tasks", SidebarRoute.Task.path, Icons.Default.CheckCircle),
)
Sidebar {
SidebarHeader(
title = "My App",
icon = {
Box(
modifier = Modifier.size(28.dp).background(
MaterialTheme.styles.foreground,
RoundedCornerShape(MaterialTheme.radius.md),
),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Star,
contentDescription = "logo",
tint = MaterialTheme.styles.sidebarPrimaryForeground,
modifier = Modifier.size(24.dp),
)
}
},
)
SidebarContent {
SidebarGroup {
SidebarGroupLabel("Navigation")
SidebarGroupContent {
SidebarMenu {
menus.forEach { item ->
SidebarMenuItem {
SidebarMenuButton(
onClick = {
onMenuClick(item.title)
sidebarNav.navigate(item.route)
},
isActive = selectedMenu == item.title,
icon = {
Icon(
imageVector = when (item.title) {
"Dashboard" -> Icons.Default.Home
"Projects" -> Icons.Default.Star
else -> Icons.AutoMirrored.Filled.List
},
contentDescription = item.title,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.styles.sidebarForeground,
)
},
) {
Text(
text = item.title,
color = MaterialTheme.styles.sidebarForeground,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.weight(1f),
)
}
}
}
}
}
}
}
SidebarFooter(text = "© 2025 KomoUI")
}
}Create SidebarLayout
Place AppSidebar and SidebarInset as direct children of SidebarProvider. The provider chooses the layout based on viewport width.
import com.komoui.components.sidebar.SidebarInset
import com.komoui.components.sidebar.SidebarProvider
import com.komoui.components.sidebar.SidebarTrigger
@Composable
fun SidebarLayoutPage() {
var selectedItem by remember { mutableStateOf("Dashboard") }
val sidebarNav = rememberNavController()
SidebarProvider(defaultOpen = true) {
AppSidebar(sidebarNav, selectedItem) { selectedItem = it }
SidebarInset {
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.styles.background)
.padding(horizontal = 8.dp),
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
SidebarTrigger()
Spacer(modifier = Modifier.width(16.dp))
Text(
text = selectedItem,
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.styles.foreground,
)
}
Spacer(modifier = Modifier.height(24.dp))
SidebarNavigation(sidebarNav)
}
}
}
}Result
You've created your first sidebar, you should see something like this:

Composition model
Sidebar and SidebarInset are slot-registering composables — they don't emit their own subtree, they record their content into provider-owned slots. The provider then renders those slots in the right layout for the viewport. Practically:
- Always place
SidebarandSidebarInsetas direct children ofSidebarProvider. - The composition order matches the React docs (sidebar first, then inset).
- The body of
Sidebar/SidebarInsetmay freely readLocalSidebarState; the state is in scope at the position the provider eventually renders them.
Controlled vs uncontrolled
// Uncontrolled — provider owns the state.
SidebarProvider(defaultOpen = true) { /* ... */ }
// Controlled — you own the state.
var open by rememberSaveable { mutableStateOf(true) }
SidebarProvider(open = open, onOpenChange = { open = it }) { /* ... */ }Mobile open state is always uncontrolled and session-scoped (matches the React behavior).
Variants
Pass variant to SidebarProvider to switch the sidebar / inset chrome.
| Variant | Sidebar chrome | SidebarInset chrome |
|---|---|---|
Sidebar | Full-bleed Column on styles.sidebar | Flush content on styles.background |
Floating | Padded outer container; rounded bordered card | Same as Sidebar |
Inset | Sidebar background extends to the screen edge | Margin + radius.xl rounded card with shadow |
Here is the preview of sidebar inset and floating
Inset

Floating

With collapsible icon
Side
SidebarSide.Left (default) and SidebarSide.Right are supported on both desktop and mobile. On mobile, the right-anchored ModalNavigationDrawer is achieved by wrapping the drawer in LayoutDirection.Rtl; drawer content is restored to the host's layout direction so text never mirrors.
Collapsible modes
| Mode | Behavior | Animation |
|---|---|---|
Offcanvas | Default. The sidebar hides entirely when closed; the inset reclaims the freed width. | Slide + width expand/shrink (~200 ms). |
Icon | Sidebar collapses to an icon-only rail of widthIcon (48.dp). | Width tween (SidebarDefaults.AnimationSpec). |
None | Always visible; state.toggleSidebar() is a no-op. | None. |
When Icon is active and the sidebar is collapsed:
- Labels, badges, sub-menus, group labels, and group actions hide.
SidebarMenuButton(tooltip = ...)activates a Material3PlainTooltipon long-press / hover.SidebarMenuSubdisappears entirely (matches React).- The text-overload of
SidebarHeader/SidebarFooterreduces to the icon (or hides if no icon is provided).
Tunables
Override per-provider or globally via a wrapper:
SidebarProvider(
width = 280.dp,
widthMobile = 320.dp,
widthIcon = 56.dp,
mobileBreakpoint = 600.dp,
animationSpec = tween(durationMillis = 250),
) { /* ... */ }Defaults live in SidebarDefaults:
| Token | Default | Purpose |
|---|---|---|
Width | 256.dp | Desktop expanded width (~16rem). |
WidthMobile | 288.dp | Mobile drawer width (~18rem). |
WidthIcon | 48.dp | Collapsed icon-rail width (~3rem). |
MobileBreakpoint | 768.dp | Below this viewport width the sidebar uses a drawer. |
AnimationSpec | tween(200) | Width / inset transition spec. |
System bars / safe area
On Android, the sidebar background extends behind the status and navigation bars so the system bars tint with the sidebar color, while the inner content (header, content, footer) gets windowInsetsPadding(WindowInsets.systemBars) applied so it stays inside the safe area. The same applies to SidebarInset when variant = Inset. Mobile drawer mode delegates inset handling to Material3's ModalNavigationDrawer.
API reference
Composables
| Composable | Purpose |
|---|---|
SidebarProvider | State, viewport detection, mobile drawer; slot orchestrator. |
Sidebar | Sidebar root; registers content into the sidebar slot. |
SidebarInset | Main content area; registers content into the inset slot. |
SidebarTrigger | Ghost icon button bound to state.toggleSidebar(). |
SidebarRail | Marker — opts the sidebar into rendering an outer-edge click rail. |
SidebarHeader { content } | Top sticky section, free-form slot. |
SidebarHeader(title, icon) | Brand-style overload — title hides in icon mode; icon (if any) remains. |
SidebarFooter { content } | Bottom sticky section, free-form slot. |
SidebarFooter(text, icon) | Convenience overload — text hides in icon mode; icon (if any) remains. |
SidebarSeparator | Themed horizontal divider. |
SidebarInput | Themed text field (uses styles.background). |
SidebarContent | Scrollable middle section; must be inside Sidebar's ColumnScope. |
SidebarGroup | Container for a group of items. |
SidebarGroupLabel(text) / { content } | Group heading; hidden in icon mode. |
SidebarGroupAction | Trailing button for the group; hidden in icon mode. |
SidebarGroupContent | Items wrapper inside a group. |
SidebarMenu | Vertical list of menu items. |
SidebarMenuItem | Box wrapper for a row + its action/badge. |
SidebarMenuButton(text, ...) / { ... } | Clickable row; variant/size/tooltip aware; icon slot sets its own size. |
SidebarMenuAction | Trailing affordance; hidden in icon mode. |
SidebarMenuBadge | Trailing counter / status pill; hidden in icon mode. |
SidebarMenuSkeleton | Loading placeholder; adapts to icon mode. |
SidebarMenuSub / SubItem / SubButton | Nested level; hidden in icon mode. |
Enums
| Enum | Values |
|---|---|
SidebarSide | Left, Right |
SidebarVariant | Sidebar, Floating, Inset |
SidebarCollapsible | Offcanvas, Icon, None |
SidebarStateValue | Expanded, Collapsed |
SidebarMenuButtonVariant | Default, Outline |
SidebarMenuButtonSize | Default, Small, Large |
Recipes
Header with logo
SidebarHeader(
title = "Acme Inc.",
icon = { Icon(Icons.Default.Star, contentDescription = "logo") },
)Footer with copyright (hidden in icon mode)
SidebarFooter(text = "© 2025 Komo UI")Footer with NavUser (avatar + name/email)
The icon slot of SidebarMenuButton does not clamp its size, so an Avatar renders at its natural size beside the name/email column. When the sidebar collapses to icon mode, the menu button reduces to just the avatar.
SidebarFooter {
SidebarMenu {
SidebarMenuItem {
SidebarMenuButton(
onClick = { /* open user menu */ },
size = SidebarMenuButtonSize.Large,
tooltip = "komoui",
icon = {
Avatar(
model = null,
size = 32.dp,
fallbackText = "KO",
)
},
) {
Column(modifier = Modifier.weight(1f)) {
Text("komoui", fontSize = 14.sp, fontWeight = FontWeight.Medium)
Text(
"m@example.com",
fontSize = 12.sp,
color = MaterialTheme.styles.mutedForeground,
)
}
Icon(
Icons.Default.UnfoldMore,
contentDescription = "Open user menu",
modifier = Modifier.size(16.dp),
)
}
}
}
}Theming
The sidebar reads from MaterialTheme.styles:
sidebar,sidebarForeground— root colors.sidebarAccent,sidebarAccentForeground— active row colors.sidebarBorder— separators, outline-variant border, and the rail.sidebarRing— focus ring (reserved, not yet applied).
Corner radii come from MaterialTheme.radius (sm, md, lg, xl).