Guide Porchlight CSS

Layout & Page Building

Use Porchlight's layout primitives, app shell, and container-query sidebar to compose responsive SaaS pages.

52 components 51 stable 1 experimental

Command

Search Porchlight

Layout & Page Building

Porchlight provides a collection of lightweight layout primitives instead of rigid page grids. They are designed to manage the regions around components, allowing you to compose full pages out of custom components and utilities.

All layout primitives live in @layer porchlight.layout and share a few core design principles:

  • Zero specificity: They are wrapped in :where() selectors, making them easy to override in your application CSS.
  • Token-driven spacing: Spacing gaps default to standard token values (e.g. var(--pl-space-4)).
  • Tunable variables: Each primitive exposes one or more --l-* CSS custom properties (like --l-stack-gap or --l-grid-min) so you can tune specific instances directly in HTML.
  • Container queries: Primitives like .l-sidebar adapt to the size of their containing panel rather than the browser window, ensuring responsiveness when nested inside complex dashboards.

Primitive Reference

1. Stack (.l-stack)

Use for vertical flow, such as form groups, card contents, settings sections, or stacked list items.

<div class="l-stack" style="--l-stack-gap: var(--pl-space-3);">
  <label class="c-field">...</label>
  <label class="c-field">...</label>
</div>
TokenDescriptionDefault
--l-stack-gapVertical gap between itemsvar(--pl-space-4)

2. Cluster (.l-cluster)

Use for horizontal wrapping groups where elements flow naturally. Ideal for toolbar controls, filter lists, chip groups, or buttons in an action row.

<div class="l-cluster" style="--l-cluster-justify: space-between;">
  <div>Status filters...</div>
  <button class="c-button">Action</button>
</div>
TokenDescriptionDefault
--l-cluster-gapSpacing between itemsvar(--pl-space-3)
--l-cluster-alignVertical alignment (align-items)center
--l-cluster-justifyHorizontal alignment (justify-content)flex-start

3. Grid (.l-grid)

An auto-fitting grid that wraps columns automatically when they fall below a minimum width. Perfect for dashboard KPI grids, search results, or card directories.

<div class="l-grid" style="--l-grid-min: 18rem;">
  <div class="c-card">Card A</div>
  <div class="c-card">Card B</div>
  <div class="c-card">Card C</div>
</div>
TokenDescriptionDefault
--l-grid-gapGrid gapvar(--pl-space-4)
--l-grid-minMinimum width before columns wrap16rem

4. Sidebar (.l-sidebar)

A two-column layout consisting of a fixed-width column (typically a sidebar navigation or filter panel) and a flexible main column. It collapses to a single column when the viewport or parent container is narrower than 48rem.

[!NOTE] To trigger container-query collapse, wrap .l-sidebar inside an element with container-type: inline-size, or nest it directly within .l-app-shell__main (which already defines a query container).

<div class="l-sidebar" style="--l-sidebar-size: 14rem;">
  <aside>Sidebar Nav</aside>
  <main>Main Content</main>
</div>
TokenDescriptionDefault
--l-sidebar-gapColumn gapvar(--pl-space-4)
--l-sidebar-sizeWidth of the sidebar column16rem

5. Scroll Area (.l-scroll-area)

An independent scroll container that contains overscroll to prevent scroll chaining to the main page. It uses scrollbar-gutter: stable to prevent layout shifts when scrollbars appear or disappear.

<div class="l-scroll-area" style="block-size: 20rem;">
  <!-- long content -->
</div>

6. Columns (.l-columns)

A CSS multi-column layout optimized for card walls, search listings, or image galleries. Unlike CSS grids, masonry columns allow elements of varying heights to stack cleanly without horizontal gaps.

<div class="l-columns" style="--l-columns-width: 16rem;">
  <div class="c-card">Tall item</div>
  <div class="c-card">Short item</div>
</div>
TokenDescriptionDefault
--l-columns-gapGap between columns and cardsvar(--pl-space-4)
--l-columns-widthTarget column width16rem
--l-columns-countMaximum column count (optional)auto

7. Container (.l-container)

A centered, responsive max-width wrapper designed for dashboards, list views, and settings forms.

<div class="l-container">
  <h1>Dashboard</h1>
  <!-- grid etc. -->
</div>
TokenDescriptionDefault
--l-container-maxMaximum inline width80rem
--l-container-padInline padding (gutter)var(--pl-space-4)

8. Inset (.l-inset)

A centered column designed to constrain content inside a wider, full-bleed element (such as forms inside a hero banner or captions in a video section).

<div class="bleed-section">
  <div class="l-inset">
    <h2>Constrained Form</h2>
  </div>
</div>
TokenDescriptionDefault
--l-inset-maxMaximum inline width48rem

9. Page (.l-page)

A centered max-width container optimized for readable prose and documentation content.

TokenDescriptionDefault
--l-page-maxMaximum inline width88rem

App Shell Layout

The .l-app-shell coordinates the main layout of a SaaS or dashboard application. It provides:

  • A sticky topbar (.l-app-shell__topbar).
  • A sidebar navigation rail (.l-app-shell__sidebar) supporting manual collapse toggles.
  • A scrolling main window (.l-app-shell__main) configured as a container query context.

Responsive Behavior

  • Desktop (60rem+): Sidebar and topbar are displayed side-by-side.
  • Tablet / Mobile (< 60rem): The sidebar is hidden (display: none), prioritizing the work canvas.
  • Sidebar Collapse: Setting data-sidebar="collapsed" on .l-app-shell__sidebar transitions its width from 16rem to 3.5rem, transforming it into an icon rail.
<div class="l-app-shell">
  <header class="l-app-shell__topbar">Topbar Content</header>
  <aside class="l-app-shell__sidebar" data-sidebar="collapsed">
    <!-- Icon Navigation -->
  </aside>
  <main class="l-app-shell__main">
    <div class="l-container">
      <!-- Page Content -->
    </div>
  </main>
</div>

Page-Building Examples

Example 1: SaaS Dashboard Page

A complete dashboard composed of a sticky topbar, sidebar rail, KPI grid, and a split layout for recent activity.

<div class="l-app-shell">
  <!-- Sticky Header -->
  <header
    class="l-app-shell__topbar c-card"
    style="border-radius: 0; border-inline: 0;"
  >
    <div
      class="l-cluster"
      style="--l-cluster-justify: space-between; padding: var(--pl-space-3);"
    >
      <strong>Logo</strong>
      <div class="l-cluster">
        <span>User Profile</span>
      </div>
    </div>
  </header>

  <!-- Sidebar Rail -->
  <aside
    class="l-app-shell__sidebar"
    style="background: var(--pl-color-surface-2); border-inline-end: 1px solid var(--pl-color-border);"
  >
    <nav
      class="l-stack"
      style="--l-stack-gap: var(--pl-space-1); padding: var(--pl-space-3);"
    >
      <a class="c-nav__item" href="#" aria-current="page">Dashboard</a>
      <a class="c-nav__item" href="#">Team</a>
      <a class="c-nav__item" href="#">Settings</a>
    </nav>
  </aside>

  <!-- Main Workspace -->
  <main class="l-app-shell__main" style="padding-block: var(--pl-space-6);">
    <div class="l-container l-stack" style="--l-stack-gap: var(--pl-space-6);">
      <!-- Top Section -->
      <div class="l-cluster" style="--l-cluster-justify: space-between;">
        <h1>Dashboard</h1>
        <button class="c-button" data-variant="primary">Create Project</button>
      </div>

      <!-- KPI Grid -->
      <div class="l-grid" style="--l-grid-min: 16rem;">
        <div class="c-card">
          <div class="c-card__body">Active Seats: 1,280</div>
        </div>
        <div class="c-card">
          <div class="c-card__body">MRR: $48.2k</div>
        </div>
        <div class="c-card">
          <div class="c-card__body">Open Tickets: 14</div>
        </div>
      </div>

      <!-- Main Columns -->
      <div class="l-sidebar" style="--l-sidebar-size: 20rem;">
        <!-- Right side (Sidebar column) -->
        <div class="c-card">
          <div class="c-card__header">
            <h3 class="c-card__title">Activity Feed</h3>
          </div>
          <div
            class="c-card__body l-stack"
            style="--l-stack-gap: var(--pl-space-3);"
          >
            <div>User created task A</div>
            <div>User merged branch B</div>
          </div>
        </div>

        <!-- Left side (Main column) -->
        <div class="c-card">
          <div class="c-card__header">
            <h3 class="c-card__title">Active Issues</h3>
          </div>
          <div class="c-card__body">
            <!-- Table component -->
          </div>
        </div>
      </div>
    </div>
  </main>
</div>

Example 2: Settings Form

A classic setting view showing a side menu and stacked inputs that adapt when screen width decreases.

<div class="l-container" style="padding-block: var(--pl-space-6);">
  <div class="l-sidebar" style="--l-sidebar-size: 16rem;">
    <!-- Local Sub-navigation -->
    <nav class="l-stack" style="--l-stack-gap: var(--pl-space-1);">
      <a class="c-nav__item" href="#" aria-current="page">General Settings</a>
      <a class="c-nav__item" href="#">Security</a>
      <a class="c-nav__item" href="#">Billing</a>
    </nav>

    <!-- Settings Content -->
    <form class="l-stack c-card" style="--l-stack-gap: var(--pl-space-6);">
      <div class="c-card__header">
        <h2 class="c-card__title">Profile Settings</h2>
      </div>

      <div
        class="c-card__body l-stack"
        style="--l-stack-gap: var(--pl-space-4);"
      >
        <label class="c-field">
          <span class="c-field__label">Full Name</span>
          <input
            class="c-field__control"
            type="text"
            value="Jane Doe"
            required
          />
        </label>

        <label class="c-field">
          <span class="c-field__label">Email Address</span>
          <input
            class="c-field__control"
            type="email"
            value="jane@example.com"
            required
          />
        </label>
      </div>

      <div
        class="c-card__footer l-cluster"
        style="--l-cluster-justify: flex-end;"
      >
        <button class="c-button" type="button">Cancel</button>
        <button class="c-button" data-variant="primary" type="submit">
          Save
        </button>
      </div>
    </form>
  </div>
</div>