Layouts
The shell around every page is one of four layouts, chosen by config or by a route.
The four
| Layout | Shape |
|---|---|
classic | Fixed sidebar, sticky navbar, footer, as a two-column grid. |
modern | The same grid with a floating sidebar and a transparent navbar. The differences are visual only. This is the default. |
minimal | Navbar only, no sidebar. The hamburger stays at every width, because dropping the sidebar would otherwise leave a desktop with no route to the menu. |
horizontal | A top strip instead of a sidebar. Its top level is the sidebar's section captions, each opening a dropdown of that section's items. |
Two more exist for routes rather than config: auth is a centered card on the app canvas for the sign-in pages, and blank is no chrome at all, for print views and embedded widgets.
Choosing one
import { useConfig } from '@core/config/useConfig'
useConfig().setUi('layout', 'classic')A route can override it for itself:
{
"path": "/invoices/:id/print",
"name": "invoice-print",
"meta": { "layout": "blank", "title": "invoice.print" }
}The resolution order is route.meta.layout, then config.ui.layout, then classic.
import { useLayout } from '@/layouts/useLayout'
const { resolvedLayout, setLayout } = useLayout()setLayout writes the persisted choice and accepts only the four config layouts; auth and blank are route-meta only.
Layouts are imported eagerly, not lazily. A layout the reader can switch from the settings drawer has to change instantly, and a loading flash on every switch would be worse than the bytes.
Responsive behavior
One composable decides what "mobile" means, and everything in the shell reads it.
| Width | Sidebar |
|---|---|
| Below 768px | Not in the layout at all. An off-canvas drawer replaces it. |
| 768 to 1023px | Present, forced to the icon rail. |
| 1024px and up | Honors ui.sidebarStyle. |
The tablet rail is derived, never written. Writing sidebarStyle: 'collapsed' on a resize would overwrite the reader's stored desktop preference and survive the reload — which is the bug that rule exists to prevent.
import { useResponsiveLayout } from '@/layouts/useResponsiveLayout'
const {
isMobile,
isTablet,
isDesktop,
mobileNavOpen,
openMobileNav,
closeMobileNav,
railFor,
toggleSidebar,
} = useResponsiveLayout()| Member | Description |
|---|---|
isMobile | Below 768px. |
isTablet | 768 to 1023px. |
isDesktop | 1024px and up. |
mobileNavOpen | The drawer's state, module-scope, so the navbar's hamburger and the drawer are the same switch whichever layout composed them. |
openMobileNav / closeMobileNav | Open and close it. |
railFor(configuredCollapsed) | Whether the sidebar should render as the icon rail right now. |
toggleSidebar() | On desktop with a sidebar, flips ui.sidebarStyle. Anywhere else, toggles the drawer. |
t then s toggles the sidebar from the keyboard.
Sidebar options
| Key | Values | Effect |
|---|---|---|
ui.sidebarPosition | left, right | Which side it sits on, independently of reading direction. |
ui.sidebarStyle | expanded, collapsed, floating | Its shape at 1024px and up. |
ui.navbarStyle | sticky, static | Whether the navbar scrolls away with the content. |
In a right-to-left layout the grid mirrors on its own. sidebarPosition is a separate, direction-independent override, so an Arabic reader who wants the sidebar on the left can have it.
The scroll container
The window never scrolls in this shell. Each layout scrolls its own column, marked data-sa-scroll-root, which is what the sticky navbar anchors to. The router's scrollBehavior scrolls that element rather than the window, and caches each route's offset so the browser back button lands where the reader left.
This matters when you write a page: a position: fixed element is positioned against the viewport, not that column, and window.scrollTo does nothing.
Slots
classic and modern take the shared set: #sidebar, #navbar, #footer and the default slot. auth takes #brand, the default slot and #footer.
Every layout wraps its content in an ErrorBoundary, so a page that throws shows a recoverable panel instead of a blank shell.
Where to go next
- Navigation — the tree the sidebar and the top strip both read.
- Global config — the
uikeys in full.