Page
<cs-page>
Pages scaffold an entire application layout with header, navigation, sidebar, main content, aside, and footer regions. Use them to structure full pages with minimal markup and responsive behavior built in.
The page component is designed to power full webpages. It is flexible enough to handle most modern designs and includes a simple mechanism for handling desktop and mobile navigation.
Layout Anatomy
This image depicts a page’s anatomy, including the default positions of each section. The labels represent the named slots you can use to populate them.
Most slots are optional. Slots that have no content will not be shown, allowing you to opt-in to just the sections you actually need.
Using cs-page
If you’re not familiar with how slots work in HTML, you might want to learn more about slots before using this component.
A number of sections are available as part of the page component, most of which are optional. Content is populated by slotting elements into various locations.
This component does not implement any content sectioning or “semantic elements” internally (such as <main>, <header>, <footer>, etc.). Instead, we recommend that you slot in content sectioning elements wherever you feel they’re appropriate.
When using <cs-page>, make sure to zero out all paddings and margins on <html> and <body>, otherwise you may see unexpected gaps. We highly recommend adding the following styles when using <cs-page>:
html, body { min-height: 100%; padding: 0; margin: 0; }
If you use native styles, this is already taken care of.
DOM Morphing
<cs-page> writes the heights it measures to its own inline style, as Header Heights describes. DOM-diffing libraries such as Turbo 8 morph refreshes, idiomorph with htmx, and Alpine’s morph plugin reset that inline style to the server’s HTML, which doesn’t have those heights.
The page puts its measured heights back when that happens, and leaves the rest of the server’s inline styles alone. You don’t need a turbo:before-morph-attribute listener to keep Turbo off the page’s style attribute.
Examples
Open demos in a new tab to examine their behavior in different window sizes. The previews below use simulated zooming which, depending on your browser, may not be accurate.
Documentation
A sample documentation page using all available slots. The navigation menu collapses into a drawer at a custom mobile-breakpoint of 920px. It can be opened using a button with [data-toggle-nav] that appears in the subheader slot. The aside slot is also hidden below 920px.
Media
A sample media app page using header, navigation-header, main-header, and main-footer along with the default slot. The navigation menu collapses into a drawer at the default mobile-breakpoint and can be opened using a button with [data-toggle-nav] that appears in the header slot.
Customization
Sticky Sections
The following sections of a page are “sticky” by default, meaning they remain in position as the user scrolls.
bannerheadersubheadermenu(navigationitself is not sticky, but its parentmenuis)aside
This is often desirable, but you can change this behavior using the disable-sticky attribute. Use a space-delimited list of names to tell the page which sections should not be sticky.
<cs-page disable-sticky="header aside"> ... </cs-page>
Header Heights
The page measures its banner, header and subheader and keeps each height in a custom property on itself: --cs-page-banner-height, --cs-page-header-height and --cs-page-subheader-height. It uses them to place its sticky sections below one another. Your own styles can use them too, because everything inside the page inherits them. This makes the page’s <main> a panel that fills the screen below the header and scrolls on its own:
cs-page > main { box-sizing: border-box; block-size: calc(100dvh - var(--cs-page-header-height)); overflow: auto; }
Each height is 0px until the page has measured it, so a layout sized from one shifts when the page loads. The panel above starts as tall as the whole screen, then shrinks by the header’s height.
If you know a height ahead of time, set it on <cs-page> in your stylesheet, and the layout is right from the start. The page still measures, and its value replaces yours. So a guess that’s a little off costs only a small shift, and the value stays right if the header’s size changes later.
cs-page { --cs-page-header-height: 64px; }
Set it on cs-page itself, not on :root or another ancestor. The page gives each height a 0px default on its own element, and that default wins over a value inherited from above.
Skip to Content
The layout provides a “skip to content” link that’s visually hidden until the user tabs into it. You don’t have to do anything to configure this, unless you want to change the text displayed in the link. In that case, you can slot in your own text using the skip-to-content slot.
This example localizes the “skip to content” link for German users.
<cs-page> ... <span slot="skip-to-content">Zum Inhalt springen</span> ... </cs-page>
Responsiveness
A page isn’t very opinionated when it comes to responsive behaviors, but there are tools in place to help make responsiveness easy.
Default Slot Styles
Most slots lay out the element you put in them, so its children are arranged before you write any styles. Each of these wrappers is a flex container with a --cs-space-m gap between its children.
| Slot | Direction | Alignment | Wraps |
|---|---|---|---|
header, subheader, main-header | row | align-items: center, justify-content: space-between | yes |
main-footer, footer | row | align-items: start, justify-content: space-between | yes |
banner | row | align-items: center, justify-content: center | no |
navigation-header, navigation, navigation-footer, menu, aside | column | not set, so children stretch across | no |
The desktop and mobile navigation slots are columns too, like the navigation slots they stand in for.
The default slot is the exception. A <main> or <section> there gets padding, but its display is left alone.
Changing a Wrapper’s Layout
To arrange a wrapper’s children another way, put layout utilities on the wrapper instead of writing CSS for it. The defaults above are set from inside the page’s shadow DOM, and a style on the slotted element itself always wins over them, whatever its specificity. So a utility class replaces a default outright.
A navigation in two parts, such as an icon rail beside a list of links, is the usual case. The navigation wrapper is a column, so the parts stack until you make it a row.
<cs-page> ... <nav slot="navigation" class="cs-split cs-align-items-stretch cs-justify-content-start"> <div class="cs-stack cs-gap-xs"> <cs-button appearance="plain" href="#home" data-drawer="close"> <cs-icon name="home" label="Home"></cs-icon> </cs-button> <cs-button appearance="plain" href="#search" data-drawer="close"> <cs-icon name="search" label="Search"></cs-icon> </cs-button> <cs-button appearance="plain" href="#settings" data-drawer="close"> <cs-icon name="settings" label="Settings"></cs-icon> </cs-button> </div> <div class="cs-stack cs-gap-2xs"> <a href="#overview" data-drawer="close">Overview</a> <a href="#reports" data-drawer="close">Reports</a> <a href="#members" data-drawer="close">Members</a> </div> </nav> ... </cs-page>
Each class does one job:
cs-splitmakes the wrapper a row. It setsflex-direction: row, andcs-clustersets no direction, so a cluster leaves the column in place.cs-align-items-stretchruns both parts the full height of the navigation. A split centers its children by default, which floats a short rail to the middle.cs-justify-content-startkeeps the parts together. A split pushes its children to opposite ends, which opens a gap between them in the mobile drawer, or whenever--cs-page-menu-widthis wider than the parts.
The split also puts a --cs-space-m gap between the parts. Add a gap utility to change it, such as cs-gap-0 when the parts should meet edge to edge.
On mobile the page moves this same wrapper into its drawer, classes included, so the parts stay side by side there as long as they fit. A split wraps, so when the drawer or a fixed --cs-page-menu-width is narrower than both parts, the second part drops below the first.
Responsive Navigation
When you use the navigation slot, your slotted content automatically collapses into a drawer on smaller screens. The breakpoint at which this occurs is 768px by default, but you can change it using the mobile-breakpoint attribute, which takes either a number or a CSS length.
<cs-page mobile-breakpoint="600"> ... </cs-page>
By default, a “hamburger” button appears at the start of the header to toggle the navigation menu on smaller screens. You can customize what this looks like by slotting your own button into the navigation-toggle slot, or place the data-toggle-nav attribute on any button on your page. This does not have to be a Cornerstone element.
The default button will not be shown when using either of these methods — if you want to use multiple navigation toggles on your page, add the data-toggle-nav attribute to multiple elements.
<cs-page mobile-breakpoint="600"> ... <cs-button data-toggle-nav>Menu</cs-button> ... </cs-page>
Alternatively, you can apply nav-state="open" and nav-state="closed" to the layout component to show and hide the navigation, respectively.
<cs-page nav-state="open"> ... </cs-page>
<cs-page> is given the attribute view="mobile" or view="desktop" when the viewport narrower or wider than the mobile-breakpoint value, respectively. You can leverage these attributes to change styles depending on the size of the viewport. This is especially useful to hide your data-toggle-nav button when the viewport is wider.
cs-page[view='desktop'] [data-toggle-nav] { display: none; }
If you use native styles, this is handled for you, and the data-toggle-nav button is already hidden on wider screens.
Different Navigation on Mobile and Desktop
The navigation appears in two places: the sidebar on desktop and the drawer on mobile. The navigation-header, navigation and navigation-footer slots fill both. To show something different in one place, fill that place’s own slot. It replaces the shared slot there, and the other place keeps showing the shared one.
| Shared slot | Sidebar only (desktop) | Drawer only (mobile) |
|---|---|---|
navigation-header | desktop-navigation-header | mobile-navigation-header |
navigation | desktop-navigation | mobile-navigation |
navigation-footer | desktop-navigation-footer | mobile-navigation-footer |
A drawer often needs a title that the sidebar doesn’t, because on desktop the page’s header already names the app. This gives the drawer a header and leaves the sidebar without one.
<cs-page> <header slot="header">...</header> <strong slot="mobile-navigation-header">Menu</strong> <nav slot="navigation">...</nav> ... </cs-page>
Don’t use cs-mobile-only on a navigation-header to keep it out of the sidebar. It hides the header on desktop, but it breaks the sidebar’s layout. The sidebar is a grid of three rows: the header, then the navigation in a row that grows to fill the column, then the footer. A hidden element drops out of that grid, so the navigation moves up into the header’s row and stops filling the sidebar, and the footer moves up off the bottom. Use mobile-navigation-header instead, which leaves the sidebar’s grid alone.
Two gaps to know about for now:
- Content in a
desktop-navigation-*slot still shows on mobile. It stays at the side of the page, beside the main content, where the sidebar would be. Addcs-desktop-onlyto it as well. The sidebar isn’t meant to show on mobile at all, so hiding its content there doesn’t disturb anything. - The menu button only appears when a shared slot has content. The page decides whether to show its button from
navigation,navigation-headerandnavigation-footeralone. If you fill only themobile-navigation-*slots, add your own button withdata-toggle-nav.
Custom Widths
You specify widths for some slots on your page with CSS custom properties for --cs-page-menu-width, --cs-page-main-width, and --cs-page-aside-width.
If you specify --cs-page-menu-width to apply a specific width to your navigation slot, space will still be reserved on the page even below the mobile-breakpoint. To collapse this space on smaller screens, add the following code to your styles.
cs-page[view='mobile'] { --cs-page-menu-width: auto; }
You can use a similar approach for --cs-page-aside-width to hide the aside slot on smaller screens. Be sure to also specify display: none for the slot:
cs-page[view='mobile'] { --cs-page-aside-width: auto; [slot='aside'] { display: none; } }
Spacing
A page specifies default padding within each slot and a gap between the slot’s direct children. You can drop elements into any slot, and reasonable spacing is already applied for you.
You can override the default spacing for each slot with your own CSS. In this example, we’re setting custom gap and padding for the footer slot.
[slot='footer'] { gap: var(--cs-space-xl); padding: var(--cs-space-xl); }
Rules Between Regions
The page doesn’t offer a rule (divider) between its regions yet, such as a line under the header. There’s no attribute that adds one, and no slot sits between two regions where a <cs-divider> could go.
Utility Classes
Native styles define a few useful defaults for <cs-page>, as well as two utility classes you can use for common responsive design tasks:
.cs-mobile-onlyhides an element on the desktop view.cs-desktop-onlyhides an element on the mobile view
Before you use them on navigation content, read Different Navigation on Mobile and Desktop.
API
Importing
If you're using the autoloader or a hosted project, components load on demand — no manual import needed. To cherry-pick this component, use one of the following snippets.
import '@cruglobal/cornerstone-components/components/page/page.js';
import './cornerstone/components/page/page.js';
import CsPage from '@cruglobal/cornerstone-components/react/page/index.js';
Slots
Learn more about using slots.
| Name | Description |
|---|---|
| (default) | The page's main content. |
aside | Content to be shown on the right side of the page. Typically contains a table of contents, ads, etc. This section "sticks" to the top as the page scrolls. |
banner | The banner that gets display above the header. The banner will not be shown if no content is provided. |
desktop-navigation | The sidebar's content on desktop, in place of navigation. On mobile, <cs-drawer> still shows navigation. |
desktop-navigation-footer | The sidebar's footer on desktop, in place of navigation-footer. On mobile, <cs-drawer> still shows navigation-footer. |
desktop-navigation-header | The sidebar's header on desktop, in place of navigation-header. On mobile, <cs-drawer> still shows navigation-header. |
footer | The content to display in the footer. This is always displayed underneath the viewport so will always make the page "scrollable". |
header | The header to display at the top of the page. If a banner is present, the header will appear below the banner. The header will not be shown if there is no content. |
main-footer | Footer to display inline below the main content. |
main-header | Header to display inline above the main content. |
menu | The left side of the page. If you slot an element in here, you will override the default navigation slot and will be handling navigation on your own. This also will not disable the fallback behavior of the navigation button. This section "sticks" to the top as the page scrolls. |
mobile-navigation | The content of <cs-drawer> on mobile, in place of navigation. On desktop, the sidebar still shows navigation. |
mobile-navigation-footer | The footer for <cs-drawer> on mobile, in place of navigation-footer. On desktop, the sidebar still shows navigation-footer. |
mobile-navigation-header | The header for <cs-drawer> on mobile, in place of navigation-header. On desktop, the sidebar still shows navigation-header. Use it for a header only the drawer should have. |
navigation | The main content to display in the navigation area. This is displayed on the left side of the page, if menu is not used. This section "sticks" to the top as the page scrolls. |
navigation-footer | The footer for a navigation area. On mobile this will be the footer for <cs-drawer>. |
navigation-header | The header for a navigation area. On mobile this will be the header for <cs-drawer>. |
navigation-toggle | Use this slot to slot in your own button + icon for toggling the navigation drawer. By default it is a <cs-button> + a 3 bars <cs-icon> |
navigation-toggle-icon | Use this to slot in your own icon for toggling the navigation drawer. By default it is 3 bars <cs-icon>. |
skip-to-content | The "skip to content" slot. You can override this If you would like to override the Skip to content button and add additional "Skip to X", they can be inserted here. |
skip-to-content-target | Where the "skip to content" link lands. If the document has no element with id="main-content", the page adds an empty one to this slot, at the start of the main content. You don't need to fill it. To choose where the link lands, put id="main-content" on your own element in the main content. |
subheader | A subheader to display below the header. This is a good place to put things like breadcrumbs. |
Attributes & Properties
Learn more about attributes and properties.
| Property | Attribute | Description | Type | Default | Reflects |
|---|---|---|---|---|---|
disableNavigationToggle | disable-navigation-toggle | Determines whether or not to hide the default hamburger button. This will automatically flip to "true" if you add an element with data-toggle-nav anywhere in the element light DOM. Generally this will be set for you and you don't need to do anything, unless you're using SSR, in which case you should set this manually for initial page loads. | boolean | false | Yes |
mobileBreakpoint | mobile-breakpoint | At what page width to hide the "navigation" slot and collapse into a hamburger button. Accepts both numbers (interpreted as px) and CSS lengths (e.g. 50em), which are resolved based on the root element. | string | '768px' | — |
navigationPlacement | navigation-placement | Where to place the navigation when in the mobile viewport. | 'start' | 'end' | 'start' | Yes |
navOpen | nav-open | Whether or not the navigation drawer is open. Note, the navigation drawer is only "open" on mobile views. | boolean | false | Yes |
view | view | The view is a reflection of the "mobileBreakpoint", when the page is larger than the mobile-breakpoint (768px by default), it is considered to be a "desktop" view. The view is merely a way to distinguish when to show/hide the navigation. You can use additional media queries to make other adjustments to content as necessary. The default is "desktop" because the "mobile navigation drawer" isn't accessible via SSR due to drawer requiring JS. | 'mobile' | 'desktop' | 'desktop' | Yes |
Methods
Learn more about methods.
| Name | Description | Arguments |
|---|---|---|
hideNavigation() | Hides the mobile navigation drawer | — |
showNavigation() | Shows the mobile navigation drawer | — |
toggleNavigation() | Toggles the mobile navigation drawer | — |
visiblePixelsInViewport() | https://stackoverflow.com/a/26831113 This prevents awkward gaps when scrolling the page and the aside / menu don't "fill" the gaps. | element: HTMLElement | null |
CSS Custom Properties
Learn more about CSS custom properties.
| Name | Description | Default |
|---|---|---|
--cs-page-aside-width | The width of the page's "aside" section. | auto |
--cs-page-banner-height | The height of the banner. This gets calculated when the page initializes. If the height is known, you can set it here to prevent shifting when the page loads. | 0px |
--cs-page-header-height | The height of the header. This gets calculated when the page initializes. If the height is known, you can set it here to prevent shifting when the page loads. | 0px |
--cs-page-main-width | The width of the page's "main" section. | 1fr |
--cs-page-menu-width | The width of the page's "menu" section. | auto |
--cs-page-subheader-height | The height of the subheader. This gets calculated when the page initializes. If the height is known, you can set it here to prevent shifting when the page loads. | 0px |
CSS Parts
Learn more about CSS parts.
| Name | Description | CSS selector |
|---|---|---|
aside | The right hand side of the page. Used for things like table of contents, ads, etc. | ::part(aside) |
banner | The banner to show above header. | ::part(banner) |
body | The wrapper around menu, main, and aside. | ::part(body) |
drawer | The <cs-drawer> that contains the navigation on mobile viewports. | ::part(drawer) |
drawer__body | The navigation drawer's body part. | ::part(drawer__body) |
drawer__close-button | The navigation drawer's close button. | ::part(drawer__close-button) |
drawer__close-button__button | The navigation drawer's close button's button part. | ::part(drawer__close-button__button) |
drawer__dialog | The navigation drawer's dialog part. | ::part(drawer__dialog) |
drawer__footer | The navigation drawer's footer part. | ::part(drawer__footer) |
drawer__header | The navigation drawer's header part. | ::part(drawer__header) |
drawer__header-actions | The navigation drawer's header-actions part. | ::part(drawer__header-actions) |
drawer__title | The navigation drawer's title part. | ::part(drawer__title) |
footer | The footer of the page. This is always below the initial viewport size. | ::part(footer) |
header | The header, usually for top level navigation / branding. | ::part(header) |
main | The wrapper around the main header, content, and footer. | ::part(main) |
main-content | The main content. | ::part(main-content) |
main-footer | The footer below main content. | ::part(main-footer) |
main-header | The header above main content. | ::part(main-header) |
menu | The left hand side of the page. Generally intended for navigation. | ::part(menu) |
navigation | The <nav> that wraps the navigation slots on desktop viewports. | ::part(navigation) |
navigation-desktop | The <nav> for navigation on desktop viewports. | ::part(navigation-desktop) |
navigation-footer | The footer for a navigation area. On mobile this will be the footer for <cs-drawer>. | ::part(navigation-footer) |
navigation-header | The header for a navigation area. On mobile this will be the header for <cs-drawer>. | ::part(navigation-header) |
navigation-toggle | The default <cs-button> that will toggle the <cs-drawer> for mobile viewports. | ::part(navigation-toggle) |
navigation-toggle-icon | The default <cs-icon> displayed inside of the navigation-toggle button. | ::part(navigation-toggle-icon) |
page | The component's outer wrapper. | ::part(page) |
skip-to-content | The "skip to content" link that lets keyboard users bypass navigation. | ::part(skip-to-content) |
subheader | Shown below the header, usually intended for things like breadcrumbs and other page level navigation. | ::part(subheader) |

