ComponentPorchlight CSS

Filter builder

CSS-only query builder, query chips, and saved view bar for SaaS data surfaces.

51 components9 patterns46 stable5 experimental

Command

Search Porchlight

Filter builder

.pl-c-filter-builder styles query rows and groups for data-heavy SaaS screens. .pl-c-query-bar, .pl-c-query-chip, and .pl-c-saved-views cover the companion saved-view and active-filter surfaces. Porchlight does not define a query grammar, serialize filters, run searches, or persist views.

Query builder HTML

<form class="pl-c-filter-builder" aria-labelledby="filter-title">
  <header class="pl-c-filter-builder__header">
    <div>
      <h2 class="pl-c-filter-builder__title" id="filter-title">
        Filter requests
      </h2>
      <p class="pl-c-filter-builder__description">Build an AND/OR query.</p>
    </div>
    <div class="pl-c-filter-builder__actions">
      <button class="pl-c-button" data-variant="secondary" type="button">
        Add group
      </button>
      <button class="pl-c-button" data-variant="primary" type="submit">
        Apply
      </button>
    </div>
  </header>

  <div class="pl-c-filter-builder__body">
    <section
      class="pl-c-filter-builder__group"
      data-operator="and"
      aria-labelledby="group-risk"
    >
      <header class="pl-c-filter-builder__group-header">
        <h3 class="pl-c-filter-builder__group-title" id="group-risk">
          <span class="pl-c-filter-builder__joiner">AND</span>
          Risk filters
        </h3>
      </header>

      <div class="pl-c-filter-builder__rows">
        <div class="pl-c-filter-builder__row">
          <div class="pl-c-filter-builder__field">
            <label class="pl-u-sr-only" for="field-1">Field</label>
            <select
              class="pl-c-field__control pl-c-filter-builder__control"
              id="field-1"
            >
              <option>Status</option>
            </select>
          </div>
          <div class="pl-c-filter-builder__operator">
            <label class="pl-u-sr-only" for="operator-1">Operator</label>
            <select
              class="pl-c-field__control pl-c-filter-builder__control"
              id="operator-1"
            >
              <option>is</option>
            </select>
          </div>
          <div class="pl-c-filter-builder__value">
            <label class="pl-u-sr-only" for="value-1">Value</label>
            <input
              class="pl-c-field__control pl-c-filter-builder__control"
              id="value-1"
              value="Blocked"
            />
          </div>
          <button
            class="pl-c-button pl-c-filter-builder__remove"
            data-variant="ghost"
            type="button"
            aria-label="Remove filter"
          >
            x
          </button>
        </div>
      </div>
    </section>
  </div>
</form>

Active filters and saved views

<nav class="pl-c-saved-views" aria-label="Saved views">
  <span class="pl-c-saved-views__label">Views</span>
  <ul class="pl-c-saved-views__list">
    <li class="pl-c-saved-views__item">
      <button class="pl-c-saved-views__button" aria-current="true">
        My queue <span class="pl-c-saved-views__count">18</span>
      </button>
    </li>
  </ul>
</nav>

<div class="pl-c-query-bar" role="search" aria-label="Active request filters">
  <span class="pl-c-query-bar__label">Filters</span>
  <div class="pl-c-query-bar__chips">
    <span class="pl-c-query-chip">
      <span class="pl-c-query-chip__field">Status</span>
      <span class="pl-c-query-chip__operator">is</span>
      <span class="pl-c-query-chip__value">Blocked</span>
      <button class="pl-c-query-chip__remove" aria-label="Remove status filter">
        x
      </button>
    </span>
  </div>
</div>

State hooks

Selector or attribute Purpose
.pl-c-filter-builder[data-pl-density="compact"], [data-pl-density="dense"] Tighter builder spacing.
.pl-c-filter-builder__group[data-operator="and"] AND group metadata hook.
.pl-c-filter-builder__group[data-operator="or"] OR group metadata hook with accent border.
.pl-c-filter-builder__row[aria-invalid="true"], [data-invalid] Invalid query row.
.pl-c-filter-builder__row[aria-disabled="true"], [data-disabled] Disabled query row.
.pl-c-query-chip[aria-invalid="true"], [data-invalid] Invalid active-filter chip.
.pl-c-saved-views__button[aria-current="true"], [aria-pressed="true"], [data-active] Current saved view.

Legacy .is-invalid and .is-disabled aliases still render for pre-namespace markup, but new integrations should prefer ARIA or data-* hooks.

Accessibility responsibilities

Use real form controls with labels, even when labels are visually hidden. Keep invalid rows connected to error text with aria-describedby, and set aria-invalid="true" on the row or the individual control. Saved views should be buttons or links depending on whether they mutate state or navigate. When a filter changes result counts, announce the new count in an app-owned live region.

SSR, hypermedia, and frameworks

The contract is plain rendered HTML. SSR can render an applied query and saved views from server state. Hypermedia apps can replace one group, one row, or the result count without changing CSS. Component frameworks can map their query objects to the same classes and attributes without framework-specific selectors.

Preview

See the filter builder preview.