App Shell
The frame of an application: header, navigation, main, an optional details panel, banner and footer, laid out from its own width.
c2-app-shell
The frame of an application: a banner, a header, a navigation, the main content, an optional aside (a details panel) and a footer, each in its own slot and landmark, plus a built-in skip link to main.
The layout follows the shell's own width, not the window's, so the same markup behaves inside an iframe, a split
view or a docs example frame. Below 600px the navigation is a modal drawer (over); from 600px tablet-mode and from
960px desktop-mode choose between side (pushes main), rail (collapses to its icons; expanded it pushes) and
over. The aside is a column from 960px and a sheet from the end edge below that. Container queries draw the
layout, so the server-rendered first paint is already right.
Put a c2-app-shell-toggle (or any element with app-shell-toggle="navigation" / "aside") anywhere inside the
shell, including inside the slotted c2-header, to open and close the navigation or the aside with no script. With
the drawer open, focus moves into it and stays there, Escape and the backdrop close it, and focus returns to the
toggle. In a collapsed rail, hide the item labels with
c2-app-shell:state(rail):not(:state(navigation-opened)) .label { display: none }.
With scrolling="page" (default) the page scrolls and the header region sticks to the top; with scrolling="main" the
shell takes --c2-app-shell--height and only main scrolls. Either way the shell sets
--c2-app-shell--sticky-offset to the height that sticky content in main must clear, which c2-page-header reads.
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
layout | AppShellLayout | header-first | header-first: the header spans the width. navigation-first: the navigation runs the full height and the header sits above main. |
navigation-opened | boolean | false | Whether the navigation is open: the side panel shown, the rail expanded, the drawer open. The shell changes it
itself when its width changes, so it does not reflect; style the live state with :state(navigation-opened).
Below 600px the initial value is ignored and the drawer starts closed. |
desktop-mode | AppShellMode | side | Presentation of the navigation at 960px of width and up. |
tablet-mode | AppShellMode | rail | Presentation of the navigation from 600px to 959px. Below 600px it is always over. |
aside-opened | boolean | false | Whether the aside is shown: a column from 960px, a sheet below. Does not reflect; style it with :state(aside-opened). |
scrolling | AppShellScrolling | page | page: the page scrolls and the header sticks. main: the shell takes --c2-app-shell--height and only main scrolls. |
close-on-navigate | boolean | false | In over mode, a click on a link inside the navigation closes the drawer and moves focus to main. |
navigation-label | string | — | Accessible name of the navigation landmark. Empty: "Main" in the page language. |
aside-label | string | — | Accessible name of the aside landmark, for slotted content that does not name itself. |
storage-key | string | — | When set, the user's choice to expand or collapse a side or rail navigation is kept in localStorage under this key. |
Slots
| Name | Description |
|---|---|
banner | A strip above everything, such as a c2-banner for test mode or impersonation. |
header | The application header, usually a c2-header. Kept outside <main> in both layouts, so it stays the banner landmark. |
navigation-header | Above the navigation: a logo or a workspace switcher. |
navigation | The navigation itself, such as a c2-list of links with their icons in the prefix slot. |
navigation-footer | Pinned to the bottom of the navigation: settings, help, the user. |
(default) | The main content, rendered inside <main>. |
aside | A details panel or inspector beside main. |
footer | A status bar or page footer below main. |
skip-link | Replaces the built-in "Skip to main content" link. |
Events
| Name | Type | Description |
|---|---|---|
mode-change | CustomEvent<ModeChangeEventDetail> | The shell's width crossed 600px or 960px, or a mode attribute changed the presentation in effect. Does not bubble. |
navigation-opened-change | CustomEvent<NavigationOpenedChangeEventDetail> | The user (a toggle, the backdrop, Escape, a link with close-on-navigate) or a width change opened or closed the navigation. Not fired when navigationOpened is set from code. Does not bubble. |
aside-opened-change | CustomEvent<AsideOpenedChangeEventDetail> | The user or a width change opened or closed the aside. Not fired when asideOpened is set from code. Does not bubble. |
CSS parts
Shadow DOM styling hooks used with ::part(name). Prefer CSS custom properties when they cover the change.
| Name | Description |
|---|---|
skip-link | The built-in skip link, the fallback content of the skip-link slot. |
banner | Container wrapping the banner slot. |
header | Container wrapping the header slot; it sticks to the top while the page scrolls. |
navigation | The navigation panel (the drawer in over mode), containing the navigation-header, navigation and navigation-footer slots. |
main | The <main> landmark region wrapping the default slot. |
aside | The aside panel (a sheet below 960px) containing the aside slot. |
footer | Container wrapping the footer slot. |
backdrop | The backdrop behind an open drawer or aside sheet. |
CSS custom properties
Grouped by semantic target and state from --c2-<component>__<target>__<state>--<property>. Click a group to collapse it; click a name to copy it.
| Name | Type | Default | Description |
|---|---|---|---|
--c2-app-shell--height | pixel | 100dvh | Height of the shell with scrolling="main". |
--c2-app-shell--transition-duration | time | 250ms | |
--c2-app-shell--sticky-offset | pixel | — | Output, not an input: the height sticky content in main must clear (the header's height with scrolling="page", 0px with scrolling="main"). |
--c2-app-shell__header--z-index | pixel | 10 | |
--c2-app-shell__navigation--width | pixel | 240px | Width of the side navigation and of an expanded rail. |
--c2-app-shell__navigation--background-color | color | #fafafa | |
--c2-app-shell__navigation--color | color | — | |
--c2-app-shell__navigation--padding | padding | 8px | |
--c2-app-shell__navigation__rail--width | pixel | 64px | Width of a collapsed rail. |
--c2-app-shell__navigation__over--width | pixel | min(280px, 85%) | Width of the drawer. |
--c2-app-shell__aside--width | pixel | 320px | |
--c2-app-shell__aside--background-color | color | #ffffff | |
--c2-app-shell__aside--padding | padding | 0px | |
--c2-app-shell__main--background-color | color | transparent | |
--c2-app-shell__main--padding | padding | 0px | |
--c2-app-shell__divider--width | pixel | 1px | Line between the navigation, main and the aside. |
--c2-app-shell__divider--color | color | #e4e4e7 | |
--c2-app-shell__scrollbar--color | color | rgba(0, 0, 0, 0.2) | |
--c2-app-shell__over--position | position | fixed | absolute keeps the drawer and the aside sheet inside the shell (and does not lock page scrolling). |
--c2-app-shell__over--box-shadow | box-shadow | 0 12px 40px rgba(0, 0, 0, 0.18) | |
--c2-app-shell__backdrop--background-color | color | rgba(0, 0, 0, 0.32) | |
--c2-app-shell__backdrop--z-index | number | 1000 | |
--c2-app-shell__skip-link--background-color | color | rgb(2, 101, 220) | |
--c2-app-shell__skip-link--color | color | #ffffff | |
--c2-app-shell__skip-link--padding | padding | 8px 16px | |
--c2-app-shell__skip-link--border-radius | border-radius | 6px | |
--c2-app-shell__skip-link--font-size | font-size | 14px | |
--c2-app-shell__skip-link--font-weight | font-weight | 600 | |
--c2-app-shell__skip-link__focus--outline | outline | 2px solid rgba(2, 101, 220, 0.4) | |
Text and languages
The text the component shows, in English. It shows another language when the app adds one with addTranslations, under app-shell, and the nearest lang asks for it; any message not translated stays in English. {0}, {1}… stand for the values a message carries. Languages guide.
| Key | English | Values |
|---|---|---|
skipToContent | Skip to main content | — |
navigation | Main | — |
openNavigation | Open navigation | — |
closeNavigation | Close navigation | — |
openDetails | Show details | — |
closeDetails | Close details | — |
c2-app-shell-toggle
The button that opens and closes the navigation (or the aside) of the enclosing c2-app-shell. It finds its shell
through a context, so it works anywhere inside it, including inside a slotted c2-header, with no id and no script.
Its aria-expanded and its name follow the shell ("Open navigation" / "Close navigation"), and its default icon is a
menu glyph while the navigation is a drawer and a sidebar glyph beside main. Outside a shell it renders nothing.
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
for | AppShellToggleTarget | navigation | What the button opens: the shell's navigation or its aside. |
label | string | — | Accessible name. Empty: "Open navigation" / "Close navigation" (or "Show details" / "Close details") in the page language. |
Slots
| Name | Description |
|---|---|
icon | Replaces the default icon. |
CSS parts
Shadow DOM styling hooks used with ::part(name). Prefer CSS custom properties when they cover the change.
| Name | Description |
|---|---|
button | The native button. |
CSS custom properties
Grouped by semantic target and state from --c2-<component>__<target>__<state>--<property>. Click a group to collapse it; click a name to copy it.
| Name | Type | Default | Description |
|---|---|---|---|
--c2-app-shell-toggle--size | pixel | 40px | |
--c2-app-shell-toggle--border-radius | border-radius | 8px | |
--c2-app-shell-toggle--color | color | #18181b | |
--c2-app-shell-toggle--background-color | color | transparent | |
--c2-app-shell-toggle__icon--size | pixel | 20px | |
--c2-app-shell-toggle__hover--background-color | color | rgba(24, 24, 27, 0.08) | |
--c2-app-shell-toggle__focus--outline | outline | 2px solid rgba(2, 101, 220, 0.4) | |