Component Porchlight CSS

Form

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

52 components 51 stable 1 experimental

Command

Search Porchlight

Form

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

Class Contract

SelectorRole
.c-formForm root with vertical section rhythm.
.c-form__sectionA logical form section.
.c-form__headerSection heading and summary copy.
.c-form__bodyField stack inside a section.
.c-form__gridResponsive field grid.
.c-form__rowWrapping row for filters and short fields.
.c-form__actionsSubmit/cancel action row.
.c-choice-groupFieldset wrapper for checkbox/radio choices.
.c-choice-group__hintHelper, warning, success, or error text for choices.
.c-choice-listStacked or inline list of choices.
.c-choiceNative checkbox/radio label row.
.c-choice__inputNative checkbox/radio input.
.c-choice__labelPrimary choice text.
.c-choice__descriptionOptional secondary choice text.
.c-input-groupPrefix/suffix/action wrapper around a native field.
.c-input-group__addonNon-interactive prefix or suffix.
.c-input-group__actionButton aligned inside the group.

Choice Groups

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

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

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

Input Groups

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

<div class="c-field">
  <label class="c-field__label" for="seat-count">Seats</label>
  <div class="c-input-group">
    <input id="seat-count" class="c-field__control" type="number" value="24" />
    <span class="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="c-form" action="/settings" method="post">
  <div class="c-field">
    <label class="c-field__label" for="team-slug">Team slug</label>
    <input
      id="team-slug"
      class="c-field__control"
      name="slug"
      required
      pattern="[a-z0-9-]+"
      aria-invalid="true"
      aria-describedby="team-slug-error"
    />
    <span class="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="c-field">
  <label class="c-field__label" for="budget-limit">Budget limit</label>
  <input
    id="budget-limit"
    class="c-field__control"
    type="number"
    :aria-invalid="error ? 'true' : 'false'"
    aria-describedby="budget-limit-error"
  />
  <span class="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="c-field">
  <label class="c-field__label" for="quota">Quota</label>
  <input
    id="quota"
    class="c-field__control"
    name="quota"
    type="number"
    aria-invalid="false"
    aria-describedby="quota-help"
  />
  <span class="c-field__hint" id="quota-help" data-tone="success">
    Current quota is valid.
  </span>
</div>

Tokens

TokenDefaultPurpose
--c-form-gap--pl-space-5Space between form sections.
--c-form-section-gap--pl-space-4Space inside each section.
--c-form-grid-min16remMinimum column size before wrapping.
--c-form-actions-gap--pl-space-2Gap between action buttons.
--c-choice-gap--pl-space-2Gap between choices.
--c-input-group-addon-size--pl-control-block-sizeMinimum 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 .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 .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.