---
title: SidebarFrame
description: Sidebar and main frame with mobile Sheet support and desktop layout wrappers.
sidebar:
  order: 6
  badge: New
search:
  tags: [app shell, layout, navigation, mobile]
---

# SidebarFrame

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

## Import

```tsx
import { SidebarFrame, SidebarFrameSheetResizableRender } from 'moraine'
```

## Slot Structure

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

```text
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

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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`.

```tsx
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>
  )
}
```

### Header and Footer Slots

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

```tsx
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

#### `root`

Frame container that coordinates sidebar and main content layout.

##### Data Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| data-orientation | string \| undefined | Stores the rendered orientation. |
| data-resizable-root | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |

#### `sidebar`

Sidebar region rendered inline on desktop or inside a sheet on mobile.

##### Data Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| data-mobile | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
| data-side | string \| undefined | Stores the resolved floating content side. |

##### ARIA Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| aria-hidden | boolean \| string \| undefined | Hides decorative content from assistive technology. |

#### `sidebarHeader`

Optional header region at the top of the sidebar.

#### `sidebarBody`

Main sidebar content region.

#### `sidebarFooter`

Optional footer region at the bottom of the sidebar.

#### `main`

Primary content region beside or beneath the sidebar.

### Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| class | ClassValue | — | Class applied to the component root or trigger element. |
| classes | SidebarFrameT.Classes \| undefined | — | — |
| frameRender | ComponentOrElement<SidebarFrameT.FrameRenderProps> \| undefined | SidebarFrameSheetOnlyRender | Optional frame renderer used to compose sidebar/main layout. |
| isMobile | boolean \| undefined | — | Controlled mobile mode state.<br>When omitted, mobile state is resolved from `matchMedia`. |
| mainRender* | ComponentOrElement<SidebarFrameT.MainRenderProps> | — | Render function for main content section. |
| ref | JSX.HTMLElementTags["div"] extends { ref?: infer Ref; } ? Ref : never \| undefined | — | — |
| scrollThreshold | number \| undefined | 60 | Scroll threshold for `scrolled` state. |
| side | "right" \| "left" \| undefined | — | — |
| sidebarBodyRender* | ComponentOrElement<SidebarFrameT.SidebarBodyRenderProps> | — | Render function for sidebar body section. |
| sidebarFooterRender | ComponentOrElement<SidebarFrameT.SidebarFooterRenderProps> \| undefined | — | Optional render function for sidebar footer section. |
| sidebarHeaderRender | ComponentOrElement<SidebarFrameT.SidebarHeaderRenderProps> \| undefined | — | Optional render function for sidebar header section. |
| style | JSX.CSSProperties \| undefined | — | — |
| styles | SidebarFrameT.Styles \| undefined | — | — |
| variant | "default" \| "floating" \| "inset" \| undefined | — | — |

### ARIA

Accessibility attributes and roles emitted by the component markup.

| Attribute | Type | Description |
| --- | --- | --- |
| aria-controls | boolean \| string \| undefined | References the controlled element while the related content is mounted. |
| aria-describedby | boolean \| string \| undefined | References descriptive text associated with the control. |
| aria-disabled | boolean \| string \| undefined | Indicates that the control is disabled. |
| aria-hidden | boolean \| string \| undefined | Hides decorative content from assistive technology. |
| aria-label | boolean \| string \| undefined | Provides an accessible label when visible text is not sufficient. |
| aria-labelledby | boolean \| string \| undefined | References the element that labels the control or region. |
| aria-modal | boolean \| string \| undefined | Identifies modal content that traps interaction outside the dialog. |
| aria-orientation | boolean \| string \| undefined | Communicates horizontal or vertical orientation. |
| aria-valuemax | boolean \| string \| undefined | Defines the maximum value for range-like controls. |
| aria-valuemin | boolean \| string \| undefined | Defines the minimum value for range-like controls. |
| aria-valuenow | boolean \| string \| undefined | Defines the current numeric value for range-like controls. |
| role | string | Defines the semantic role exposed to assistive technology. |

### Data Attributes

State and slot attributes exposed for styling hooks and selectors.

| Attribute | Type | Description |
| --- | --- | --- |
| data-active | string \| undefined | Present when the item is active. |
| data-collapsed | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
| data-cross | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
| data-dragging | string \| undefined | Present while the related thumb or handle is being dragged. |
| data-expanded | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
| data-mobile | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
| data-orientation | string \| undefined | Stores the rendered orientation. |
| data-resizable-handle-end-target | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
| data-resizable-handle-start-target | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
| data-resizable-root | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
| data-resizing | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
| data-side | string \| undefined | Stores the resolved floating content side. |
| data-slot | string | Identifies the rendered slot for styling hooks and selectors. |
| data-transitioning | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
