@akhatmirzo/ui

AppShell

An admin panel and dashboard layout — a collapsible Sidebar, a sticky header, a Drawer on phones and a skip link.

Asosiy kontentga o'tish
Bosh sahifa

Bosh sahifa

Tugma yoki Ctrl/⌘ + B sidebar'ni yig'adi. Holat localStorage'da saqlanadi.

Import

import { AppShell, Sidebar, useAppShell } from '@akhatmirzo/ui';

When to use

  • Admin panels, CRMs, dashboards — a persistent menu on the left, page content on the right.
  • Apps with many sections that users switch between often.
  • Not for marketing pages or blogs — a top navigation works better there.
  • The layout fills the screen; put it in your app's layout once.

Anatomy

PartWhat it does
AppShellState (collapsed/expanded), persistence, the mod+B shortcut, skip link
SidebarA left panel on desktop (256px ↔ 64px), a left Drawer below 1024px
Sidebar.HeaderLogo, app name
Sidebar.Content<nav aria-label="Asosiy menyu">, scrollable
Sidebar.GroupA heading + a list of items; can be collapsible
Sidebar.ItemA link (href), a button (onClick) or a Next Link via renderLink
Sidebar.FooterBottom area: a user menu
AppShell.Main<main> — the skip link jumps here
AppShell.HeaderSticky (sticky top-0), 56px, bottom border
AppShell.SidebarToggleThe sidebar button: collapses on desktop, opens the Drawer on phones
// app/(admin)/layout.tsx
<AppShell sidebarStorageKey="sidebar">
  <Sidebar>
    <Sidebar.Header>
      <Logo />
    </Sidebar.Header>
    <Sidebar.Content>
      <Sidebar.Group label="Main">
        <Sidebar.Item icon={<Home />} href="/" active={pathname === '/'}>
          Home
        </Sidebar.Item>
        <Sidebar.Item icon={<Box />} href="/orders" badge={12}>
          Orders
        </Sidebar.Item>
      </Sidebar.Group>
    </Sidebar.Content>
    <Sidebar.Footer>
      <UserMenu />
    </Sidebar.Footer>
  </Sidebar>
  <AppShell.Main>
    <AppShell.Header>
      <AppShell.SidebarToggle />
      <Breadcrumb items={…} />
    </AppShell.Header>
    {children}
  </AppShell.Main>
</AppShell>

Collapsed state

On desktop (≥ 1024px) the sidebar collapses to a 64px icon rail: item labels are hidden, each item gets a Tooltip and an aria-label, and badges turn into a dot. Collapse it with AppShell.SidebarToggle or Ctrl/⌘ + B. The animation runs on grid-template-columns and jumps straight to the end with prefers-reduced-motion.

A collapsible group folds when its heading is clicked (Accordion style); in the collapsed rail groups are always open and a divider replaces the heading.

Asosiy kontentga o'tish
Bosh sahifa

Yig'ilgan holatda faqat ikonkalar ko'rinadi — ustiga kelsangiz Tooltip chiqadi. "Hisobotlar" guruhi to'liq holatda yig'iladi.

Persisting the state

With sidebarStorageKey, the state is written to localStorage and restored on the next visit. It's SSR-safe: the server and the first client render use defaultSidebarOpen, and the stored value is read after mount (no hydration mismatch). If storage is blocked, the state still works.

Phones and tablets

Below 1024px a left Drawer replaces the sidebar: AppShell.SidebarToggle opens it and clicking a Sidebar.Item (navigating) closes it. The desktop/mobile switch happens through CSS breakpoints, so the server and the browser render the same HTML.

renderLink lets you use your own link component. It receives href, className, aria-current, onClick and the inner content — spread all of them.

'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';

function NavItem({ href, icon, children }) {
  const pathname = usePathname();
  return (
    <Sidebar.Item
      href={href}
      icon={icon}
      active={pathname.startsWith(href)}
      renderLink={(props) => <Link {...props} />}
    >
      {children}
    </Sidebar.Item>
  );
}

useAppShell

Any component inside AppShell can read and change the state: { sidebarOpen, setSidebarOpen, toggleSidebar, mobileOpen, setMobileOpen, collapsed }.

Examples on this page

The examples here live in a bounded frame: className="[--ak-app-shell-height:480px]" sets the layout height (the default is 100dvh). Don't pass it in a real app.

Props

AppShell

Also accepts <div> attributes and ref.

PropTypeDefaultDescription
sidebarOpenboolean—Whether the desktop sidebar is expanded (controlled)
defaultSidebarOpenbooleantrueInitial state
onSidebarOpenChange(open: boolean) => void—Called on collapse / expand
sidebarStorageKeystring—localStorage key — persists the state (SSR-safe)
shortcutstring | false'mod+b'Collapse shortcut (useHotkeys format); false disables it
labels{ skipToContent?: string }{ skipToContent: "Asosiy kontentga o'tish" }Skip link text

Also accepts <aside> attributes (the desktop panel) and ref.

PropTypeDefaultDescription
widthnumber | string256Expanded width; 64px when collapsed
labels{ nav?: string; drawerTitle?: string }{ nav: "Asosiy menyu", drawerTitle: "Menyu" }The <nav> name and the mobile Drawer title (for screen readers)

Sidebar.Item

PropTypeDefaultDescription
iconReactNode—Leading icon (18px); the only thing shown when collapsed
hrefstring—Link target; without it — a <button>
onClick(event: MouseEvent) => void—Called on click (the mobile Drawer closes either way)
activebooleanfalseCurrent page — aria-current="page" and a highlighted background
badgeReactNode—A counter (12) or your own element
disabledbooleanfalseDisabled
labelstring—Tooltip and aria-label when collapsed (defaults to the children text)
renderLink(props: SidebarItemLinkProps) => ReactElement—Your own link component: (p) => <Link {...p} />

Sidebar.Group

PropTypeDefaultDescription
labelReactNode—Group heading
collapsiblebooleanfalseThe group folds when its heading is clicked
defaultOpenbooleantrueWhether a collapsible group starts open

AppShell.SidebarToggle

IconButton props; aria-label defaults to "Yon menyu", variant to ghost. Pass children to replace the built-in icon.

Data attributes

AttributeWhereValue
data-sidebar-stateAppShellexpanded · collapsed
data-collapsedSidebarpresent when collapsed
data-activeSidebar.Itempresent when active
data-disabledSidebar.Itempresent when disabled

Keyboard

KeyAction
Tab (first)Reveals the "skip to main content" link
Ctrl/⌘ + BCollapses/expands on desktop, opens the menu on phones
EscCloses the mobile menu

Accessibility

  • Skip link. AppShell puts a "skip to main content" link at the top of the page — it's visible only on keyboard focus and jumps to AppShell.Main. Change its text with labels.
  • Navigation. The menu is inside <nav aria-label="Asosiy menyu"> and the active item gets aria-current="page"; groups are role="group" and labelled by their heading.
  • Collapsed state. Even with the text hidden, every item keeps its name through aria-label and a Tooltip. Badge counts stay available to screen readers as text.
  • Toggle. AppShell.SidebarToggle gets aria-expanded and aria-controls (the panel on desktop, the Drawer on phones).
  • Mobile menu is a Drawer: focus is trapped inside, Esc closes it and focus returns to the button.

On this page