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 of SidebarInset.
  • 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 inside SidebarContent.
  • 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.
sidebar-structure

Usage

Create Page

Page.kt
@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

SidebarNavigation.kt
@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.

AppSidebar.kt
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.

Layout.kt
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:

basic-sidebar-example

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 Sidebar and SidebarInset as direct children of SidebarProvider.
  • The composition order matches the React docs (sidebar first, then inset).
  • The body of Sidebar/SidebarInset may freely read LocalSidebarState; 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.

VariantSidebar chromeSidebarInset chrome
SidebarFull-bleed Column on styles.sidebarFlush content on styles.background
FloatingPadded outer container; rounded bordered cardSame as Sidebar
InsetSidebar background extends to the screen edgeMargin + radius.xl rounded card with shadow

Here is the preview of sidebar inset and floating

Inset

inser-sidebar-example

Floating

floating-sidebar-example

With collapsible icon

cllapsible-icon-sidebar

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

ModeBehaviorAnimation
OffcanvasDefault. The sidebar hides entirely when closed; the inset reclaims the freed width.Slide + width expand/shrink (~200 ms).
IconSidebar collapses to an icon-only rail of widthIcon (48.dp).Width tween (SidebarDefaults.AnimationSpec).
NoneAlways 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 Material3 PlainTooltip on long-press / hover.
  • SidebarMenuSub disappears entirely (matches React).
  • The text-overload of SidebarHeader / SidebarFooter reduces 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:

TokenDefaultPurpose
Width256.dpDesktop expanded width (~16rem).
WidthMobile288.dpMobile drawer width (~18rem).
WidthIcon48.dpCollapsed icon-rail width (~3rem).
MobileBreakpoint768.dpBelow this viewport width the sidebar uses a drawer.
AnimationSpectween(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

ComposablePurpose
SidebarProviderState, viewport detection, mobile drawer; slot orchestrator.
SidebarSidebar root; registers content into the sidebar slot.
SidebarInsetMain content area; registers content into the inset slot.
SidebarTriggerGhost icon button bound to state.toggleSidebar().
SidebarRailMarker — 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.
SidebarSeparatorThemed horizontal divider.
SidebarInputThemed text field (uses styles.background).
SidebarContentScrollable middle section; must be inside Sidebar's ColumnScope.
SidebarGroupContainer for a group of items.
SidebarGroupLabel(text) / { content }Group heading; hidden in icon mode.
SidebarGroupActionTrailing button for the group; hidden in icon mode.
SidebarGroupContentItems wrapper inside a group.
SidebarMenuVertical list of menu items.
SidebarMenuItemBox wrapper for a row + its action/badge.
SidebarMenuButton(text, ...) / { ... }Clickable row; variant/size/tooltip aware; icon slot sets its own size.
SidebarMenuActionTrailing affordance; hidden in icon mode.
SidebarMenuBadgeTrailing counter / status pill; hidden in icon mode.
SidebarMenuSkeletonLoading placeholder; adapts to icon mode.
SidebarMenuSub / SubItem / SubButtonNested level; hidden in icon mode.

Enums

EnumValues
SidebarSideLeft, Right
SidebarVariantSidebar, Floating, Inset
SidebarCollapsibleOffcanvas, Icon, None
SidebarStateValueExpanded, Collapsed
SidebarMenuButtonVariantDefault, Outline
SidebarMenuButtonSizeDefault, Small, Large

Recipes

SidebarHeader(
    title = "Acme Inc.",
    icon = { Icon(Icons.Default.Star, contentDescription = "logo") },
)
SidebarFooter(text = "© 2025 Komo UI")

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).

Edit this page on GitHub