Navigation

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.

Acme dashboard

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.

6 active projects
HTMLKotlin
<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

Start panel Main content End panel
HTMLKotlin
<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

Header, sized to content Content, sized to content Footer, sized to content
HTMLKotlin
<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

Centered, capped at 320px The content region's inner box is centered and capped the same way.
HTMLKotlin
<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

Header Content Footer

default-has-dividers

Header Content Footer
HTMLKotlin
<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>

AttributeTypeDefaultDescription
heightfillautofillWhether the layout stretches to fill its container or sizes to its own content.
padding00-511-523456810Outer 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-widthstring | undefinedMax 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-dividersbooleanfalseCascading default for descendant header/footer has-divider.

<oblyx-layout-header>

AttributeTypeDefaultDescription
has-dividerbooleanfalseDivider border between this header and the content below it.
heightstring | undefinedRaw CSS length ("64px", …), applied to the host directly as inline-style sizing.
padding00-511-523456810Overrides the layout's default inner padding for this region alone.
labelstring | undefinedaria-label for the landmark.
landmark-rolestring | undefinedARIA 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>

AttributeTypeDefaultDescription
padding00-511-523456810Overrides the layout's default inner padding for this region alone.
scrollablebooleantrueWhether this region scrolls its own overflow.
labelstring | undefinedaria-label for the landmark.
landmark-rolestring | undefinedARIA landmark role, e.g. 'main'. Unset by default, same reasoning as oblyx-layout-header's landmarkRole.

<oblyx-layout-panel>

AttributeTypeDefaultDescription
areastartendWhich 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-dividerbooleanfalseDivider border on the side facing the content region.
padding00-511-523456810Overrides the layout's default inner padding for this region alone.
scrollablebooleantrueWhether this region scrolls its own overflow.
widthstring | undefinedRaw CSS length, applied to the host directly as inline-style sizing.
labelstring | undefinedaria-label for the landmark.
landmark-rolestring | undefinedARIA 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>

AttributeTypeDefaultDescription
has-dividerbooleanfalseDivider border between this footer and the content above it.
heightstring | undefinedRaw CSS length, applied to the host directly.
padding00-511-523456810Overrides the layout's default inner padding for this region alone.
labelstring | undefinedaria-label for the landmark.
landmark-rolestring | undefinedARIA 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.