App Shell
A resizable, foldable sidebar with sections and items that becomes a drawer on small screens.
Rendering App Shell…
Installation
Copy the source into your project with the CLI. It lands under @/components/ui and is yours to edit.
npx mlola-ui add app-shellUsage
import { IconHome, IconMail, IconSettings } from "@mlola-ui/icons";
import { AppShell, Sidebar, SidebarItem, SidebarSection } from "@/components/ui/app-shell";
export default function Example() {
return (
<div style={{ height: 320 }}>
<AppShell
label="Workspace"
sidebar={
<Sidebar header={<strong>Atlas</strong>}>
<SidebarSection title="Workspace">
<SidebarItem label="Home" icon={<IconHome size="1em" />} href="/" current />
<SidebarItem label="Inbox" icon={<IconMail size="1em" />} href="/inbox" count={3} />
<SidebarItem label="Settings" icon={<IconSettings size="1em" />} href="/settings" />
</SidebarSection>
</Sidebar>
}
>
<main>Home</main>
</AppShell>
</div>
);
}Styling comes from the engine stylesheet and the nearest theme. Set it once at the root:
<html data-theme="graphite" data-mode="light">HTML
The markup the example above renders, at rest. The interaction lives in the React component; from another framework, set the attributes below as the state changes.
<div style="height:320px">
<div class="ml-app-shell">
<div class="ml-resizable ml-app-shell-frame" data-direction="horizontal" data-anchor="first" style="--ml-resizable-size:248px">
<div class="ml-resizable-pane">
<nav class="ml-app-shell-sidebar" aria-label="Workspace">
<div class="ml-sidebar">
<div class="ml-sidebar-header"><strong>Atlas</strong></div>
<div class="ml-sidebar-body">
<section class="ml-sidebar-section">
<h2 class="ml-sidebar-title">Workspace</h2>
<ul class="ml-sidebar-list" id="app-shell-1">
<li class="ml-sidebar-entry">
<a class="ml-sidebar-item" href="/" aria-current="page"><span class="ml-sidebar-icon" aria-hidden="true"><svg class="mlola-glyph" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="var(--mlola-glyph-stroke, var(--ml-icon-stroke, 1.75))" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true" data-optical-size="default" style="color:var(--mlola-glyph-color, currentColor);scale:var(--mlola-glyph-optical-scale, 1);transform-origin:center"><path d="m3 11 9-8 9 8v10h-6v-6H9v6H3Z" pathLength="1"></path></svg></span><span class="ml-sidebar-label">Home</span></a>
</li>
<li class="ml-sidebar-entry">
<a class="ml-sidebar-item" href="/inbox"><span class="ml-sidebar-icon" aria-hidden="true"><svg class="mlola-glyph" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="var(--mlola-glyph-stroke, var(--ml-icon-stroke, 1.75))" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true" data-optical-size="default" style="color:var(--mlola-glyph-color, currentColor);scale:var(--mlola-glyph-optical-scale, 1);transform-origin:center"><rect x="3" y="5" width="18" height="14" rx="2" pathLength="1"></rect><path d="m4 7 8 6 8-6" pathLength="1"></path></svg></span><span class="ml-sidebar-label">Inbox</span><span class="ml-sidebar-count">3</span></a>
</li>
<li class="ml-sidebar-entry">
<a class="ml-sidebar-item" href="/settings"><span class="ml-sidebar-icon" aria-hidden="true"><svg class="mlola-glyph" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="var(--mlola-glyph-stroke, var(--ml-icon-stroke, 1.75))" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true" data-optical-size="default" style="color:var(--mlola-glyph-color, currentColor);scale:var(--mlola-glyph-optical-scale, 1);transform-origin:center"><circle cx="12" cy="12" r="3" pathLength="1"></circle><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1-2.8 2.8-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.6v.2h-4V21a1.7 1.7 0 0 0-1-1.6 1.7 1.7 0 0 0-1.9.3l-.1.1L4.2 17l.1-.1a1.7 1.7 0 0 0 .3-1.9A1.7 1.7 0 0 0 3 14H2.8v-4H3a1.7 1.7 0 0 0 1.6-1 1.7 1.7 0 0 0-.3-1.9L4.2 7 7 4.2l.1.1A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-1.6v-.2h4V3a1.7 1.7 0 0 0 1 1.6 1.7 1.7 0 0 0 1.9-.3l.1-.1L19.8 7l-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.6 1h.2v4H21a1.7 1.7 0 0 0-1.6 1Z" pathLength="1"></path></svg></span><span class="ml-sidebar-label">Settings</span></a>
</li>
</ul>
</section>
</div>
</div>
</nav>
</div>
<div class="ml-resizable-handle" role="separator" tabindex="0" aria-label="Resize workspace" aria-orientation="vertical" aria-valuemin="0" aria-valuemax="360" aria-valuenow="248"></div>
<div class="ml-resizable-pane"><div class="ml-app-shell-main"><main>Home</main></div></div>
</div>
</div>
</div>Classes and attributes
A class names each element; data-* and aria-* attributes carry its variant and state. The values listed are the ones the stylesheet draws; the default needs no attribute.
| Class | Attributes |
|---|---|
ml-app-shell | data-narrow |
ml-app-shell-frame | — |
ml-app-shell-sidebar | — |
ml-app-shell-main | — |
ml-resizable | data-anchor="second"data-direction="horizontal | vertical" |
ml-app-shell-drawer | — |
ml-app-shell-menu | — |
ml-sidebar | — |
ml-sidebar-header | — |
ml-sidebar-footer | — |
ml-sidebar-body | — |
ml-sidebar-section | — |
ml-sidebar-title | aria-expanded="false"data-collapsible |
ml-sidebar-chevron | — |
ml-sidebar-list | — |
ml-sidebar-entry | — |
ml-sidebar-item | aria-current="page" |
ml-sidebar-icon | — |
ml-sidebar-label | — |
ml-sidebar-count | — |
ml-sidebar-actions | — |
Accessibility and theming
- Semantic elements, accessible names and state relationships are part of the contract.
- Keyboard, pointer and touch paths are operable; focus stays visible in every theme.
- Light and dark modes pass WCAG AA contrast in all 5 canonical themes.
- Reduced motion and forced colors keep meaning and operation.
Dependencies
Installed alongside this component. No third-party UI runtime is ever added.