ComponentPorchlight CSS

Form

Layout helpers for SaaS forms, grids, sections, actions, choice lists, choice messages, and input groups.

51 components9 patterns46 stable5 experimental

Command

Search Porchlight

Form

The .pl-c-form kit composes native fields into real application forms: settings sections, billing forms, filter bars, modal create/edit flows, dense admin metadata, choice groups, and prefix/suffix input groups. It is CSS-only and keeps native form controls in the DOM.

Porchlight is frontend agnostic. Plain HTML forms, HATEOAS/server-rendered fragments, htmx-style swaps, Alpine-style state, Vue, React, and heavier client frameworks all use the same contract: native controls, semantic form elements, HTML5 constraints, aria-invalid, aria-describedby, and optional data-tone message attributes.

Form Layout

<form class="pl-c-form">
  <section class="pl-c-form__section">
    <div class="pl-c-form__header">
      <h2>Workspace</h2>
      <p>Names, URLs, and defaults used across the account.</p>
    </div>
    <div class="pl-c-form__body">
      <div class="pl-c-form__grid">
        <label class="pl-c-field">
          <span class="pl-c-field__label">Workspace name</span>
          <input class="pl-c-field__control" value="Acme Ops" />
        </label>
        <label class="pl-c-field">
          <span class="pl-c-field__label">Timezone</span>
          <select class="pl-c-field__control">
            <option>America/Chicago</option>
          </select>
        </label>
      </div>
    </div>
  </section>
  <div class="pl-c-form__actions">
    <button class="pl-c-button" type="button" data-variant="ghost">
      Cancel
    </button>
    <button class="pl-c-button" type="submit" data-variant="primary">
      Save
    </button>
  </div>
</form>

Class Contract

Selector Role
.pl-c-form Form root with vertical section rhythm.
.pl-c-form__section A logical form section.
.pl-c-form__header Section heading and summary copy.
.pl-c-form__body Field stack inside a section.
.pl-c-form__grid Responsive field grid.
.pl-c-form__row Wrapping row for filters and short fields.
.pl-c-form__actions Submit/cancel action row.
.pl-c-choice-group Fieldset wrapper for checkbox/radio choices.
.pl-c-choice-group__hint Helper, warning, success, or error text for choices.
.pl-c-choice-list Stacked or inline list of choices.
.pl-c-choice Native checkbox/radio label row.
.pl-c-choice__input Native checkbox/radio input.
.pl-c-choice__label Primary choice text.
.pl-c-choice__description Optional secondary choice text.
.pl-c-input-group Prefix/suffix/action wrapper around a native field.
.pl-c-input-group__addon Non-interactive prefix or suffix.
.pl-c-input-group__action Button aligned inside the group.

Choice Groups

Use real fieldset and legend for groups of related checkboxes or radios.

<fieldset class="pl-c-choice-group">
  <legend class="pl-c-choice-group__legend">Notifications</legend>
  <div class="pl-c-choice-list">
    <label class="pl-c-choice">
      <input class="pl-c-choice__input" type="checkbox" checked />
      <span>
        <span class="pl-c-choice__label">Weekly summary</span>
        <span class="pl-c-choice__description">Send every Monday morning.</span>
      </span>
    </label>
  </div>
  <span class="pl-c-choice-group__hint" data-tone="warning">
    Security alerts stay enabled for account owners.
  </span>
</fieldset>

Set data-layout="inline" on .pl-c-choice-list for compact radio groups such as billing cadence or plan type.

Input Groups

Input groups keep a native .pl-c-field__control while adding a prefix, suffix, or action. Use explicit labels when an action button sits beside the input.

<div class="pl-c-field">
  <label class="pl-c-field__label" for="seat-count">Seats</label>
  <div class="pl-c-input-group">
    <input
      id="seat-count"
      class="pl-c-field__control"
      type="number"
      value="24"
    />
    <span class="pl-c-input-group__addon">users</span>
  </div>
</div>

Framework-agnostic rendering

Porchlight has no runtime dependency on any rendering layer. Use servers, hypermedia libraries, reactive attribute bindings, or component frameworks to emit standard HTML. The CSS never selects on hx-*, x-*, v-*, React data attributes, or framework-owned classes.

Server or HATEOAS fragment

<form class="pl-c-form" action="/settings" method="post">
  <div class="pl-c-field">
    <label class="pl-c-field__label" for="team-slug">Team slug</label>
    <input
      id="team-slug"
      class="pl-c-field__control"
      name="slug"
      required
      pattern="[a-z0-9-]+"
      aria-invalid="true"
      aria-describedby="team-slug-error"
    />
    <span class="pl-c-field__hint" id="team-slug-error" role="alert">
      Use lowercase letters, numbers, and hyphens.
    </span>
  </div>
</form>

The server can return the same form or field fragment with updated aria-invalid, aria-describedby, values, and messages. A hypermedia library such as htmx can perform that swap, but Porchlight only sees the resulting HTML.

Reactive attributes

<div x-data="{ error: '' }" class="pl-c-field">
  <label class="pl-c-field__label" for="budget-limit">Budget limit</label>
  <input
    id="budget-limit"
    class="pl-c-field__control"
    type="number"
    :aria-invalid="error ? 'true' : 'false'"
    aria-describedby="budget-limit-error"
  />
  <span class="pl-c-field__hint" id="budget-limit-error" x-text="error"></span>
</div>

The Alpine-style example above toggles standard attributes. Vue, React, and other component systems should render the same final DOM rather than needing a Porchlight adapter:

<div class="pl-c-field">
  <label class="pl-c-field__label" for="quota">Quota</label>
  <input
    id="quota"
    class="pl-c-field__control"
    name="quota"
    type="number"
    aria-invalid="false"
    aria-describedby="quota-help"
  />
  <span class="pl-c-field__hint" id="quota-help" data-tone="success">
    Current quota is valid.
  </span>
</div>

Tokens

Token Default Purpose
--pl-c-form-gap --pl-space-5 Space between form sections.
--pl-c-form-section-gap --pl-space-4 Space inside each section.
--pl-c-form-grid-min 16rem Minimum column size before wrapping.
--pl-c-form-actions-gap --pl-space-2 Gap between action buttons.
--pl-c-choice-gap --pl-space-2 Gap between choices.
--pl-c-input-group-addon-size --pl-control-block-size Minimum prefix/suffix width.

Accessibility

  • Use real form, fieldset, legend, and label elements.
  • Keep labels visible unless the surrounding UI already names the control; if hidden, use .pl-u-sr-only.
  • Connect hints or error text with aria-describedby when the message is needed by assistive technology.
  • Use aria-invalid="true" for server-side errors or framework-managed errors; native HTML5 validation continues to use :user-invalid.
  • Use .pl-c-choice-group__hint with aria-describedby on the fieldset for grouped checkbox/radio help, warnings, or errors.
  • Do not put buttons inside wrapper labels. Use for/id for input groups with actions.