Skip to content
Component installs need the registry setup
VegaStack Design

Document Layout

A page read as a document — title, a fluid text column, a contents rail before it and a comment margin after it — on one grid with five tiers by its own width.

Status
stable
Since
0.25.35
Accessibility pattern
two labelled complementary landmarks, status skeleton

Last updated

Pricing v2

Updated 3m ago by Sanjay · 3 pages inside

Pricing for the annual plan is set per seat and billed once a year. Teams that grow during the term add seats at the same rate, prorated to the renewal date, and a team that shrinks keeps its seats until the term ends.

Seats

Discounts apply from twenty seats. Every plan includes the same support, the same storage and the same security review, so the choice between plans is about the number of people, never about features.

Renewal

  • Annual plans renew on the same date each year
  • Monthly plans can move to annual at any time

Limits

Seats are capped at 500 per workspace; a larger organisation splits into workspaces.

Pages inside · 2

  • Annual plan
  • Add-ons

Install

Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Document Layout before installation. Retain the digest printed by this command.

pnpm exec vegastack-design verify document-layout

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/document-layout

Run the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.

The same command installs the registry items it composes: @vegastack/badge, @vegastack/button, @vegastack/input-group, @vegastack/skeleton, @vegastack/item, @vegastack/use-overflow, @vegastack/sheet, @vegastack/tabs.

Usage

import {
  DocumentLayout,
  DocumentLayoutBody,
  DocumentLayoutContents,
  DocumentLayoutFoot,
  DocumentLayoutHeader,
  DocumentLayoutIcon,
  DocumentLayoutMargin,
  DocumentLayoutTitle,
  DocumentLayoutTitleGroup,
} from "@/components/ui/document-layout";

<DocumentLayout margin={threadCount > 0 || drafting}>
  <DocumentLayoutHeader>
    <DocumentLayoutTitleGroup>
      <DocumentLayoutIcon>📘</DocumentLayoutIcon>
      <DocumentLayoutTitle>Pricing v2</DocumentLayoutTitle>
    </DocumentLayoutTitleGroup>
    <MetaLine>
      <MetaLineItem>Updated 3m ago</MetaLineItem>
    </MetaLine>
  </DocumentLayoutHeader>
  {outline.length > 1 ? (
    <DocumentLayoutContents>
      <TableOfContents
        variant="rail"
        side="start"
        sticky={false}
        items={outline}
      />
    </DocumentLayoutContents>
  ) : null}
  <DocumentLayoutBody>
    <MarkdownView typeScale="document">{markdown}</MarkdownView>
  </DocumentLayoutBody>
  <DocumentLayoutFoot>{pagesInside}</DocumentLayoutFoot>
  <DocumentLayoutMargin>{threads}</DocumentLayoutMargin>
</DocumentLayout>;

DocumentLayout is the frame of a page people read and write as a document — a wiki page, a spec, its public view and a version's full view all wear the same one. The text column is fluid: about three quarters of the layout's width, between bounds that grow with each tier, so a large monitor gets a generous column rather than a narrow strip. The title area sits above the text and what follows it — sub-pages, the page's own comments — below, in the same column. Before the text, in its gutter, a contents rail; after it, when there is room, a comment margin.

The layout follows its own width, not the window's (container queries in rem), so a collapsed sidebar or a split view lands in the right tier:

TierWidthGuttersTextContentsComments
S< 40rem0the full widththe host's menu or sheetthe host's sheet or popover
M40–64rem3remmax(36rem, 75%)the rail, in the guttera popover at the highlight
L64–90rem3remclamp(40rem, 75%, 52rem)the rail, in the gutterthe margin (20rem)
XL90–120rem4rem60remthe rail, in the gutterthe margin (24rem)
2XL≥ 120rem4rem64remthe rail, in the gutterthe margin (24rem)

At the document type scale (14px Geist) that is about 94 characters a line at M's 36rem, 128 at L's 52rem, 150 at XL and 161 at 2XL; the 24px line height carries the longer lines.

The tier bounds are the system's layout tiers — tablet 40rem, laptop 64rem, wide 90rem (LAYOUT_TIERS in page-layout) — plus a document-only 120rem step.

The rail, the text and the margin are one block, centred, and the text stays in the middle when the comment margin shows: the start side widens to mirror the margin, so wherever the width can hold the margin on both sides the text sits exactly where it sat without comments. With less room the start side gives way first, and the margin shifts the block towards the start only by what is missing; the text narrows only when the width cannot hold the text, the margin and the gutters at once.

It is one subtree at every width: a column with no room is display: none, never unmounted, so an editor inside never remounts when the window resizes. margin shows the comment column from L up; set it only while the column holds something (open threads beside the text, or a comment being written), decided with the text so the first paint is right. A page's first comment keeps its text in place while there is room for the margin on both sides. From M up the grid keeps its start gutter whether or not a page has headings, and DocumentLayoutContents sits in it, stacked above the text and the margin: a TableOfContents variant="rail" side="start" there is a slim column of dashes that opens into the labelled list over the text — so headings that arrive (or go) never move the text, and the open list takes the pointer wherever it lies. Put the parts in reading order — header, contents, body, foot, margin. Server-safe: the tiers are CSS.

Pricing v2

Updated 3m ago by Sanjay · 3 pages inside

Pricing for the annual plan is set per seat and billed once a year. Teams that grow during the term add seats at the same rate, prorated to the renewal date, and a team that shrinks keeps its seats until the term ends.

Seats

Discounts apply from twenty seats. Every plan includes the same support, the same storage and the same security review, so the choice between plans is about the number of people, never about features.

Renewal

  • Annual plans renew on the same date each year
  • Monthly plans can move to annual at any time

Limits

Seats are capped at 500 per workspace; a larger organisation splits into workspaces.

Pages inside · 2

  • Annual plan
  • Add-ons

Anatomy

DocumentLayout — data-slot="document-layout" | "document-layout-grid"
DocumentLayoutHeader — data-slot="document-layout-header"
DocumentLayoutTitleGroup — data-slot="document-layout-title-group"
DocumentLayoutIcon — data-slot="document-layout-icon"
DocumentLayoutTitle — data-slot="document-layout-title"
DocumentLayoutBody — data-slot="document-layout-body"
DocumentLayoutFoot — data-slot="document-layout-foot"
DocumentLayoutContents — data-slot="document-layout-contents" | "document-layout-contents-inner"
DocumentLayoutMargin — data-slot="document-layout-margin"
DocumentLayoutSkeleton — data-slot="document-layout-skeleton" | "document-layout-skeleton-line"
DocumentLayoutContentsButton — data-slot="document-layout-contents-button"
DocumentLayoutMeta — data-slot="document-layout-meta" | "document-layout-meta-row"
DocumentLayoutMetaItem — data-slot="document-layout-meta-item"
DocumentLayoutMetaGroup — data-slot="document-layout-meta-group"
DocumentLayoutMetaField — data-slot="document-layout-meta-field"
DocumentLayoutMetaButton — data-slot="document-layout-meta-button"
DocumentLayoutMetaLink
DocumentLayoutMetaSkeleton
DocumentLayoutMetaWords — data-slot="document-layout-meta-words" | "document-layout-meta-words-rest"
DocumentLayoutMetaBadge
DocumentLayoutFindBar — data-slot="document-layout-find-bar" | "document-layout-find-status"
DocumentLayoutFloatingButton — data-slot="document-layout-floating-button"
DocumentLayoutJumpCue — data-slot="document-layout-jump-cue"
DocumentLayoutNote — data-slot="document-layout-note"
DocumentLayoutStarter — data-slot="document-layout-starter"
DocumentLayoutStarters — data-slot="document-layout-starters"
DocumentLayoutDecisionBar — data-slot="document-layout-decision-bar" | "document-layout-dock-space"
DocumentLayoutDecisionBarActions — data-slot="document-layout-decision-bar-actions"
DocumentLayoutDecisionBarText — data-slot="document-layout-decision-bar-text"
DocumentLayoutBlock — data-slot="document-layout-block"
DocumentLayoutBlockAction
DocumentLayoutBlockFooter — data-slot="document-layout-block-footer"
DocumentLayoutBlockHeader — data-slot="document-layout-block-header"
DocumentLayoutBlockNote — data-slot="document-layout-block-note"
DocumentLayoutBlockRow — data-slot="document-layout-block-row"
DocumentLayoutBlockRowMain — data-slot="document-layout-block-row-main"
DocumentLayoutBlockRowMeta — data-slot="document-layout-block-row-meta"
DocumentLayoutBlockRowRef — data-slot="document-layout-block-row-ref"
DocumentLayoutBlockRows — data-slot="document-layout-block-rows"
DocumentLayoutBlockSkeleton — data-slot="document-layout-block-skeleton"
DocumentLayoutBlockTitle — data-slot="document-layout-block-title"
DocumentLayoutBlockTable — data-slot="document-layout-block-table"
DocumentLayoutBlockTableBody — data-slot="document-layout-block-table-body"
DocumentLayoutBlockTableCell — data-slot="document-layout-block-table-cell"
DocumentLayoutBlockTableColumn — data-slot="document-layout-block-table-column"
DocumentLayoutBlockTableHead — data-slot="document-layout-block-table-head"
DocumentLayoutBlockTableRow — data-slot="document-layout-block-table-row"
DocumentLayoutPageItem — data-slot="document-layout-page-item"
DocumentLayoutPageLink — data-slot="document-layout-page-link"
DocumentLayoutPageList — data-slot="document-layout-page-list"
DocumentLayoutSection — data-slot="document-layout-section"
DocumentLayoutSectionButton
DocumentLayoutSectionHeader — data-slot="document-layout-section-header"
DocumentLayoutSectionSkeleton — data-slot="document-layout-section-skeleton"
DocumentLayoutSectionTitle — data-slot="document-layout-section-title"
DocumentLayoutAside — data-slot="document-layout-aside" | "document-layout-aside-body" | "document-layout-aside-close" | "document-layout-aside-count" | "document-layout-aside-header" | "document-layout-aside-panel" | "document-layout-aside-panel-frame" | "document-layout-aside-sheet" | "document-layout-aside-tab-count" | "document-layout-aside-tablist" | "document-layout-aside-tabs"

Examples

Wide

measure="wide" gives the text the whole width less the gutters (and the margin, where it shows), up to 75rem (1200px), past which a line is too long to read — each reader's choice, never the document's.

Pricing v2

Updated 3m ago by Sanjay · 3 pages inside

Pricing for the annual plan is set per seat and billed once a year. Teams that grow during the term add seats at the same rate, prorated to the renewal date, and a team that shrinks keeps its seats until the term ends.

Seats

Discounts apply from twenty seats. Every plan includes the same support, the same storage and the same security review, so the choice between plans is about the number of people, never about features.

Renewal

  • Annual plans renew on the same date each year
  • Monthly plans can move to annual at any time

Limits

Seats are capped at 500 per workspace; a larger organisation splits into workspaces.

Aside

aside gives the layout a rail after the text: "discussion" (the comments) or "outline" (the contents), and null while it is closed. A layout with an aside — even null — keeps the end track for it from the L tier, so opening or closing the rail never moves or narrows the text; in wide the text and an open aside share wide's min(100%, 1200px) block — the text keeps its start edge and gives the aside its end. Put a DocumentLayoutAside of each kind in the layout and the one aside names shows:

  • Discussion — a non-modal rail as tall as the window: the heading with its count, a × (onClose), and tabs such as Open · Resolved · Page, plus Review while a review is open, each with an optional count. The page stays readable and editable beside it. It takes the comment margin's place, so pass CommentMargin quote={false} for the cards beside the text and keep the quote in the rail. Below the L tier it is a bottom sheet instead, the whole screen below 640px.
  • Outline — the contents list, pinned from the XL tier while the discussion is closed; below XL the contents rail's hover ticks stay.

Leave aside unset and nothing changes: the margin and the rail behave as before.

Pricing v2

Updated 3m ago by Sanjay · 3 pages inside

Pricing for the annual plan is set per seat and billed once a year. Teams that grow during the term add seats at the same rate, prorated to the renewal date, and a team that shrinks keeps its seats until the term ends.

Seats

Discounts apply from twenty seats. Every plan includes the same support, the same storage and the same security review, so the choice between plans is about the number of people, never about features.

Renewal

  • Annual plans renew on the same date each year
  • Monthly plans can move to annual at any time

Limits

Seats are capped at 500 per workspace; a larger organisation splits into workspaces.

<DocumentLayout
  aside={discussion ? "discussion" : wideScreen ? "outline" : null}
>
  …
  <DocumentLayoutAside
    count={open.length}
    onClose={() => setDiscussion(false)}
    tabs={[
      {
        value: "open",
        label: "Open",
        count: open.length,
        content: <OpenThreads />,
      },
      { value: "resolved", label: "Resolved", content: <ResolvedThreads /> },
      { value: "page", label: "Page", content: <PageThread /> },
    ]}
  />
  <DocumentLayoutAside kind="outline">
    <TableOfContents items={outline} sticky={false} />
  </DocumentLayoutAside>
</DocumentLayout>

Title

DocumentLayoutTitle is the document type scale's top step: 30/36 semibold in the heading face, one step above the text's # (24/32). DocumentLayoutTitleGroup holds an optional DocumentLayoutIcon over it, 4px apart: an emoji at the title's size, or the control that changes it. reveal hides an "Add icon" control until the title group is hovered or holds focus (always shown on a coarse pointer). The row is 32px whatever it holds, so showing, adding or removing an icon moves nothing.

<DocumentLayoutTitleGroup>
  <DocumentLayoutIcon reveal>
    <Button variant="ghost" size="xs">
      <SmilePlus aria-hidden />
      Add icon
    </Button>
  </DocumentLayoutIcon>
  <DocumentLayoutTitle>Pricing v2</DocumentLayoutTitle>
</DocumentLayoutTitleGroup>

Meta lines

DocumentLayoutMeta is a line of small facts under the title — who updated the page, where it came from, what links to it. Each DocumentLayoutMetaItem opens with a ·, and the row is drawn one dot's width to the start of a clipped box, so a wrapped line never starts (or the line before it ends) with a stray dot. An item takes an icon, and nowrap keeps a fact built around a link on one line (the link truncates); DocumentLayoutMetaButton is a fact that opens something ("Used in 2 tasks") and DocumentLayoutMetaLink a fact's link (render takes the router's link).

variant="status" is the status row: one reserved line from sm and two on a phone, from the first paint, so chips that stream in move nothing. Group its facts: a DocumentLayoutMetaGroup (kind="unit") stays together — a DocumentLayoutMetaField ("Owner Sanjay") and its chip, wrapping inside only when even the chip's short form doesn't fit — and a kind="flow" group of flags flows with the row from sm; on a phone each group is a whole line. A DocumentLayoutMetaBadge with label and short keeps all its words while the row has room; on a tight row the chips drop to their short forms, the longest first (the whole label stays in the chip's tooltip), and only if those don't fit either does the row wrap. Pass what the chips say as watch, and DocumentLayoutMetaSkeleton while they stream in.

<DocumentLayoutMeta variant="status" watch={[status, review]}>
  <DocumentLayoutMetaGroup>
    <DocumentLayoutMetaField label="Owner">
      Sanjay Desai
    </DocumentLayoutMetaField>
    <DocumentLayoutMetaBadge
      variant="success"
      label="Verified · Oct 14"
      short="Verified"
    >
      <BadgeCheck data-icon="inline-start" aria-hidden />
    </DocumentLayoutMetaBadge>
  </DocumentLayoutMetaGroup>
  <DocumentLayoutMetaGroup kind="flow">
    <DocumentLayoutMetaItem>
      <DocumentLayoutMetaBadge variant="warning">
        2 out of date
      </DocumentLayoutMetaBadge>
    </DocumentLayoutMetaItem>
  </DocumentLayoutMetaGroup>
</DocumentLayoutMeta>

Where the contents rail has no room (the S tier), DocumentLayoutContentsButton is a round "On this page" button floating at the window's bottom end — give it to TableOfContents as its trigger. DocumentLayoutTitle takes editable when the title renames in place (the text cursor over the whole row).

Page tools

The tools a page lays over or around its text, each without a class of your own:

  • DocumentLayoutFindBar — find in the text: a search field docked at the top of the text on a band of the page's background, with where the find stands ("3 of 12") and Previous, Next and Close (Enter, Shift+Enter, Escape). The host finds the matches; pass highlights (two CSS Custom Highlight names) and the bar paints them.
  • DocumentLayoutJumpCue — "2 new comments ↓", a pill pinned to the bottom of the view while something new is out of sight; it takes no room.
  • DocumentLayoutFloatingButton — a phone's one floating action ("Edit"), at the window's bottom end, clear of the home indicator.
  • DocumentLayoutStarters with DocumentLayoutStarter pills — ways to start an empty page ("Start from: Meeting notes · Spec · More templates…"; more for the quiet last one).
  • DocumentLayoutNote — a quiet line under the text.
  • DocumentLayoutDecisionBar with DocumentLayoutDecisionBarText and DocumentLayoutDecisionBarActions — a decision the reader owes the page ("Asha asked you to review this page · Approve"). Put it first in DocumentLayoutBody: it sticks under the top bar while the text is in view, and on a phone floats at the bottom with the floating buttons above it.
  • DocumentLayout focusable — the canvas takes focus from a click in its read text (never a tab stop) and shows no focus cue of its own.

Sections, pages and live blocks

What a document holds besides its prose, again without a class of your own:

  • DocumentLayoutSection — a section at the foot ("Pages inside"), named by its DocumentLayoutSectionTitle (count follows it in the muted ink: "Pages inside · 3"), its title row a DocumentLayoutSectionHeader with an action at the end. DocumentLayoutSectionButton is its quiet button: look="more" loads the rest ("Show more pages", "Load earlier"), look="add" is the one line an empty section shows to someone who can fill it. DocumentLayoutSectionSkeleton holds its place while it loads.
  • DocumentLayoutPageList with DocumentLayoutPageItem rows — the pages under a document, ruled above, between and below. DocumentLayoutPageLink is a row's target: its emoji (icon, a page icon without one), name, one-line excerpt and meta, the whole row the link; a ⋯ menu follows it in the item.
  • DocumentLayoutBlock — a live block drawn into the text (a task list): a card with a DocumentLayoutBlockHeader (DocumentLayoutBlockTitle, a ghost button at the end), then DocumentLayoutBlockRows of DocumentLayoutBlockRows (DocumentLayoutBlockRowMain with a DocumentLayoutBlockRowRef, then DocumentLayoutBlockRowMeta, which wraps under the main line below 640px), or a DocumentLayoutBlockNote ("No tasks match") or a DocumentLayoutBlockSkeleton, and a DocumentLayoutBlockFooter. DocumentLayoutBlockAction is the link-styled action inside it (Try again, Open in Tasks →). A block of a table's records shows them as a DocumentLayoutBlockTable (columns, the name column first): a DocumentLayoutBlockTableHead row of DocumentLayoutBlockTableColumns, then a DocumentLayoutBlockTableBody of DocumentLayoutBlockTableRows of DocumentLayoutBlockTableCells (primary on the name column's, a row header). It is read-only, every row's columns line up, and it scrolls sideways when its columns don't fit.

Pricing v2

Updated 3m ago by Sanjay · 3 pages inside

Open tasks · this page
T-12Order the seat licencesdue Oct 24
T-14Send the renewal dates to financedue Oct 30
Sample requests · SR
Name
Status
Qty
Lux survey for the atrium refit
Requested
12
Sample of the linear pendant
Sent
3

Skeleton

DocumentLayoutSkeleton is the page's loading state on the SAME grid and tier — an icon row (icon), a title line, metaLines meta lines (default 1) and paragraphs of text bars at the document line pitch (a 14px bar every 24px, 16 every 26 on a coarse pointer), 12px between paragraphs — so nothing moves when the page arrives. Pass it the margin, measure, icon and metaLines the page will have. It is one status region, named by label.

The tier in script

Branches CSS cannot take — comments in the margin or in a popover, the contents on the rail or in a menu — read the tier with useDocumentLayoutTier(ref). It reads --document-layout-tier, which the container queries set, so script and stylesheet never disagree about where a tier starts. fallback (default "xl") is the answer on the server and before the first measurement.

import { useDocumentLayoutTier } from "@/components/ui/document-layout";

const ref = React.useRef<HTMLDivElement>(null);
const tier = useDocumentLayoutTier(ref);

<DocumentLayout ref={ref} margin>
  …
  {tier !== "s" && tier !== "m" ? (
    <DocumentLayoutMargin>{threads}</DocumentLayoutMargin>
  ) : null}
</DocumentLayout>;

Print

A document prints as one column of its header, body and foot — the contents and the comment margin drop out — on a named page with a 16mm × 14mm paper margin, in the light palette whatever the reader's theme. With typeScale="document", MarkdownView and TextEdit add the text's own print rules: code wraps, tables print in full, highlights and controls drop out, and an external link prints its address.

API Reference

PropTypeDefaultDescription
aside"discussion" | "outline" | null—The aside after the text: "discussion" (the comments rail), "outline" (the contents, pinned from the XL tier — below it the contents rail's hover ticks stay) or null (closed). Giving it at all, even null, keeps the end track for the aside from the L tier, so opening or closing it never moves the text (in wide, an open aside takes its width out of wide's 1200px block and the text keeps its start edge). Put a DocumentLayoutAside of each kind in the layout; the one named here shows. An open discussion takes the comment margin's place; the pinned outline replaces the contents rail. Leave it unset for a layout with no aside (the margin and rail behave as before).
focusablebooleanfalseThe canvas takes focus from a click in its read text (tabIndex={-1}, never a tab stop), so a host knows the reader is in the page; it shows no focus cue of its own (the text does).
marginbooleanfalseShow the comment margin (from the L tier): set it only while the margin holds something — open threads beside the text or a comment being written — and decide it with the text (on the server, from the thread count), so the first paint is right. The text stays centred either way while the width holds the margin on both sides; below that the margin shifts the block towards the start.
measure"document" | "wide""document"The text column: document (fluid, about three quarters of the layout within each tier's bounds) or wide (the whole width less the gutters and the margin, up to 75rem — 1200px; each reader's own choice, never the document's).

Data attributes and CSS variables on DocumentLayout

AttributeValues
data-asidemirrors a prop or state value
data-margin""
data-measuremirrors a prop or state value
data-slot"document-layout" | "document-layout-grid"
PropTypeDefaultDescription
childrenReact.ReactNode—What the aside shows when it has no tabs.
closeLabelstring"Close"The × button's name.
countnumber—A count after the title — the open threads.
defaultValuestringthe first tabThe tab selected first, uncontrolled.
kind"discussion" | "outline""discussion"Which aside this is: discussion (the comments rail; a bottom sheet below the L tier) or outline (the contents, pinned from the XL tier). It shows while DocumentLayout's aside names it.
onClose(() => void)—Close the aside (set the layout's aside to null): a × in the heading, and the sheet's own close below L. Without it the aside has no close control.
onValueChange((value: string) => void)—Called when the reader picks a tab.
tabsreadonly DocumentLayoutAsideTab[]—Tabs under the heading, in order — Open · Resolved · Page, and Review while a review is open. Without tabs the aside shows children.
titlestring"Comments" (discussion), "On this page" (outline)The heading, and the region's name. The outline shows no heading unless one is given (its TableOfContents carries its own title); its region is still named.
valuestring—The selected tab, controlled. Pair with onValueChange.

Data attributes and CSS variables on DocumentLayoutAside

AttributeValues
data-kindmirrors a prop or state value
data-slot"document-layout-aside" | "document-layout-aside-body" | "document-layout-aside-close" | "document-layout-aside-count" | "document-layout-aside-header" | "document-layout-aside-panel" | "document-layout-aside-panel-frame" | "document-layout-aside-sheet" | "document-layout-aside-tab-count" | "document-layout-aside-tablist" | "document-layout-aside-tabs"
PropTypeDefaultDescription
aside"discussion" | "outline" | null—The aside, as the page will (its track, so the text lands where the page puts it).
iconbooleanfalseReserve the icon row above the title (32px and the group's 4px), as a page whose DocumentLayoutTitleGroup holds a DocumentLayoutIcon will — an editable page always does.
labelstring"Loading page"The loading state's accessible name.
linesnumber8How many lines of text to sketch, in paragraphs of 3, 2 and 3 lines.
marginbooleanfalseReserve the comment margin, as the page will.
measure"document" | "wide""document"The text column, as the page will.
metaLinesnumber1How many 20px meta lines to sketch under the title (8px apart), as the page's header has.

Data attributes and CSS variables on DocumentLayoutSkeleton

AttributeValues
data-slot"document-layout-skeleton" | "document-layout-skeleton-line"
PropTypeDefaultDescription
revealbooleanfalseHidden until the title group is hovered or holds focus (a fine pointer only; a coarse pointer always sees it) — for an "Add icon" control on a page that has none. It keeps its row, so showing it moves nothing.

Data attributes and CSS variables on DocumentLayoutIcon

AttributeValues
data-reveal""
data-slot"document-layout-icon"
PropTypeDefaultDescription
variant"line" | "status""line"line — facts that wrap freely. status — the status row: one reserved line from sm, two on a phone, its chips fitted to the width (short forms, then wrapping).
watchreadonly unknown[][]status only: what the chips say — the row is fitted again whenever any of these change (it is also fitted on every width change and once web fonts arrive).

Data attributes and CSS variables on DocumentLayoutMeta

AttributeValues
data-slot"document-layout-meta" | "document-layout-meta-row"
data-variantmirrors a prop or state value
PropTypeDefaultDescription
labelstring—The chip's whole label; with short, its words give way to the short form on a tight status row (and the chip's title defaults to it). Without, the chip shows its children and keeps its size.
shortstring—The short form, a prefix of label ("Verified").
variant"default" | "destructive" | "ghost" | "info" | "link" | "outline" | "secondary" | "success" | "warning" | null—

Data attributes and CSS variables on DocumentLayoutMetaBadge

AttributeValues
data-fitted""
PropTypeDefaultDescription
hasMatches*boolean—There are matches to step through (Previous and Next are disabled without).
onClose*() => void—Closes the find (also Escape in the field).
onNext*() => void—The next match (also Enter in the field).
onPrevious*() => void—The previous match (also Shift+Enter in the field).
onValueChange*(value: string) => void—Called with the query as it is typed.
value*string—The query.
autoFocusbooleanfalseFocus the field when the bar mounts.
classNamestring—Classes for the bar.
highlights{ all: string; current: string; }undefined (the host paints its own)The CSS Custom Highlight names the host paints its matches under: every match on a light primary tint, the current one in the primary ink.
labelstring"Find in page"The bar's accessible name and the field's.
statusstring—Where the find stands ("3 of 12", "No matches"); nothing while the query is empty.

Data attributes and CSS variables on DocumentLayoutFindBar

AttributeValues
data-slot"document-layout-find-bar" | "document-layout-find-status"
PropTypeDefaultDescription
countReact.ReactNode—How many the section holds, after the title in the muted ink ("Pages inside · 3").

Data attributes and CSS variables on DocumentLayoutSectionTitle

AttributeValues
data-slot"document-layout-section-title"
PropTypeDefaultDescription
loadingboolean—Shows a spinner over the label, blocks activation and sets aria-busy. The label keeps its box at opacity: 0, so the button's width does not move and its accessible name survives (API-5, A11Y-12).
look"add" | "more""more"more — a quiet ghost button at the start of the section ("Show more pages", "Load earlier"); add — the bare line a section is while it holds nothing ("+ Add a page inside"), muted until hovered, with no fill.

Data attributes and CSS variables on DocumentLayoutSectionButton

AttributeValues
data-lookmirrors a prop or state value
PropTypeDefaultDescription
title*React.ReactNode—The page's name: it takes the row before the excerpt does, cut short to one line.
excerptReact.ReactNode—One line of its text, after the name from 640px, under it below.
highlightedboolean—
iconstring | null—The page's own icon (an emoji); a page icon when it has none.
metaReact.ReactNode—What closes the row, small and muted (when it changed).

Data attributes and CSS variables on DocumentLayoutPageLink

AttributeValues
data-slot"document-layout-page-link"

DocumentLayoutHeader, DocumentLayoutTitleGroup, DocumentLayoutBody and DocumentLayoutFoot take a div's native props, DocumentLayoutTitle an h1's; DocumentLayoutContents and DocumentLayoutMargin take an aside's. What the contents hold sticks --document-layout-sticky-top (default --spacing(8)) below the top of the scroll container. The discussion rail sticks there too, as tall as the window less --document-layout-aside-offset (default --spacing(24): the bar above and the offsets). DocumentLayoutAsideTab is { value, label, count?, content }.

Accessibility

  • The contents and the margin are two aside landmarks, named "Contents" and "Comments" (override with aria-label). Below their tier they are display: none, so they leave the accessibility tree with the screen.
  • The aside is an aside landmark named by its title ("Comments", "On this page"); its tabs are a Base UI tab list named the same, and the × is a "Close" button. It is non-modal: focus stays where the reader puts it, and the page beside it keeps working. Below L the discussion is a modal bottom sheet with its own close.
  • Source order is reading order: the title area, the contents, the text, what follows it, then the comments — whatever the columns look like.
  • The skeleton is one status region named "Loading page" with aria-busy; its bars are hidden.
KeyAction
TabMove through the contents, the text's links, then the margin or aside
← / →Move between the aside's tabs
EscClose the discussion sheet (below L)
ContractStates tested
Behaviourtier-s, tier-m, tier-l, tier-xl, tier-2xl, wide, loading, aside-discussion, aside-outline, aside-sheet
Accessibilitylabeled, browser-accessibility-test
Visualdefault, loading

Do / Don't

Do
Set margin only while the margin holds threads or a draft, decided with the text (a server thread count) so the first paint is right, and render the skeleton with the same margin and measure as the page.
Don't
Give the text column its own max-width or padding — the column and the tiers are the layout's, and a second width fights the container queries.

On this page