This page is utilizing the holy grail layout, but you can use the buttons below to toggle layouts and test them out by resizing the screen. Try the "Toggle container labels" button below to see the css classes on each of the containers inside .layout.
Thankfully, no one needs to be expert to use the .layout element. But it does help to understand a few important concepts before beginning.
The .layout class is designed to be placed on the <html> element itself, while the layout-body element is designed to be placed on the <body> element. All other elements are children of layout-body. Specific layouts are applied by adding a class with the format layout-{nameoflayout} to the html.layout element.
Nearly all layouts have the same eight elements, and the layout-body-hero element is always optional. While the layout, layout-body, layout-body-header, layout-body-footer, and layout-body-main elements are always required. The behavior of layout-body-menu and layout-body-aside depend on the layout.
The basic boilerplate for a layout is as follows:
<html class="layout layout-{nameoflayout}">
<body class="layout-body">
<header class="layout-body-header">Header</header>
<section aria-label="hero" class="layout-body-hero">Hero</section>
<nav class="layout-body-menu">Menu</nav>
<main class="layout-body-main">Main</main>
<aside class="layout-body-aside">Aside</aside>
<footer class="layout-body-footer">Footer</footer>
</body>
</html>
The $layout-gap token is used to generate two separate classes that can be used to apply spacing to containers within each of the major layout areas. Spacing should not be applied to the layout-body-* containers themselves, but rather to elements within them - most often a single container that is a direct child of the layout-body-* element.
| Class name | Use case | Examples of use case |
|---|---|---|
.layout-padding |
Layout areas that are not full width | The menu and aside areas. The main area when not in a single layout |
.layout-margin |
Full width layout areas | The header, footer, and hero. The main area when it is full width in a single layout. |
.layout-padding-tablet |
Same as .layout-padding but only applied on tablet breakpoint and above. |
|
.layout-margin-tablet |
Same as .layout-margin but only applied on tablet breakpoint and above. |
Why not use gap? You certainly can apply any gap atomics to the layout-body to change spacing as you see fit. However, to provide adequate space to display focus rectangles and to ensure enough space to render dynamic inline elements (like heading anchors), spacing applied to an inner container is still the best choice.
The layout components behavior is inextricably bound to Atlas's breakpoints. On narrow widths on all layouts, elements are stacked on top of one another. If you don't want an element to show on a particular screensize, you should either render nothing into it or hide the inner element.
There are five available layouts.
The layout-single class provides a simple stacked layout. It is equivalent to a series of marginless and paddingless stacked block level elements. All elements in a single layout should set their own margin somewhere within their nested containers. Most likely, you'll want to apply .layout-margin or some other container-like class to elements within the single layout. This is also the default layout that will render if no other class is provide.
How do I apply it? layout-single to the layout element.
Required elements: all except layout-body-hero.
Allowed elements: all.
Can have its height constrained? No.
Can have its menu collapsed? No.
Can have its aside collapsed? No.
Narrow
┌────────────┐
│ Header │
├────────────┤
│ Hero │
├────────────┤
│ Menu │
├────────────┤
│ Main │
├────────────┤
│ Aside │
├────────────┤
│ Footer │
└────────────┘
The specification for this layout is as follows.
| Screensize | Behavior |
|---|---|
| Narrow - Widescreen | All elements are stacked. |
Named after a layout that historically was difficult to implement the holy grail is stacked on narrow widths and progressively shows up to three columns as the screen widens.
How do I apply it? layout-holy-grail to the layout element.
Required elements: all except layout-body-hero.
Allowed elements: all.
Can have its height constrained? Yes, on desktop screens and wider. This differs from other constrainable layouts, because on tablet aside wraps under main, preventing us from constraining main's height effectively.
Can have its menu collapsed? Yes, on tablet screens and wider.
Can have its aside collapsed? Yes, on desktop screens and wider.
The following block is arranged from narrow widths on the left to wider widths on the right.
Narrow Tablet Desktop
┌────────────┐ ┌──────────────┐ ┌──────────────────────┐
│ Header │ │Header │ │ Header │
├────────────┤ ├──────────────┤ ├──────────────────────┤
│ Hero │ │Hero │ │ Hero │
├────────────┤ ├─────┬────────┤ ├─────┬────────┬───────┤
│ Menu │ │Menu │ Main │ │Menu │ Main │ Aside │
├────────────┤ │ │ │ │ │ │ │
│ Main │ │ │ │ │ │ │ │
├────────────┤ │ ├────────┤ │ │ │ │
│ Aside │ │ │ Aside │ │ │ │ │
├────────────┤ ├─────┴────────┤ ├─────┴────────┴───────┤
│ Footer │ │Footer │ │ Footer │
└────────────┘ └──────────────┘ └──────────────────────┘
The specification for this layout is as follows.
| Screensize | Behavior |
|---|---|
| Narrow | All elements are stacked. |
| Tablet | Menu and main are side by side. Main is wider than menu. Aside has a collapsed height and is tucked under main. |
| Desktop | Menu, main, aside are side by side. Main is wider than menu. |
A "sidecar" is a smaller companion container that sits beside the main content container on tablet screens and wider. In layout-sidecar-left the sidecar refers to the layout-body-menu element, which sits to the left of layout-body-main. Unlike other layouts, this layout does not allow the usage of the layout-body-aside containers.
How do I apply it? layout-sidecar-left to the layout element.
Required elements: all except layout-body-hero and layout-body-aside (see allowed elements).
Allowed elements: all except layout-body-aside.
Can have its height constrained? Yes, on tablet screens and wider.
Can have its menu collapsed? Yes, on tablet screens and wider.
Can have its aside collapsed? No (aside is not present).
The following block is arranged from narrow widths on the left to wider widths on the right.
Narrow Tablet and wider
┌────────────┐ ┌──────────────┐
│ Header │ │Header │
├────────────┤ ├──────────────┤
│ Hero │ │Hero │
├────────────┤ ├─────┬────────┤
│ Menu │ │Menu │ Main │
├────────────┤ │ │ │
│ Main │ │ │ │
│ │ │ │ │
│ │ │ │ │
├────────────┤ ├─────┴────────┤
│ Footer │ │Footer │
└────────────┘ └──────────────┘
The specification for this layout is as follows.
| Screensize | Behavior |
|---|---|
| Narrow | All elements are stacked. |
| Tablet and wider | Menu and main are side by side. Main is wider than menu. |
The "sidecar" remains as defined in the section above, but in layout-sidecar-right the sidecar refers to the layout-body-aside element, which sits to the right of layout-body-main on tablet screens and wider. Unlike other layouts, this layout does not allow the usage of the layout-body-menu containers.
How do I apply it? layout-sidecar-right to the layout element.
Required elements: all except layout-body-hero and layout-body-menu (see allowed elements).
Allowed elements: all except layout-body-menu.
Can have its height constrained? Yes, on tablet screens and wider.
Can have its menu collapsed? No.
Can have its aside collapsed? Yes, on tablet screens and wider.
The following block is arranged from narrow widths on the left to wider widths on the right.
Narrow Tablet and wider
┌──────────────┐ ┌──────────────────────┐
│Header │ │ Header │
├──────────────┤ ├──────────────────────┤
│Hero │ │ Hero │
├──────────────┤ ├───────────────┬──────┤
│Main │ │ Main │ Aside│
│ │ │ │ │
│ │ │ │ │
├──────────────┤ │ │ │
│Aside │ │ │ │
├──────────────┤ ├───────────────┴──────┤
│Footer │ │ Footer │
└──────────────┘ └──────────────────────┘
The specification for this layout is as follows.
| Screensize | Behavior |
|---|---|
| Narrow | All elements are stacked. |
| Tablet and wider | Main and aside are side by side. Main is wider than aside. |
On tablet screens and large the twin layout allows two containers (main and aside) to share the central part of the screen. Unlike some of the other containers these two containers continue to grow beyond the boundaries of the widescreen breakpoint. This means the text-based content rendered in either should consider setting a maximum width to ensure readability. This layout is ideal for a side-by-side experience, where a user might read an article in main and perform some interactive task, such as editing code, in aside.
How do I apply it? layout-twin to the layout element.
Required elements: all except layout-body-hero and layout-body-menu (see allowed elements).
Allowed elements: all except layout-body-menu.
Can have its height constrained? Yes, on tablet screens and wider.
Can have its aside collapsed? Yes, on tablet screens and wider.
The following block is arranged from narrow widths on the left to wider widths on the right.
Narrow Tablet and wider
┌──────────────┐ ┌──────────────────────┐
│Header │ │ Header │
├──────────────┤ ├──────────────────────┤
│Hero │ │ Hero │
├──────────────┤ ├───────────┬──────────┤
│Main │ │ Main │ Aside│
│ │ │ │ │
│ │ │ │ │
├──────────────┤ │ │ │
│Aside │ │ │ │
├──────────────┤ ├───────────┴───-──────┤
│Footer │ │ Footer │
└──────────────┘ └──────────────────────┘
The specification for this layout is as follows.
| Screensize | Behavior |
|---|---|
| Narrow | All elements are stacked. |
| Tablet and wider | Main and aside are side by side. Main and aside are the same width |
If you'd like central containers to scroll individually, instead of the page itself, you have to constrain the height of the layout to the viewport. In order to do that, you can add the .layout-constrained class to the html.layout element. This has no effect on narrow screens or on the single layout, but will work on most other layouts on tablet and larger (the exception being holy grail, which kicks in on desktop widths). See each layout's individual section for behavior specific to it.
This requires the use of some very lightweight client JavaScript to updates a few values on the HTML element as the screen resizes. This means you must install @microsoft/atlas-js and import and call the initLayout in your client scripts for this behavior to work. See how this site does it on GitHub.
When constraint is active the height of the page is calculated to be 100vh (i.e. the size of the viewport) plus the size of the hero. This means:
- Main (and any container horizontally adjacent to it) will have a height equal to the viewport height minus the footer's height and the header's height.
- Developers must still consider narrow views first. This behavior does not change scroll on narrow screens or mobile devices.
- Consider also whether to omit footer or move footer into a different container for a more perfect full-screen fit.
- The hero is full width and never side by side with another container. It needn't be scrolled individually and what's more doing so would be quite an odd behavior.
- Including it will not harm the experience, but it isn't recommended when using height constraints.
- Users will have to scroll past hero to get to the content containers. If you do include it, ensure it isn't too tall.
- Remember, the hero is always optional.
A flyout is a container that appears on the side of the screen. It provides additional space to render elements that you may not always want to be visible. Atlas's optional flyout container can be added to any layout. This container has several important characteristics:
- It is not available on narrow and tablet-size screens. There simply isn't room for it. This means that whatever content is available in this sidebar, should be rendered elsewhere on narrow/tablet screen sizes. Typically, this would be done in a modal that renders "on top" of the other containers. Atlas does not implement this for you.
- The flyout can be shown/hidden by adding/removing the
.layout-flyout-activeclass to the.layoutelement. - The default width of the flyout on desktop can be customized with the
--layout-flyout-width-desktopCSS variable. - This means developers must handle mobile and tablet widths with a different solution, such using a modal on narrow screens.
- On expand/collapse of the flyout, it's recommended developers perform manual focus handling to ensure that focus is not lost for screenreaders.
- The same should happen on resize.
In certain scenarios, it may be advantageous to collapse the left-hand menu element on layouts that have this container. To do this, add .layout-menu-collapsed to the html element of your page. This will hide the menu element and rearrange containers on layouts that support it, such as layout-holy-grail and layout-sidecar-left.
Similarly, the right-hand aside can be collapsed by adding .layout-aside-collapsed to the html element. This narrows the aside to a minimal width on layouts that support it: layout-holy-grail, layout-sidecar-right, and layout-twin.
When a layout collapse state is active, you may want to show or hide elements inside those containers. Atlas generates display utility classes scoped to each collapse state. These classes only take effect when the corresponding state class is present on the .layout element.
The available states are layout-menu-collapsed and layout-aside-collapsed. For each state, display classes are generated for every value in the standard display atomics list: none, block, flex, grid, inline, inline-block, and inline-flex.
The class naming pattern is:
.display-{value}-{state}
For example, to hide an element when the menu is collapsed and show a different element only when the menu is collapsed:
<nav class="layout-body-menu">
<span class="display-none-layout-menu-collapsed">Full menu label</span>
<span class="display-none display-block-layout-menu-collapsed">Icon only</span>
</nav>
| Class | Effect |
|---|---|
.display-none-layout-menu-collapsed |
Hidden when menu is collapsed |
.display-block-layout-menu-collapsed |
display: block when menu is collapsed |
.display-flex-layout-aside-collapsed |
display: flex when aside is collapsed |
.display-none-layout-aside-collapsed |
Hidden when aside is collapsed |
When createLayoutState (from @microsoft/atlas-js) finishes applying stored layout state, it sets data-layout-restored="true" on <html>. These helpers apply only while that attribute is absent, letting you hide content until JS initializes or show a placeholder only while it boots.
.display-{value}-until-layout-restored
Generated values: none, block, flex, grid, inline, inline-block, inline-flex.
<!-- Hidden until restored. -->
<div class="display-none-until-layout-restored">…</div>
<!-- Shown only until restored. -->
<div class="display-block-until-layout-restored">Loading…</div>
Use these only with the inline restoration snippet; without createLayoutState, the attribute is never set and the rules keep applying.
On wider screens, the menu and aside containers automatically scale up to give navigation and supplementary content more room. This happens at two breakpoints:
| Screen width | Sidebar width |
|---|---|
Below 1500px |
275px (default) |
1500px and above |
320px |
2000px and above |
450px |
When the flyout is active, sidebar widths step down one notch to make room for the flyout column. If the sidebars are already at the default 275px, no further reduction occurs.
| Screen width | Sidebar width (flyout active) |
|---|---|
Below 1500px |
275px (unchanged) |
1500px and above |
275px (stepped down from 320px) |
2000px and above |
320px (stepped down from 450px) |
2500px and above |
450px (no reduction) |
This scaling is built in and requires no additional classes or configuration. The values can still be overridden with the --layout-menu-expanded-target-width and --layout-aside-expanded-target-width CSS variables described in Customizing layout dimensions with CSS variables.
Due to the recommendation that all content be rendering into ARIA landmark regions, the hero element should be a <section> element with an unique aria-label (or aria-labelled by).
Grid areas, the CSS feature powering the layout component, have the potential to make DOM order different from visual hierarchy (i.e. which element seems like it is first on the page). This can cause confusion for keyboard users who use tab to navigate through web pages. The recommended order of elements within layout-body is as follows:
- Header
- Hero (if present)
- Menu (if present)
- Main
- Aside (if present)
- Footer
See WCAG on Making the DOM order match the visual order for more information on this topic.
The layout component exposes several CSS custom properties that let you tune column widths and spacing without writing new grid definitions. Override these variables on the .layout element (or any ancestor) to customize dimensions site-wide, or scope them to a specific page.
| Variable | Default | Description |
|---|---|---|
--layout-menu-expanded-target-width |
275px |
The width of the menu column when expanded. |
--layout-menu-collapsed-width |
68px |
The width of the menu column when collapsed via .layout-menu-collapsed. |
--layout-aside-expanded-target-width |
275px |
The width of the aside column when expanded. |
--layout-aside-collapsed-width |
68px |
The width of the aside column when collapsed via .layout-aside-collapsed. |
For example, to make the menu wider on a particular page:
.layout {
--layout-menu-expanded-target-width: 350px;
}
Or to make the aside narrower:
.layout {
--layout-aside-expanded-target-width: 200px;
}
| Variable | Default | Description |
|---|---|---|
--layout-flyout-width-desktop |
320px |
The width of the flyout container on desktop screens. |
.layout {
--layout-flyout-width-desktop: 400px;
}
| Variable | Default | Description |
|---|---|---|
--layout-gap |
16px (narrow), 24px (desktop) |
The inline padding applied by .layout-padding and consumed by full-width elements like hero and site header. |
These variables are consumed by the grid definitions internally — you do not need to redefine any grid templates. Simply set the variable and the layout will adapt.
Because layouts generally contain the same elements and because the current layout is determined by a singular class on the .layout element, changing layout is as easy as swapping out a class.
document.documentElement.classList.remove('layout-single');
document.documentElement.classList.add('layout-holy-grail');
You'll seldom have occasion to do this in the middle of a user's visit, but it can be useful for things like opening a code editor in a wider layout or adopting a focused reading mode.
@microsoft/atlas-js exports createLayoutState, which persists layout-* classes on <html> to localStorage and restores them the next time the page loads. See the atlas-js README for the API. Skip this section if your page layout never changes after load.
Import createLayoutState from @microsoft/atlas-js and call it once per page (typically from your main module entry). It returns a LayoutStateInstance whose subscribe() method fires whenever a watched layout-* class is added to or removed from <html> — including the initial restoration on page load. A new subscriber replays immediately if the current state already matches, so the same callback that reacts to user toggles can also sync UI on first load.
const layoutState = createLayoutState({
storageKey: 'my-storage-key',
useViewTransitionOnRestore: true
});
// Keep a "Collapse menu" button's `aria-expanded` in sync with the layout
// class. Fires once on restoration with the persisted state, and again on
// every later toggle. Pass `'added'` or `'removed'` to filter; `'always'`
// fires for both.
const unsubscribe = layoutState.subscribe('layout-menu-collapsed', 'always', ({ isApplied }) => {
const button = document.querySelector<HTMLButtonElement>('[data-menu-collapse-toggle]');
button?.setAttribute('aria-expanded', String(!isApplied));
});
// Toggling the class on <html> persists the change and fires the subscriber.
document.documentElement.classList.toggle('layout-menu-collapsed');
// Drop the subscription when it's no longer needed.
unsubscribe();
The storageKey option scopes persisted state — pages with different storageKeys read and write separate localStorage buckets, so a layout toggled on one page doesn't bleed into another. Pass a function (storageKey: () => currentKey) to follow a runtime-varying key without recreating the instance. The resolved key is surfaced on LayoutCallbackEvent.storageKey so subscribers can disambiguate when one callback handles multiple instances.
When distinct pages should share persisted state — e.g. a "docs article" and "docs landing" template both restoring the same sidebar-collapsed preference — pass the same storageKey from each:
const layoutState = createLayoutState({
storageKey: 'docs-shared'
});
If you also use the inline restore script below, read from the same storageKey in the state[...] lookup.
Calling createLayoutState() from a module bundle restores classes, but not before first paint — module scripts run after HTML parse, so the browser briefly renders the original markup layout, causing a visible reflow.
Place this synchronous IIFE in <head> before the bundle to restore persisted layout-* classes before first paint. Set storageKey to match the value passed to createLayoutState(). Leave excludesKey empty unless you use the exclusions API below.
<head>
<script>
(function () {
try {
var storageKey = 'my-storage-key';
var excludesKey = '';
var state = JSON.parse(localStorage.getItem('atlas-layout-preferences') || '{}');
var view = state[storageKey] || {};
var excluded = {};
if (excludesKey) {
var ex = JSON.parse(localStorage.getItem('atlas-layout-exclusions') || '{}');
var sx = Object.prototype.hasOwnProperty.call(ex, excludesKey) ? ex[excludesKey] : null;
if (sx && typeof sx === 'object') excluded = sx;
}
var html = document.documentElement;
for (var c in view) {
if (Object.prototype.hasOwnProperty.call(excluded, c)) continue;
if (view[c]) html.classList.add(c);
else html.classList.remove(c);
}
} catch (e) {}
})();
</script>
<script type="module" src="/your-bundle.js"></script>
</head>
Atlas ships ready-made helpers — see Display helpers gated on JS restoration. Those helpers use a data attribute, not a layout-* class, so createLayoutState does not persist them.
Use excludes when a view shares storageKey with others but should ignore some layout-* classes — for example an editor view with no left nav. Excluded classes are not restored, persisted, or sent to subscribers.
const layoutState = createLayoutState({
storageKey: 'docs-shared',
excludesKey: 'editor-view',
excludes: ['layout-menu-collapsed', 'layout-aside-collapsed']
});
excludesKey names this view's list inside a second localStorage entry (atlas-layout-exclusions); excludes is the list of layout-* class names to write there on construction. Pass [] to clear this view's list; omit excludes to read the existing list under excludesKey without overwriting it. Pass a getter (excludes: () => currentList) to follow a runtime-varying exclusion list — it is re-read on every persist and every resume(), so route changes can supply different lists without recreating the instance.
To honor exclusions before first paint, set excludesKey in the inline restore script above to the same value.
If your site swaps page content via client-side navigation (e.g. fetching new HTML and replacing the <html> class list without a hard reload), pair suspend() with resume() around each navigation so a single long-lived instance can serve every route. Subscribers registered once at boot stay live — no re-registration needed.
const layoutState = createLayoutState({
storageKey: () => currentRoute.layoutKey,
excludesKey: () => currentRoute.excludesKey,
excludes: () => currentRoute.excludes,
useViewTransitionOnRestore: false
});
// Subscribers added once at boot survive every navigation.
layoutState.subscribe('layout-menu-collapsed', 'always', ({ isApplied }) => {
const button = document.querySelector<HTMLButtonElement>('[data-menu-collapse-toggle]');
button?.setAttribute('aria-expanded', String(!isApplied));
});
router.on('beforeNavigate', () => layoutState.suspend());
router.on('afterNavigate', () => layoutState.resume());
suspend() disconnects the MutationObserver synchronously and removes data-layout-restored so until-restored helpers re-engage during the navigation. resume() re-reads the storageKey / excludesKey / excludes getters, re-restores persisted state for the new route's bucket, replays matching subscribers against the freshly-restored state, re-attaches the observer, and re-sets data-layout-restored.
Two rules to follow:
- Call
suspend()BEFORE any DOM mutation that touches<html>'s class list. Reversing the order writes the destination page's server-rendered classes into the previous route's storage bucket. - Keep subscriber callbacks idempotent.
resume()re-fires every matching subscriber on every navigation, so look up the DOM on each run (don't reuse element references across routes) and treat repeated calls with the same state as a no-op.
dispose() fully tears down the instance — useful in tests or when unmounting a widget that owns the layout state. Afterward, subscribe() / suspend() / resume() throw, while getViewState() / getState() still read from localStorage. Most SPAs won't need it.
Inline <script> requires 'unsafe-inline' or a 'sha256-...' hash in script-src. Atlas places the snippet before the CSP <meta> tag so the policy applies only to later elements; alternatively, add the script body's SHA-256 hash to script-src.