Layout
Overview
The layout component structure for admin dashboards or complex applications. It manages sidebar, header, footer, tabs, and main content areas.
Features
- One unified layout —
SLayoutcombines the modern sidebar shell with admin-classic capabilities: scrollable wrapper, fixed header/footer, and orientation support. - Three variants —
sidebar(bordered),floating(rounded shadow), andinset(content with margin and rounded corners). - Collapsible sidebar —
collapsible="icon"collapses the sidebar to a rail width;collapsible="offcanvas"slides it off-canvas while preserving layout space. - Side control —
side="left"orside="right"flips the sidebar position with full RTL-aware logical properties. - Mobile drawer — unset
isMobilefollows the viewport and swaps the desktop sidebar for aDialog-based drawer with overlay and focus trap; pass a boolean to override the breakpoint. - Slot-level overrides — every region (sidebar, header, tab, content, footer) accepts per-slot
*Propsfor granular attribute forwarding. - CSS-variable driven — dimensions (
sidebarWidth,headerHeight,tabHeight,footerHeight) emit rem-based CSS variables for runtime customization. - Size scaling —
size(xs…2xl) scales the layout spacing and base typography throughthemeSizeRatio. fullContentmode — pins the content area to fill the viewport while keeping the tab bar above it.- Orientation —
Layoutsupports horizontal (sidebar beside content) and vertical (sidebar stacked under header) orientations. - Scroll behaviors —
scrollBehavior="content"scrolls only the content region;scrollBehavior="wrapper"scrolls the entire main wrapper. - Fixed header/footer —
fixedTopandfixedFooterkeep the header/footer pinned during content scroll, with automatic placeholder elements to prevent overlap. - Base z-index control —
baseZIndexderives the stacking order of sidebar, header, tab, and footer so multiple layouts compose predictably. - Aria composition — every region (
LayoutRoot,LayoutSidebar,LayoutRail,LayoutHeader,LayoutTab,LayoutContent,LayoutFooter,LayoutMobile,LayoutTrigger) is exported from@vean/aria/layoutfor custom styled builds. - SSR safe — no
window/documentaccess in setup; the viewport fallback is guarded, anduseId()generates stable scroll ids for server rendering.
Usage
Demos
Basic
API
Aria Layer
13Layout
Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | ClassValue | — | Additional class names applied to the root element. |
size | ThemeSize | — | Visual size of the component. |
ui | Partial<LayoutUi> | — | Per-slot class overrides for the component. |
open | boolean | — | The controlled open state of the layout. Can be bound with `v-model`. |
defaultOpen | boolean | — | The open state of the layout when it is initially rendered.
Use when you do not need to control its open state. |
side | HorizontalSide | — | The side of the layout. |
variant | LayoutVariant | — | The variant of the layout. |
collapsible | LayoutCollapsible | — | The collapsible state of the layout. |
sidebarVisible | boolean | true | whether to show the sidebar. |
sidebarWidth | number | 240 | The width of the sidebar. (px) |
collapsedSidebarWidth | number | 50 | The width of the sidebar when it is collapsed. (px) |
isMobile | boolean | — | Whether the layout is in mobile view.
Resolution order: this prop, then a host viewport decision provided through
`provideViewportContext` (a simulated viewport — a device frame, an embedded
shell), then `useMediaQuery(mobileViewportQuery)` — the same breakpoint the
styled desktop sidebar is hidden at. Pass an explicit boolean to force the
mode (e.g. a server-side detection); `false` re-enables the inline sidebar
even below the breakpoint. |
mobileOpen | boolean | — | The controlled open state of the mobile drawer. Can be bound with
`v-model:mobileOpen`.
Kept apart from `open`, which drives the desktop sidebar: on mobile the
inline sidebar is replaced by a drawer, so the two states never apply at the
same time. |
defaultMobileOpen | boolean | false | The open state of the mobile drawer when it is initially rendered. |
mobileSidebarWidth | number | 240 | The width of the sidebar in the mobile view. (px) |
headerVisible | boolean | true | whether to show the header. |
headerHeight | number | 56 | The height of the header. (px) |
tabVisible | boolean | true | whether to show the tab. |
tabHeight | number | 44 | The height of the tab. (px) |
footerVisible | boolean | true | whether to show the footer. |
footerHeight | number | 48 | The height of the footer. (px) |
fullContent | boolean | false | whether the content takes the full height of the layout (include). |
pxToRem | ((px: number) => number) | (px: number) => px / 16 (16 is the base font size) | The function to convert pixels to rem. |
orientation | DataOrientation | — | Orientation of the component. |
scrollBehavior | LayoutScrollBehavior | — | Scroll behavior. |
scrollId | string | — | Scroll id. |
baseZIndex | number | 50 | The base z-index of the layout. The z-index of the sidebar, header, tab, footer, and their fixed versions will be calculated based on this value. |
fixedTop | boolean | true | Whether the header and tab are fixed to the top of the layout when the orientation is vertical. If true, the header and tab will be fixed to the top of the layout when the orientation is vertical, and will scroll with the content when the orientation is horizontal. |
fixedFooter | boolean | true | Whether footer is fixed |
stretchFooter | boolean | true | Whether the footer should stretch to the full width of the layout or the content when layout orientation is vertical. |
sidebarProps | LayoutSidebarProps | — | — |
railProps | LayoutRailProps | — | — |
mainProps | LayoutMainProps | — | — |
headerProps | LayoutHeaderProps | — | — |
tabProps | LayoutTabProps | — | — |
contentProps | LayoutContentProps | — | — |
footerProps | LayoutFooterProps | — | — |
mobileProps | LayoutMobileProps | — | — |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:open | boolean | Emitted when the open state changes. |
update:mobileOpen | boolean | Emitted when the mobile drawer open state changes. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
default | — | Custom content for the default slot. |
sidebar | { open: boolean | undefined; collapsedSidebarWidth: number; collapsed: boolean; } | Custom content for the sidebar slot.
`collapsed` is the mode-aware state to render from: it is `false` on mobile,
where the sidebar is a drawer that always shows the expanded navigation, so a
collapse chosen on desktop does not follow the content into the drawer.
`open` still reports the desktop state. |
header | — | Custom content for the header slot. |
tab | — | Custom content for the tab slot. |
content | — | Custom content for the content slot. |
footer | — | Custom content for the footer slot. |
LayoutCompact
Props
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | The controlled open state of the layout. Can be bound with `v-model`. |
defaultOpen | boolean | — | The open state of the layout when it is initially rendered.
Use when you do not need to control its open state. |
side | HorizontalSide | — | The side of the layout. |
variant | LayoutVariant | — | The variant of the layout. |
collapsible | LayoutCollapsible | — | The collapsible state of the layout. |
sidebarVisible | boolean | true | whether to show the sidebar. |
sidebarWidth | number | 240 | The width of the sidebar. (px) |
collapsedSidebarWidth | number | 50 | The width of the sidebar when it is collapsed. (px) |
isMobile | boolean | — | Whether the layout is in mobile view.
Resolution order: this prop, then a host viewport decision provided through
`provideViewportContext` (a simulated viewport — a device frame, an embedded
shell), then `useMediaQuery(mobileViewportQuery)` — the same breakpoint the
styled desktop sidebar is hidden at. Pass an explicit boolean to force the
mode (e.g. a server-side detection); `false` re-enables the inline sidebar
even below the breakpoint. |
mobileOpen | boolean | — | The controlled open state of the mobile drawer. Can be bound with
`v-model:mobileOpen`.
Kept apart from `open`, which drives the desktop sidebar: on mobile the
inline sidebar is replaced by a drawer, so the two states never apply at the
same time. |
defaultMobileOpen | boolean | false | The open state of the mobile drawer when it is initially rendered. |
mobileSidebarWidth | number | 240 | The width of the sidebar in the mobile view. (px) |
headerVisible | boolean | true | whether to show the header. |
headerHeight | number | 56 | The height of the header. (px) |
tabVisible | boolean | true | whether to show the tab. |
tabHeight | number | 44 | The height of the tab. (px) |
footerVisible | boolean | true | whether to show the footer. |
footerHeight | number | 48 | The height of the footer. (px) |
fullContent | boolean | false | whether the content takes the full height of the layout (include). |
pxToRem | ((px: number) => number) | (px: number) => px / 16 (16 is the base font size) | The function to convert pixels to rem. |
orientation | DataOrientation | — | Orientation of the component. |
scrollBehavior | LayoutScrollBehavior | — | Scroll behavior. |
scrollId | string | — | Scroll id. |
baseZIndex | number | 50 | The base z-index of the layout. The z-index of the sidebar, header, tab, footer, and their fixed versions will be calculated based on this value. |
fixedTop | boolean | true | Whether the header and tab are fixed to the top of the layout when the orientation is vertical. If true, the header and tab will be fixed to the top of the layout when the orientation is vertical, and will scroll with the content when the orientation is horizontal. |
fixedFooter | boolean | true | Whether footer is fixed |
stretchFooter | boolean | true | Whether the footer should stretch to the full width of the layout or the content when layout orientation is vertical. |
sidebarProps | LayoutSidebarProps | — | — |
railProps | LayoutRailProps | — | — |
mainProps | LayoutMainProps | — | — |
headerProps | LayoutHeaderProps | — | — |
tabProps | LayoutTabProps | — | — |
contentProps | LayoutContentProps | — | — |
footerProps | LayoutFooterProps | — | — |
mobileProps | LayoutMobileProps | — | — |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:open | boolean | Emitted when the open state changes. |
update:mobileOpen | boolean | Emitted when the mobile drawer open state changes. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
default | — | Custom content for the default slot. |
sidebar | { open: boolean | undefined; collapsedSidebarWidth: number; collapsed: boolean; } | Custom content for the sidebar slot.
`collapsed` is the mode-aware state to render from: it is `false` on mobile,
where the sidebar is a drawer that always shows the expanded navigation, so a
collapse chosen on desktop does not follow the content into the drawer.
`open` still reports the desktop state. |
header | — | Custom content for the header slot. |
tab | — | Custom content for the tab slot. |
content | — | Custom content for the content slot. |
footer | — | Custom content for the footer slot. |
LayoutContent
LayoutFooter
LayoutHeader
LayoutMain
LayoutMobile
LayoutPlaceholder
Props
| Prop | Type | Default | Description |
|---|---|---|---|
type | 'header' | 'tab' | 'footer' | — | Which region the placeholder reserves space for. |
LayoutRail
LayoutRoot
Props
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | The controlled open state of the layout. Can be bound with `v-model`. |
defaultOpen | boolean | — | The open state of the layout when it is initially rendered.
Use when you do not need to control its open state. |
side | HorizontalSide | — | The side of the layout. |
variant | LayoutVariant | — | The variant of the layout. |
collapsible | LayoutCollapsible | — | The collapsible state of the layout. |
sidebarVisible | boolean | true | whether to show the sidebar. |
sidebarWidth | number | 240 | The width of the sidebar. (px) |
collapsedSidebarWidth | number | 50 | The width of the sidebar when it is collapsed. (px) |
isMobile | boolean | — | Whether the layout is in mobile view.
Resolution order: this prop, then a host viewport decision provided through
`provideViewportContext` (a simulated viewport — a device frame, an embedded
shell), then `useMediaQuery(mobileViewportQuery)` — the same breakpoint the
styled desktop sidebar is hidden at. Pass an explicit boolean to force the
mode (e.g. a server-side detection); `false` re-enables the inline sidebar
even below the breakpoint. |
mobileOpen | boolean | — | The controlled open state of the mobile drawer. Can be bound with
`v-model:mobileOpen`.
Kept apart from `open`, which drives the desktop sidebar: on mobile the
inline sidebar is replaced by a drawer, so the two states never apply at the
same time. |
defaultMobileOpen | boolean | false | The open state of the mobile drawer when it is initially rendered. |
mobileSidebarWidth | number | 240 | The width of the sidebar in the mobile view. (px) |
headerVisible | boolean | true | whether to show the header. |
headerHeight | number | 56 | The height of the header. (px) |
tabVisible | boolean | true | whether to show the tab. |
tabHeight | number | 44 | The height of the tab. (px) |
footerVisible | boolean | true | whether to show the footer. |
footerHeight | number | 48 | The height of the footer. (px) |
fullContent | boolean | false | whether the content takes the full height of the layout (include). |
pxToRem | ((px: number) => number) | (px: number) => px / 16 (16 is the base font size) | The function to convert pixels to rem. |
orientation | DataOrientation | — | Orientation of the component. |
scrollBehavior | LayoutScrollBehavior | — | Scroll behavior. |
scrollId | string | — | Scroll id. |
baseZIndex | number | 50 | The base z-index of the layout. The z-index of the sidebar, header, tab, footer, and their fixed versions will be calculated based on this value. |
fixedTop | boolean | true | Whether the header and tab are fixed to the top of the layout when the orientation is vertical. If true, the header and tab will be fixed to the top of the layout when the orientation is vertical, and will scroll with the content when the orientation is horizontal. |
fixedFooter | boolean | true | Whether footer is fixed |
stretchFooter | boolean | true | Whether the footer should stretch to the full width of the layout or the content when layout orientation is vertical. |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:open | boolean | Emitted when the open state changes. |
update:mobileOpen | boolean | Emitted when the mobile drawer open state changes. |
LayoutSidebar
LayoutTab
LayoutTrigger
Props
| Prop | Type | Default | Description |
|---|---|---|---|
type | ButtonType | 'button' | The type of the button element. Can be one of 'button', 'submit', or 'reset'. |
disabled | boolean | — | Whether the component is disabled. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Notes
Architecture and benchmark comparison
| Concern | VeanUI | Ant Design Layout/Header/Sider/Content/Footer | Element Plus ElContainer/ElHeader/ElAside/ElMain/ElFooter |
|---|---|---|---|
| Aria / styled separation | ✅ @vean/aria/layout ships logic + structure; @vean/ui ships scv() recipes | ❌ single styled package | ❌ single styled package |
| Sidebar variants | sidebar / floating / inset | sider only | aside only |
| Collapsible modes | icon (rail) + offcanvas (slide out) | collapsible + collapsedWidth | — |
| Mobile drawer | built-in Dialog-based drawer (follows the viewport) | requires Drawer composition | requires Drawer composition |
| Fixed header/footer | Layout with fixedTop / fixedFooter + automatic placeholders | requires manual sticky CSS | requires manual sticky CSS |
| Orientation | Layout orientation="horizontal" | "vertical" | — | — |
| Scroll behavior | wrapper / content on Layout | — | — |
| CSS-variable dimensions | --vean-sidebar-width, --vean-layout-header-height, etc. | inline width on Sider | inline width on Aside |
| RTL support | logical properties (start-*, end-*, ps-*, pe-*) + rtl: variants on rail | — | — |
| Z-index orchestration | baseZIndex derives sidebar/header/tab/footer z-index | manual | manual |
| Region visibility | sidebarVisible / headerVisible / tabVisible / footerVisible props | remove the component | remove the component |
Runtime considerations
- CSS variables are rem-based —
sidebarWidth,collapsedSidebarWidth,headerHeight,tabHeight,footerHeight, andmobileSidebarWidthare converted viapxToRem(defaultpx / 16). Pass a custompxToRemto align with a non-default root font size. sizescales spacing and typography — the UI wrapper multiplies pixel dimensions bythemeSizeRatio[size] / themeSizeMap.md, sosize="xs"shrinks both text and sidebar width proportionally.isMobileresolves through three levels — the prop, then a viewport a host publishes withprovideViewportContext(a documentation device frame, an embedded shell), thenuseMediaQuery(mobileViewportQuery), the breakpoint shared with the styled sidebar’slt-mdhiding rule (767.9px). The winning source is published asdata-mobile-source="explicit|viewport", and the styledlt-mdfallback only applies toviewport: an explicitisMobile="false"below the breakpoint keeps the inline sidebar and its reserved width, instead of reserving space for a sidebar the CSS had hidden. The drawer is teleported out of the layout, so on mobile the sidebar reserves no space: the start gaps (--vean-layout-start-gap,--vean-layout-header-start-gap,--vean-layout-footer-start-gap) collapse to0, and each variant falls back to its own end gap —sidebar/floatingbecome full-bleed,insetstays inset symmetrically on both edges. The same fallback coverssidebarVisible="false"on desktop: a hidden sidebar reserves no space either, so the extra spacingfloating/insetadd beside the sidebar width is withdrawn with it, and the main column stays aligned with the fixed header and footer. A host that resolves the sidebar to no column at all —SAppShell’s split modes do, while a first-level leaf is active — declaressidebarWidth="0"instead: the root then reportsdata-sidebar-flow="false", the styled layer withdraws the same spacing and paints no column, and because the region itself stays mounted (its mount targets host teleported panes), only the column disappears, not the subtree.- The
sidebarslot renders from a mode-awarecollapsed— the slot hands its contentcollapsednext toopen: it isfalseon mobile, where the sidebar is a drawer that always shows the expanded navigation, so a collapse picked on desktop does not follow the content into the drawer. Render fromcollapsed(a menu’scollapsed, a brand title’s visibility);openkeeps reporting the desktop state, and the collapse returns the moment the mode does. LayoutTriggervsLayoutRail—LayoutTriggeris a focusable button in the header for keyboard users;LayoutRailis the edge drag affordance withtabindex="-1"(click-only). Both reflectaria-expandedfor whichever sidebar the current mode renders — on mobile that is the drawer, not the desktopopenstate.Layoutplaceholder elements — whenfixedToporfixedFooteris enabled,LayoutPlaceholderrenders empty spacer divs (data-vean-layout-{header|tab|footer}-placeholder) to prevent content from sliding under the fixed region. Hiding a region removes its placeholder with it, and the fixed regions below close the gap: with the header hidden the tab takes the layout’s top edge instead of keeping the header’s height (insetkeeps only its own half-spacing).scrollIdfor scroll restoration —Layoutgenerates a stablesoybean-layout-scroll-{id}on the scrolling element (wrapper or content depending onscrollBehavior). PassscrollIdto make it deterministic across SSR/CSR.
FAQ
Which layout mode should I use?
Use SLayout for both modern application shells and admin dashboards. It handles fixed header/footer, orientation switching (horizontal / vertical), and wrapper-level scrolling with placeholder spacers through a single unified component.
How do I control the sidebar open state?
Use v-model:open (controlled) or default-open (uncontrolled). The state is reflected on the root via data-state="expanded|collapsed" and on LayoutTrigger/LayoutRail via aria-expanded.
On mobile the inline sidebar is replaced by a drawer, which carries its own state: bind v-model:mobileOpen (or default-mobile-open uncontrolled) to drive it. open keeps controlling the desktop sidebar, so a host that pins isMobile reaches what the user sees through mobileOpen — and the trigger reports that state. A single open binding cannot mean both, because the desktop sidebar defaults to expanded while the drawer has to start closed.
How do I make the sidebar collapse to icons instead of sliding away?
Set collapsible="icon" (default) and collapsedSidebarWidth to the rail width. The sidebar shrinks to the collapsed width and the sidebarGapHandler adjusts the main area accordingly. Use collapsible="offcanvas" to slide the sidebar off-canvas instead.
How does mobile mode work?
Leave isMobile unset (the default) and the layout follows the viewport, swapping the desktop sidebar for a Dialog-based drawer; pass a boolean to pin the mode yourself. The drawer inherits mobileSidebarWidth and reuses the same sidebar slot content. The drawer overlay and focus trap are provided by the underlying Dialog component.
Can a host decide the mode for a whole subtree?
Yes. A host that already knows the answer — a device preview frame rendering a phone, a shell embedded in a desktop app, an SSR pass with a device hint — publishes it once with provideViewportContext({ isMobile }) from @vean/aria/composables; every layout below it follows, and an explicit isMobile prop still wins per instance. Publishing at the frame rather than per component is also what keeps a documentation preview honest: the frame can simulate a phone viewport while the browser window stays wide. Note that viewport-prefixed utilities (lt-md:hidden, sm:, md:) still key off the browser window, so a simulated viewport moves the JS structure (drawer vs. inline sidebar) and the prop-driven spacing, not those breakpoint classes.
Can I render the sidebar on the right?
Yes — set side="right". The layout uses RTL-aware logical properties (start-*, end-*, border-s, border-e) so the sidebar, gap handler, rail cursor, and fixed header/footer insets all flip correctly.
How are z-index values coordinated?
Layout accepts a baseZIndex (default 50). The sidebar, header, tab, and footer z-index values are derived from this base so they stack predictably. The derived values are exposed as --layout-{sidebar|header|tab|footer}-z-index CSS variables.
How do I customize region-level attributes?
Each region accepts a *Props prop on the compact component (e.g. sidebarProps, headerProps, tabProps, contentProps, footerProps, mainProps, railProps, mobileProps). These are forwarded to the corresponding Aria region component.