icon

Morainev0.5.0

SidebarFrame
navigationsidebar-frame

SidebarFrame

Sidebar and main frame with mobile Sheet support and desktop layout wrappers.

Import#

import { SidebarFrame, SidebarFrameSheetResizableRender } from 'moraine'

Slot Structure#

Root frame containing a sidebar with optional header/footer and a scrollable main area.

root
├── sidebar
│ ├── sidebarHeader (optional)
│ ├── sidebarBody
│ └── sidebarFooter (optional)
└── main

Examples#

Basic#

Desktop fixed layout with a simple header/body/main composition. Use --sidebar-width to control sidebar width

function Basic() {
return (
<div class="b-1 b-border rounded-xl h-72 w-full overflow-hidden">
<SidebarFrame
isMobile={false}
sidebarHeaderRender={() => <div class="text-sm font-semibold p-4">Documentation</div>}
sidebarBodyRender={() => (
<div class="p-2 h-full overflow-y-auto">
<div class="flex flex-col gap-1">
<For each={PAGES}>
{(item) => (
<button
type="button"
class="text-sm px-2.5 py-1.5 text-left rounded-md hover:bg-accent"
>
{item}
</button>
)}
</For>
</div>
</div>
)}
mainRender={(ctx) => (
<>
<div class="flex flex-row items-center">
<Button variant="ghost" class="m-2" onClick={() => ctx.toggle()}>
<Icon name="i-lucide-sidebar" />
</Button>
<h3 class="text-base font-semibold">Getting Started</h3>
</div>
<p class="text-muted-foreground px-4">
Use <code class="docs-inline-code">SidebarFrame</code> component to compose sidebar
and content in one place.
</p>
</>
)}
/>
</div>
)
}

Variants#

Compare default, floating, and inset visual variants with the same content.

function Variants() {
return (
<div class="flex flex-col gap-3 w-full">
<For each={['default', 'floating', 'inset'] as const}>
{(variant) => (
<div class="b-1 b-border rounded-xl h-72 w-full overflow-hidden">
<SidebarFrame
isMobile={false}
variant={variant}
sidebarHeaderRender={() => <div class="text-xs p-3">{variant}</div>}
sidebarBodyRender={() => (
<div class="text-sm text-muted-foreground p-2">Sidebar content</div>
)}
mainRender={() => (
<div class="p-4 h-full">
<div class="text-sm text-foreground p-4 b-1 b-border rounded-lg b-dashed bg-muted/20 h-full">
Main content area
</div>
</div>
)}
/>
</div>
)}
</For>
</div>
)
}

Sides#

Compare side="left" and side="right" desktop layouts.

function Sides() {
return (
<div class="gap-3 grid w-full md:grid-cols-2">
<div class="b-1 b-border rounded-xl h-64 w-full overflow-hidden">
<SidebarFrame
isMobile={false}
side="left"
sidebarHeaderRender={() => <div class="text-xs p-3">side=left</div>}
sidebarBodyRender={() => (
<div class="text-sm text-muted-foreground p-2 h-full">Sidebar panel (left)</div>
)}
mainRender={() => <div class="text-sm p-3 h-full">Main panel</div>}
/>
</div>
<div class="b-1 b-border rounded-xl h-64 w-full overflow-hidden">
<SidebarFrame
isMobile={false}
side="right"
sidebarHeaderRender={() => <div class="text-xs p-3">side=right</div>}
sidebarBodyRender={() => (
<div class="text-sm text-muted-foreground p-2 h-full">Sidebar panel (right)</div>
)}
mainRender={() => <div class="text-sm p-3 h-full">Main panel</div>}
/>
</div>
</div>
)
}

SheetResizableRender#

Use SidebarFrameSheetResizableRender with external collapse button, collapsibleMin, and icon handle.

function SheetResizableRender() {
const [collapsed, setCollapsed] = createSignal(false)
return (
<div class="b-1 b-border rounded-xl h-72 w-full overflow-hidden">
<SidebarFrame
isMobile={false}
frameRender={(ctx) => (
<SidebarFrameSheetResizableRender
{...ctx}
resizablePanelOptions={{
defaultSize: '24%',
min: 100,
max: 200,
collapsible: collapsed(),
collapsibleMin: 56,
}}
resizableOptions={{
handleAction: 'collapse',
classes: {
divider:
'after:(transition duration-200 ease-out z-20) hover:after:(bg-accent w-1.5)',
},
}}
/>
)}
sidebarHeaderRender={() => <div class="text-sm p-3">Workspace</div>}
sidebarBodyRender={() => (
<div class="p-2 h-full overflow-y-auto">
<div class="flex flex-col gap-1">
<For each={ITEMS}>
{(item) => (
<button
type="button"
class="text-sm px-2.5 py-1.5 text-left rounded-md hover:bg-accent"
>
{item}
</button>
)}
</For>
</div>
</div>
)}
mainRender={() => (
<div class="p-4 flex flex-col gap-3 h-full">
<h3 class="text-base font-semibold">Resizable Desktop Frame</h3>
<p class="text-sm text-muted-foreground">
Drag divider to resize sidebar width. Click button to toggle collapse.
</p>
<p class="text-xs text-muted-foreground">collapsibleMin: 56px</p>
<div>
<Button
size="sm"
leading={collapsed() ? 'i-lucide:panel-left-open' : 'i-lucide:panel-left'}
onClick={() => setCollapsed((prev) => !prev)}
>
{collapsed() ? 'Expand Sidebar' : 'Collapse Sidebar'}
</Button>
</div>
</div>
)}
/>
</div>
)
}

ForcedMobile#

Force mobile mode and open the sidebar sheet from main content via ctx.toggle.

function ForcedMobile() {
return (
<div class="b-1 b-border rounded-xl h-72 overflow-hidden">
<SidebarFrame
isMobile
sidebarHeaderRender={() => <div class="text-sm p-3">Mobile Menu</div>}
sidebarBodyRender={(ctx) => (
<div class="text-sm p-3 h-full overflow-y-auto">
<p class="text-muted-foreground">This sidebar is rendered inside Sheet.</p>
<Button class="mt-3" variant="outline" onClick={() => ctx.setOpen(false)}>
Close
</Button>
</div>
)}
mainRender={(ctx) => (
<div class="p-4 flex flex-col gap-3 h-full">
<h3 class="text-base font-semibold">Forced Mobile Mode</h3>
<p class="text-sm text-muted-foreground">
Click the button below to open sidebar sheet via render context.
</p>
<Button variant="outline" onClick={ctx.toggle}>
Toggle Sidebar
</Button>
</div>
)}
/>
</div>
)
}

Use optional sidebarHeaderRender and sidebarFooterRender while keeping body as the scroll region.

function HeaderFooterSlots() {
return (
<div class="b-1 b-border rounded-xl h-72 w-full overflow-hidden">
<SidebarFrame
isMobile={false}
sidebarHeaderRender={() => (
<div class="p-3">
<p class="text-sm font-medium">Project Tasks</p>
<p class="text-xs text-muted-foreground mt-1">Header slot content</p>
</div>
)}
sidebarBodyRender={() => (
<div class="p-2">
<div class="flex flex-col gap-1">
<For each={TASKS}>
{(task) => (
<button
type="button"
class="text-sm px-2.5 py-1.5 text-left rounded-md hover:bg-accent"
>
{task}
</button>
)}
</For>
</div>
</div>
)}
sidebarFooterRender={() => (
<div class="p-2 b-t b-border bg-background/80 flex gap-2 items-center justify-between">
<span class="text-xs text-muted-foreground">12 tasks</span>
<Button size="sm" variant="ghost">
Footer Action
</Button>
</div>
)}
mainRender={() => (
<div class="p-4 h-full">
<h3 class="text-base font-semibold">Main Content</h3>
<p class="text-sm text-muted-foreground mt-2">
Sidebar header and footer are rendered from dedicated slots.
</p>
</div>
)}
/>
</div>
)
}

API Reference#

Attributes#

Slotroot2 attributes
Frame container that coordinates sidebar and main content layout.

Data Attributes

2
Data AttributeDescription
data-orientation
Stores the rendered orientation.
data-resizable-root
State or slot attribute exposed for styling hooks and selectors.

Props#

PropTypeDefaultDescription
classClassValue
Class applied to the component root or trigger element.
classesSidebarFrameT.Classes | undefined
frameRenderComponentOrElement<SidebarFrameT.FrameRenderProps> | undefinedSidebarFrameSheetOnlyRender
Optional frame renderer used to compose sidebar/main layout.
isMobileboolean | undefined
Controlled mobile mode state. When omitted, mobile state is resolved from `matchMedia`.
mainRender*ComponentOrElement<SidebarFrameT.MainRenderProps>
Render function for main content section.
refJSX.HTMLElementTags["div"] extends { ref?: infer Ref; } ? Ref : never | undefined
scrollThresholdnumber | undefined60
Scroll threshold for `scrolled` state.
side"right" | "left" | undefined
sidebarBodyRender*ComponentOrElement<SidebarFrameT.SidebarBodyRenderProps>
Render function for sidebar body section.
sidebarFooterRenderComponentOrElement<SidebarFrameT.SidebarFooterRenderProps> | undefined
Optional render function for sidebar footer section.
sidebarHeaderRenderComponentOrElement<SidebarFrameT.SidebarHeaderRenderProps> | undefined
Optional render function for sidebar header section.
styleJSX.CSSProperties | undefined
stylesSidebarFrameT.Styles | undefined
variant"default" | "floating" | "inset" | undefined