Skip to content

Layouts

@layout is for CSSOM-first composition docs: define the structure in CSS, then derive valid HTML from that model.

This guide shows a practical flow:

  1. Define reusable components in CSS docs.
  2. Define one layout record with @structure.
  3. Derive an HTML scaffold that matches the documented structure.

If you want full API details, use the TypeDoc reference. This guide stays focused on author workflow.

Step 1: Define component records in CSS

Start with reusable pieces, not markup.

css
/**
 * @component site-header
 * @summary Global page header with brand and primary navigation.
 * @part .brand - Brand link.
 */
.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.brand {
  font-weight: 700;
  text-decoration: none;
}

/**
 * @component top-nav
 * @summary Horizontal link list used in page-level navigation.
 * @part .top-nav__link - One navigation link.
 */
.top-nav {
  display: flex;
  gap: 0.75rem;
}
.top-nav__link {
  text-decoration: none;
}

/**
 * @component filter-chip
 * @summary Compact action for toggling list filters.
 * @modifier -active - Highlights the chip when selected.
 */
.filter-chip {
  border: 1px solid currentcolor;
}
.filter-chip.-active {
  font-weight: 600;
}

Step 2: Define the layout record

You can author structure in two ways. Use explicit when you want full control over the docs tree. Use implicit when the layout CSS itself already reflects the same single-root shape.

css
/**
 * @layout app-shell
 * @summary Base application frame with header, content region, and footer.
 */
.app-shell {
  min-height: 100dvh;
  display: grid;
  grid-template-rows: auto 1fr auto;

  @component site-header (--desktop-header) {
  }
  @component top-nav (--desktop-nav) {
  }

  .content-wrap {
    .side-rail {
      @component filter-chip (--desktop-filters) {
      }
    }
    .content-panel {
    }
  }

  .site-footer {
  }
}
css
/**
 * @layout app-shell
 * @summary Base application frame with header, content region, and footer.
 * @structure
 * .app-shell {
 *   @component site-header
 *   .content-wrap {
 *     .side-rail {
 *       @component filter-chip
 *     }
 *     .content-panel {}
 *   }
 *   .site-footer {}
 * }
 */
.app-shell {
  min-height: 100dvh;
  display: grid;
  grid-template-rows: auto 1fr auto;
}

.content-wrap {
  display: grid;
  grid-template-columns: 18rem 1fr;
}

With implicit mode, cssdoc infers the structure tree from the layout record's CSS nodes. If inference would produce multiple roots, cssdoc drops the inferred tree so docs remain deterministic. In that case, switch to explicit @structure.

At this point, CSS is the source of truth.

  • Component records define reusable pieces.
  • The layout record defines how those pieces compose.

Co-located components

@component inside a structure node means containment — the named component is a child of that slot. Sometimes the slot element itself carries the component class directly. Use a single-selector :is() pseudo to express this:

css
/**
 * @layout tabs-layout
 * @summary A tabs layout.
 * @structure
 * .tabs-layout {
 *   .tabs-list:optional:is(.instui-card) {}
 * }
 */
.tabs-layout {
}

cssdoc reads .tabs-list:optional:is(.instui-card) as: "this slot element carries .instui-card itself." The scaffold it derives is therefore:

html
<nav class="tabs-list instui-card">…</nav>

…not:

html
<nav class="tabs-list"><div class="instui-card">…</div></nav>

:is() accepts any valid single selector — a class, an element type, an ID, or an attribute selector:

css
* @structure
* .panel {
*   .header:is(.instui-view-header) {}
*   button:one-or-more:is(.instui-button) {}
*   nav:optional:is(#primary-nav) {}
* }

When the :is() argument resolves to a known component, cssdoc cross-links it in the Subcomponents section the same way a nested @component reference does.

Cardinality and nested nodes work the same way. A co-located child that's inside an optional parent is only required when the parent is present:

css
* @structure
* .utilities:optional {
*   button:one-or-more:is(.instui-button) {}
* }

Reads as: if .utilities is present, it must contain one or more button.instui-button elements.

Using :is() with multiple selectors (e.g. :is(.a, .b)) is not co-location — cssdoc keeps the compound verbatim and treats it as a relationship selector, just as :has() and :not() are.

Choosing between sub-component types

Sometimes a slot holds one of several sibling components rather than a single fixed one — a table's header row holds column headers, while its body rows hold row headers and cells. Write this as an ordinary CSS selector list (comma-separated), the same way you'd write :is(.a, .b) — no new tag or syntax:

css
/**
 * @component table
 * @summary A data table.
 * @structure
 * .table {
 *   .table-head {
 *     .table-row {
 *       .table-col-header:one-or-more {}
 *     }
 *   }
 *   .table-body {
 *     .table-row:one-or-more {
 *       .table-cell, .table-row-header {}
 *     }
 *   }
 * }
 */
.table {
}

Every class named in the list is scanned and cross-linked independently — both table-cell and table-row-header show up in table's Subcomponents section, and the rendered text tree joins every resolved sibling with | (table-cell | table-row-header (component, 1..n)). Since a comma-joined selector is one CSS rule, a shared cardinality marker goes once, at the end of the list (.table-cell, .table-row-header:one-or-more) — it applies to the whole alternation, not just the last name.

Step 3: Derive valid HTML from the documented structure

Once records are defined, scaffold markup that matches the structure tree.

html
<!doctype html>
<html lang="en">
  <body>
    <div class="app-shell">
      <header class="site-header">
        <a class="brand" href="/">Acme</a>
      </header>

      <main class="content-wrap">
        <aside class="side-rail">
          <button class="filter-chip">Open</button>
        </aside>
        <section class="content-panel">...</section>
      </main>

      <footer class="site-footer">...</footer>
    </div>
  </body>
</html>

Because the scaffold comes from @structure, docs and markup stay aligned.

Step 4: Add profiles when composition changes by breakpoint

If composition changes between breakpoints, profile the layout reference.

css
/**
 * @component top-nav
 * @summary Primary navigation.
 */
.top-nav {
}

/**
 * @component filter-chip
 * @summary Filter action chip.
 */
.filter-chip {
}

/**
 * @layout app-shell
 * @structure
 * .app-shell {
 *   @component top-nav (--desktop-nav) {}
 *   @component filter-chip (--desktop-filters) {}
 * }
 */
.app-shell {
}

Then choose one of these paths:

  1. Declare profiles directly with @custom-media.
  2. Or generate them with compileCustomMediaDeclarations.
css
@custom-media --desktop-nav (width >= 64rem);
@custom-media --desktop-filters (width >= 64rem);
ts
import { compileCustomMediaDeclarations } from "@cssdoc/core";

const declarations = compileCustomMediaDeclarations(css, {
  resolveValue: (profile) => {
    if (profile === "--desktop-nav") return "(width >= 64rem)";
    if (profile === "--desktop-filters") return "(width >= 64rem)";
    return true;
  },
});

Output:

css
@custom-media --desktop-filters (width >= 64rem);
@custom-media --desktop-nav (width >= 64rem);

When inline @custom-media declarations already exist in your CSS, compileCustomMediaDeclarations finds and absorbs those values for matching profiles, so output stays aligned and deduped.

CSSOM-first workflow

  1. Start from component and layout CSS.
  2. Document reusable blocks as @component records.
  3. Add one @layout record per page shell.
  4. Derive HTML scaffold from the @structure tree.
  5. Add profiles only when composition changes.

Common issues

  • structure-unknown-record: A @structure reference points to a name that has no record yet.
  • structure-ambiguous-record: An untyped reference matches more than one record kind.
  • structure-unknown-selector: A structure selector does not map to a documented member.

If you intentionally reference external classes, add them to structureIgnore in cssdoc.json.

Released under the MIT License