<cs-page>
StableLayoutSince 0.1

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.

Slots
banner header subheader navigation-header navigation navigation-footer main-header main-footer aside footer

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.

Open demo in a new window

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.

Open demo in a new window

Customization

Sticky Sections

The following sections of a page are “sticky” by default, meaning they remain in position as the user scrolls.

  • banner
  • header
  • subheader
  • menu (navigation itself is not sticky, but its parent menu is)
  • 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.

SlotDirectionAlignmentWraps
header, subheader, main-headerrowalign-items: center, justify-content: space-betweenyes
main-footer, footerrowalign-items: start, justify-content: space-betweenyes
bannerrowalign-items: center, justify-content: centerno
navigation-header, navigation, navigation-footer, menu, asidecolumnnot set, so children stretch acrossno

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-split makes the wrapper a row. It sets flex-direction: row, and cs-cluster sets no direction, so a cluster leaves the column in place.
  • cs-align-items-stretch runs 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-start keeps 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-width is 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 slotSidebar only (desktop)Drawer only (mobile)
navigation-headerdesktop-navigation-headermobile-navigation-header
navigationdesktop-navigationmobile-navigation
navigation-footerdesktop-navigation-footermobile-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. Add cs-desktop-only to 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-header and navigation-footer alone. If you fill only the mobile-navigation-* slots, add your own button with data-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-only hides an element on the desktop view
  • .cs-desktop-only hides 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.

npm Self-Hosted React
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.

NameDescription
(default)The page's main content.
asideContent 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.
bannerThe banner that gets display above the header. The banner will not be shown if no content is provided.
desktop-navigationThe sidebar's content on desktop, in place of navigation. On mobile, <cs-drawer> still shows navigation.
desktop-navigation-footerThe sidebar's footer on desktop, in place of navigation-footer. On mobile, <cs-drawer> still shows navigation-footer.
desktop-navigation-headerThe sidebar's header on desktop, in place of navigation-header. On mobile, <cs-drawer> still shows navigation-header.
footerThe content to display in the footer. This is always displayed underneath the viewport so will always make the page "scrollable".
headerThe 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-footerFooter to display inline below the main content.
main-headerHeader to display inline above the main content.
menuThe 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-navigationThe content of <cs-drawer> on mobile, in place of navigation. On desktop, the sidebar still shows navigation.
mobile-navigation-footerThe footer for <cs-drawer> on mobile, in place of navigation-footer. On desktop, the sidebar still shows navigation-footer.
mobile-navigation-headerThe 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.
navigationThe 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-footerThe footer for a navigation area. On mobile this will be the footer for <cs-drawer>.
navigation-headerThe header for a navigation area. On mobile this will be the header for <cs-drawer>.
navigation-toggleUse 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-iconUse this to slot in your own icon for toggling the navigation drawer. By default it is 3 bars <cs-icon>.
skip-to-contentThe "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-targetWhere 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.
subheaderA subheader to display below the header. This is a good place to put things like breadcrumbs.

Attributes & Properties

Learn more about attributes and properties.

PropertyAttributeDescriptionTypeDefaultReflects
disableNavigationToggledisable-navigation-toggleDetermines 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.booleanfalseYes
mobileBreakpointmobile-breakpointAt 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'—
navigationPlacementnavigation-placementWhere to place the navigation when in the mobile viewport.'start' | 'end''start'Yes
navOpennav-openWhether or not the navigation drawer is open. Note, the navigation drawer is only "open" on mobile views.booleanfalseYes
viewviewThe 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.

NameDescriptionArguments
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.

NameDescriptionDefault
--cs-page-aside-widthThe width of the page's "aside" section.auto
--cs-page-banner-heightThe 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-heightThe 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-widthThe width of the page's "main" section.1fr
--cs-page-menu-widthThe width of the page's "menu" section.auto
--cs-page-subheader-heightThe 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.

NameDescriptionCSS selector
asideThe right hand side of the page. Used for things like table of contents, ads, etc.::part(aside)
bannerThe banner to show above header.::part(banner)
bodyThe wrapper around menu, main, and aside.::part(body)
drawerThe <cs-drawer> that contains the navigation on mobile viewports.::part(drawer)
drawer__bodyThe navigation drawer's body part.::part(drawer__body)
drawer__close-buttonThe navigation drawer's close button.::part(drawer__close-button)
drawer__close-button__buttonThe navigation drawer's close button's button part.::part(drawer__close-button__button)
drawer__dialogThe navigation drawer's dialog part.::part(drawer__dialog)
drawer__footerThe navigation drawer's footer part.::part(drawer__footer)
drawer__headerThe navigation drawer's header part.::part(drawer__header)
drawer__header-actionsThe navigation drawer's header-actions part.::part(drawer__header-actions)
drawer__titleThe navigation drawer's title part.::part(drawer__title)
footerThe footer of the page. This is always below the initial viewport size.::part(footer)
headerThe header, usually for top level navigation / branding.::part(header)
mainThe wrapper around the main header, content, and footer.::part(main)
main-contentThe main content.::part(main-content)
main-footerThe footer below main content.::part(main-footer)
main-headerThe header above main content.::part(main-header)
menuThe left hand side of the page. Generally intended for navigation.::part(menu)
navigationThe <nav> that wraps the navigation slots on desktop viewports.::part(navigation)
navigation-desktopThe <nav> for navigation on desktop viewports.::part(navigation-desktop)
navigation-footerThe footer for a navigation area. On mobile this will be the footer for <cs-drawer>.::part(navigation-footer)
navigation-headerThe header for a navigation area. On mobile this will be the header for <cs-drawer>.::part(navigation-header)
navigation-toggleThe default <cs-button> that will toggle the <cs-drawer> for mobile viewports.::part(navigation-toggle)
navigation-toggle-iconThe default <cs-icon> displayed inside of the navigation-toggle button.::part(navigation-toggle-icon)
pageThe component's outer wrapper.::part(page)
skip-to-contentThe "skip to content" link that lets keyboard users bypass navigation.::part(skip-to-content)
subheaderShown below the header, usually intended for things like breadcrumbs and other page level navigation.::part(subheader)