Layout
<oblyx-layout> is a two-dimensional application shell: an optional header and footer spanning the full width, and a middle row that can hold a scrollable content region plus a start and/or end side panel. Composition is CSS Grid with named areas, so <oblyx-layout-header>, <oblyx-layout-content>,<oblyx-layout-footer> and <oblyx-layout-panel> can appear in any order in the markup and still land in the right place: each one assigns its own grid area by tag, not by DOM position.
Application shell
A header, a navigation panel, a scrolling content region and a footer.
Overview
Projects
Settings
Row 1 of a long scrolling report.
Row 2 of a long scrolling report.
Row 3 of a long scrolling report.
Row 4 of a long scrolling report.
Row 5 of a long scrolling report.
Row 6 of a long scrolling report.
Row 7 of a long scrolling report.
Row 8 of a long scrolling report.
<div class="docs-shell-frame">
<oblyx-layout default-has-dividers>
<oblyx-layout-header height="56px" landmark-role="banner">
<strong>Acme dashboard</strong>
</oblyx-layout-header>
<oblyx-layout-panel area="start" width="160px" has-divider landmark-role="navigation" label="Primary">
<p>Overview</p>
<p>Projects</p>
<p>Settings</p>
</oblyx-layout-panel>
<oblyx-layout-content landmark-role="main" label="Report">
<p>Row 1 of a long scrolling report.</p>
<p>Row 2 of a long scrolling report.</p>
<p>Row 3 of a long scrolling report.</p>
<p>Row 4 of a long scrolling report.</p>
<p>Row 5 of a long scrolling report.</p>
<p>Row 6 of a long scrolling report.</p>
<p>Row 7 of a long scrolling report.</p>
<p>Row 8 of a long scrolling report.</p>
</oblyx-layout-content>
<oblyx-layout-footer height="40px">
<span>6 active projects</span>
</oblyx-layout-footer>
</oblyx-layout>
</div>div {
classes = setOf("docs-shell-frame")
oblyxLayout(defaultHasDividers = true) {
oblyxLayoutHeader(height = "56px", landmarkRole = "banner") {
strong { +"Acme dashboard" }
}
oblyxLayoutPanel(area = OblyxLayoutPanelArea.START, width = "160px", hasDivider = true, landmarkRole = "navigation", label = "Primary") {
p { +"Overview" }
p { +"Projects" }
p { +"Settings" }
}
oblyxLayoutContent(landmarkRole = "main", label = "Report") {
p { +"Row 1 of a long scrolling report." }
p { +"Row 2 of a long scrolling report." }
p { +"Row 3 of a long scrolling report." }
p { +"Row 4 of a long scrolling report." }
p { +"Row 5 of a long scrolling report." }
p { +"Row 6 of a long scrolling report." }
p { +"Row 7 of a long scrolling report." }
p { +"Row 8 of a long scrolling report." }
}
oblyxLayoutFooter(height = "40px") {
span { +"6 active projects" }
}
}
}Layout vs. card
oblyx-card's header/body/footer family reads as superficially similar (both are "a region with a header and a footer"), but they're built for different jobs. Card is a flat vertical stack with no side panels and no per-region scrolling; Layout is two-dimensional, with a start/end panel row, independently scrollable regions, and the cascading divider default described below. Nest a card inside a layout's content region, or a layout inside a card's body, if you need both; the two families share no CSS. See choosing a layout primitive for the fuller comparison.
Side panels
area="start" or area="end" places a panel on either side of the content region; a layout can have zero, one, or both. area has no default in practice: omitting it renders as start but logs a console error, since panel placement is a real content decision every panel should make explicitly.
Start and end panels
<div class="docs-shell-frame" style="height: 220px;">
<oblyx-layout>
<oblyx-layout-panel area="start" width="120px" has-divider>Start panel</oblyx-layout-panel>
<oblyx-layout-content>Main content</oblyx-layout-content>
<oblyx-layout-panel area="end" width="120px" has-divider>End panel</oblyx-layout-panel>
</oblyx-layout>
</div>div {
classes = setOf("docs-shell-frame")
style = "height: 220px;"
oblyxLayout {
oblyxLayoutPanel(area = OblyxLayoutPanelArea.START, width = "120px", hasDivider = true) {
+"Start panel"
}
oblyxLayoutContent { +"Main content" }
oblyxLayoutPanel(area = OblyxLayoutPanelArea.END, width = "120px", hasDivider = true) {
+"End panel"
}
}
}Height
height="fill" (the default) stretches the layout to its container's height, the shape every example above uses. height="auto" sizes it to its content instead, for a layout that isn't meant to occupy a fixed region of the page.
Layout sized to content
<oblyx-layout height="auto">
<oblyx-layout-header>Header, sized to content</oblyx-layout-header>
<oblyx-layout-content>Content, sized to content</oblyx-layout-content>
<oblyx-layout-footer>Footer, sized to content</oblyx-layout-footer>
</oblyx-layout>oblyxLayout(height = OblyxLayoutHeight.AUTO) {
oblyxLayoutHeader { +"Header, sized to content" }
oblyxLayoutContent { +"Content, sized to content" }
oblyxLayoutFooter { +"Footer, sized to content" }
}Content width
content-width caps and centers the inner box of the header, footer and content regions (any valid CSS length). The start/end panels themselves aren't constrained by it; only the row's centered content is, since Oblyx doesn't wrap the whole start/content/end row the way upstream does.
Layout with a capped content width
<div class="docs-shell-frame" style="height: 160px;">
<oblyx-layout content-width="320px">
<oblyx-layout-header has-divider>Centered, capped at 320px</oblyx-layout-header>
<oblyx-layout-content>The content region's inner box is centered and capped the same way.</oblyx-layout-content>
</oblyx-layout>
</div>div {
classes = setOf("docs-shell-frame")
style = "height: 160px;"
oblyxLayout(contentWidth = "320px") {
oblyxLayoutHeader(hasDivider = true) { +"Centered, capped at 320px" }
oblyxLayoutContent { +"The content region's inner box is centered and capped the same way." }
}
}Dividers
has-divider on a header or footer draws a border on the edge facing the content region. Setting default-has-dividers on the layout itself reflects has-divider onto its header and footer once, at connect time, rather than requiring the attribute on every region by hand: a real reflected HTML attribute, not hidden state, so the header and footer's own CSS keeps reading the same [has-divider] selector either way. Panels don't participate in this cascade: a panel's has-divider always defaults to false regardless of the layout's setting, matching upstream.
default-has-dividers
Same markup, only the layout's default-has-dividers differs.
No default-has-dividers
default-has-dividers
<div>
<p class="docs-demo-label">No default-has-dividers</p>
<div class="docs-shell-frame" style="height: 140px; margin-bottom: 20px;">
<oblyx-layout>
<oblyx-layout-header>Header</oblyx-layout-header>
<oblyx-layout-content>Content</oblyx-layout-content>
<oblyx-layout-footer>Footer</oblyx-layout-footer>
</oblyx-layout>
</div>
<p class="docs-demo-label">default-has-dividers</p>
<div class="docs-shell-frame" style="height: 140px;">
<oblyx-layout default-has-dividers>
<oblyx-layout-header>Header</oblyx-layout-header>
<oblyx-layout-content>Content</oblyx-layout-content>
<oblyx-layout-footer>Footer</oblyx-layout-footer>
</oblyx-layout>
</div>
</div>div {
p(classes = "docs-demo-label") { +"No default-has-dividers" }
div {
classes = setOf("docs-shell-frame")
style = "height: 140px; margin-bottom: 20px;"
oblyxLayout {
oblyxLayoutHeader { +"Header" }
oblyxLayoutContent { +"Content" }
oblyxLayoutFooter { +"Footer" }
}
}
p(classes = "docs-demo-label") { +"default-has-dividers" }
div {
classes = setOf("docs-shell-frame")
style = "height: 140px;"
oblyxLayout(defaultHasDividers = true) {
oblyxLayoutHeader { +"Header" }
oblyxLayoutContent { +"Content" }
oblyxLayoutFooter { +"Footer" }
}
}
}Padding
padding on <oblyx-layout> sets the outer edge padding, applied to whichever header/content/footer/panel edges have no adjacent sibling region; each region also accepts its own padding to override just that region. Both take a step on the same spacing token scale as oblyx-stack and oblyx-grid, including the two steps, 0 and 0-5, whose generated Kotlin constants are prefixed with an underscore (OblyxLayoutPadding._0,OblyxLayoutPadding._0_5) because a Kotlin identifier can't start with a digit.
Reference
<oblyx-layout>
| Attribute | Type | Default | Description |
|---|---|---|---|
height | fillauto | fill | Whether the layout stretches to fill its container or sizes to its own content. |
padding | 00-511-523456810 | – | Outer edge padding, applied to whichever header/content/footer/panel edges have no adjacent sibling region. Unset falls back to the --ox-spacing-4 (16px) token. |
content-width | string | undefined | – | Max width applied to <oblyx-layout-content>'s inner box, centered via margin-inline: auto. Any valid CSS length ("960px", "60ch", …), applied as a raw inline style. Constrains the content region only, not the surrounding start/end panels. |
default-has-dividers | boolean | false | Cascading default for descendant header/footer has-divider. |
<oblyx-layout-header>
| Attribute | Type | Default | Description |
|---|---|---|---|
has-divider | boolean | false | Divider border between this header and the content below it. |
height | string | undefined | – | Raw CSS length ("64px", …), applied to the host directly as inline-style sizing. |
padding | 00-511-523456810 | – | Overrides the layout's default inner padding for this region alone. |
label | string | undefined | – | aria-label for the landmark. |
landmark-role | string | undefined | – | ARIA landmark role, e.g. 'banner' for a site-wide header. Unset by default, leaving it to the caller rather than assuming every header is a page-level banner. Named landmarkRole/landmark-role, not role, since role is a native HTMLElement IDL property. |
<oblyx-layout-content>
| Attribute | Type | Default | Description |
|---|---|---|---|
padding | 00-511-523456810 | – | Overrides the layout's default inner padding for this region alone. |
scrollable | boolean | true | Whether this region scrolls its own overflow. |
label | string | undefined | – | aria-label for the landmark. |
landmark-role | string | undefined | – | ARIA landmark role, e.g. 'main'. Unset by default, same reasoning as oblyx-layout-header's landmarkRole. |
<oblyx-layout-panel>
| Attribute | Type | Default | Description |
|---|---|---|---|
area | startend | – | Which side of the content row this panel occupies. Unset renders as 'start' (the base CSS rule's default grid-area) but logs a warning: panel placement is a real content decision every panel should make explicitly, not a silently-assumed default. |
has-divider | boolean | false | Divider border on the side facing the content region. |
padding | 00-511-523456810 | – | Overrides the layout's default inner padding for this region alone. |
scrollable | boolean | true | Whether this region scrolls its own overflow. |
width | string | undefined | – | Raw CSS length, applied to the host directly as inline-style sizing. |
label | string | undefined | – | aria-label for the landmark. |
landmark-role | string | undefined | – | ARIA landmark role, e.g. 'navigation' or 'complementary'. See oblyx-layout-header's landmarkRole doc comment for why this isn't named role. |
<oblyx-layout-footer>
| Attribute | Type | Default | Description |
|---|---|---|---|
has-divider | boolean | false | Divider border between this footer and the content above it. |
height | string | undefined | – | Raw CSS length, applied to the host directly. |
padding | 00-511-523456810 | – | Overrides the layout's default inner padding for this region alone. |
label | string | undefined | – | aria-label for the landmark. |
landmark-role | string | undefined | – | ARIA landmark role, e.g. 'contentinfo' for a site-wide footer. See oblyx-layout-header's landmarkRole doc comment for why this isn't named role. |