ComponentPorchlight CSS

Tree view

Role-first tree view for object explorers, nested navigation, process folders, rule libraries, and app sidebars.

51 components9 patterns46 stable5 experimental

Command

Search Porchlight

Tree view

The .pl-c-tree component styles rendered tree markup for enterprise app navigation: process folders, case files, rule sets, document libraries, tenant hierarchies, and workflow builders. It is CSS-only and framework-neutral. Your app owns focus movement, selection, lazy loading, drag and drop, and persistence.

Use the ARIA tree roles as the contract: role="tree" on the container, role="treeitem" on each item, and role="group" for nested children. State is expressed with attributes such as aria-expanded, aria-selected, aria-current, aria-disabled, and aria-busy.

Semantic HTML

<div class="pl-c-tree" role="tree" aria-label="Process library">
  <div
    class="pl-c-tree__item"
    role="treeitem"
    aria-expanded="true"
    tabindex="0"
  >
    <div class="pl-c-tree__item-row">
      <span class="pl-c-tree__expander" aria-hidden="true">
        <svg viewBox="0 0 24 24"><path d="m9 18 6-6-6-6" /></svg>
      </span>
      <span class="pl-c-tree__icon" aria-hidden="true">...</span>
      <span class="pl-c-tree__label">Approvals</span>
      <span class="pl-c-tree__badge">12</span>
    </div>

    <div class="pl-c-tree__group" role="group">
      <div
        class="pl-c-tree__item"
        role="treeitem"
        aria-selected="true"
        tabindex="-1"
      >
        <div class="pl-c-tree__item-row">
          <span class="pl-c-tree__expander" aria-hidden="true"></span>
          <span class="pl-c-tree__icon" aria-hidden="true">...</span>
          <span class="pl-c-tree__label">Manager approval</span>
        </div>
      </div>
    </div>
  </div>
</div>

State Attributes

<div class="pl-c-tree__item" role="treeitem" aria-expanded="false">
  <div class="pl-c-tree__item-row">...</div>
  <div class="pl-c-tree__group" role="group">...</div>
</div>

<div class="pl-c-tree__item" role="treeitem" aria-selected="true">
  <div class="pl-c-tree__item-row">Selected item</div>
</div>

<div class="pl-c-tree__item" role="treeitem" aria-current="page">
  <div class="pl-c-tree__item-row">Current destination</div>
</div>

<div class="pl-c-tree__item" role="treeitem" aria-disabled="true">
  <div class="pl-c-tree__item-row">Locked folder</div>
</div>

<div class="pl-c-tree__item" role="treeitem" aria-busy="true">
  <div class="pl-c-tree__item-row">Loading children</div>
</div>

aria-expanded="false" hides the direct child .pl-c-tree__group. Omit aria-expanded on leaf nodes so the expander slot is reserved but hidden. Use aria-current when the item represents the current page or route. Use aria-selected when the item is selected inside a tree widget.

Tree items are explicit grid containers so whitespace produced by SSR, templates, or formatted JSX/HTML cannot create extra anonymous line boxes between rows. Only .pl-c-tree__item-row and .pl-c-tree__group participate in the vertical rhythm.

Actions and Metadata

<div class="pl-c-tree" role="tree" data-actions="persistent">
  <div class="pl-c-tree__item" role="treeitem" tabindex="0">
    <div class="pl-c-tree__item-row">
      <span class="pl-c-tree__expander" aria-hidden="true"></span>
      <span class="pl-c-tree__label">Eligibility rules</span>
      <span class="pl-c-tree__meta">4 changed</span>
      <span class="pl-c-tree__actions">
        <button class="pl-c-tree__action" type="button" aria-label="Add rule">
          ...
        </button>
        <button
          class="pl-c-tree__action"
          type="button"
          aria-label="More actions"
        >
          ...
        </button>
      </span>
    </div>
  </div>
</div>

Actions are optional. Keep destructive or editing actions as real buttons with durable accessible names. data-actions="persistent" keeps row actions visible; otherwise they appear on hover or focus.

Accessibility Responsibilities

Porchlight does not ship a tree controller. The app should implement the WAI-ARIA tree view interaction model that fits its product:

  • Keep exactly one tree item in the tab order when using roving focus.
  • Move focus with arrow keys, Home, End, and optional typeahead.
  • Toggle aria-expanded when folders open or close.
  • Keep aria-selected in sync with single-select or multi-select state.
  • Use aria-current only for current location, not ordinary selection.
  • Mark unavailable items with aria-disabled="true" and prevent activation.
  • Announce lazy loading with aria-busy="true" while children are fetched.

For simple sidebar navigation that only needs links and native disclosure, use .pl-c-nav instead. Use .pl-c-tree when the hierarchy behaves like a selectable widget or object explorer.

SSR and Framework Compatibility

The component is rendered HTML plus CSS. It works from server-rendered templates, hypermedia responses, Web Components, React, Vue, Svelte, Astro, Rails, Django, Phoenix, Laravel, or any other stack that can emit the class names and ARIA attributes. Porchlight does not require client-only rendering, hydration, custom elements, or framework selectors.

Class Contract

Selector Role
.pl-c-tree Tree container. Use with role="tree".
.pl-c-tree__item Tree item wrapper. Use with role="treeitem".
.pl-c-tree__item-row Visual row surface for one item.
.pl-c-tree__group Nested child group. Use with role="group".
.pl-c-tree__expander Leading disclosure icon slot.
.pl-c-tree__icon Optional item icon slot.
.pl-c-tree__label Truncated primary label.
.pl-c-tree__meta Quiet trailing metadata text.
.pl-c-tree__badge Numeric count or short status badge.
.pl-c-tree__actions Optional trailing action group.
.pl-c-tree__action Small icon button inside a row.
[aria-expanded="true"] Expanded branch, rotates expander.
[aria-expanded="false"] Collapsed branch, hides direct group.
[aria-selected="true"] Selected item.
[aria-current] Current route or current object.
[aria-disabled="true"] Disabled item.
[aria-busy="true"] Loading item.
[data-pl-density="compact"] Tighter row density.
[data-actions="persistent"] Always-visible row actions.

Tokens Exposed

Token Default Purpose
--pl-c-tree-row-min-block-size 2rem Minimum row height.
--pl-c-tree-row-gap var(--pl-space-2) Gap between row parts.
--pl-c-tree-row-padding-block var(--pl-space-1) Vertical row padding.
--pl-c-tree-row-padding-inline var(--pl-space-2) Horizontal row padding.
--pl-c-tree-indent 1.25rem Nested group indentation.
--pl-c-tree-icon-size 1rem Icon slot size.
--pl-c-tree-expander-size 1rem Expander slot size.
--pl-c-tree-branch-gap var(--pl-space-1) Gap between sibling rows.