ComponentPorchlight CSS

Field

A labeled native form control with stacked and inline layouts, required markers, messages, validation, and input-group composition.

51 components9 patterns46 stable5 experimental

Command

Search Porchlight

Field

The .pl-c-field is a labeled form control. It pairs a visible label with a native <input>, <select>, or <textarea> so you inherit constraint validation, autofill, IME, spellcheck, and platform conventions.

State is driven by native pseudos and ARIA, then reflected onto the control and messages - no Porchlight runtime, no class toggles, no framework adapter. Use the wrapper <label> form for simple fields; use for/id when the field contains grouped chrome or an action button.

.porchlight.app
Input groups preserve native input behavior.

Semantic HTML

<label class="pl-c-field">
  <span class="pl-c-field__label">
    Workspace name
    <span class="pl-c-field__required" aria-hidden="true">*</span>
  </span>
  <input
    class="pl-c-field__control"
    required
    placeholder="Acme Ops"
    aria-describedby="workspace-name-help"
  />
  <span class="pl-c-field__hint" id="workspace-name-help"
    >Use a name your team recognizes.</span
  >
</label>

Use an explicit label when the field contains an input group:

<div class="pl-c-field">
  <label class="pl-c-field__label" for="workspace-slug">Workspace URL</label>
  <div class="pl-c-input-group">
    <input
      id="workspace-slug"
      class="pl-c-field__control"
      placeholder="acme-ops"
    />
    <span class="pl-c-input-group__addon">.porchlight.app</span>
  </div>
  <span class="pl-c-field__hint"
    >Use lowercase letters, numbers, and hyphens.</span
  >
</div>

Class contract

Selector Role
.pl-c-field Field wrapper: usually a <label>, or a container with for/id.
.pl-c-field--inline Two-column label/control layout that stacks on narrow viewports.
.pl-c-field__label The visible label text.
.pl-c-field__control The native input/select/textarea.
.pl-c-field__control--select Opt-in themed native picker; add alongside the control class.
.pl-c-field__required Optional visual required marker inside the label.
.pl-c-field__hint Helper, warning, success, or error text below the control.
.pl-c-field__messages Optional stack for multiple .pl-c-field__hint messages.

Common Patterns

Stacked field

Use the default stacked layout for most create/edit forms, settings pages, and dialog forms.

<label class="pl-c-field">
  <span class="pl-c-field__label">Billing email</span>
  <input
    class="pl-c-field__control"
    type="email"
    placeholder="ops@example.com"
  />
  <span class="pl-c-field__hint">Invoices and receipts are sent here.</span>
</label>

Inline field

Use .pl-c-field--inline when labels should scan in a vertical gutter, such as admin metadata forms or dense settings panels.

<label class="pl-c-field pl-c-field--inline">
  <span class="pl-c-field__label">Region</span>
  <select class="pl-c-field__control">
    <option>US Central</option>
    <option>EU West</option>
  </select>
  <span class="pl-c-field__hint">Controls default data residency.</span>
</label>

Customizable native select

Add .pl-c-field__control--select alongside .pl-c-field__control to opt into Porchlight’s themed picker, options, groups, and selected checkmark. It uses the same field colors, sizing, density, focus, disabled and validation states. No script, replacement button, or custom ARIA roles are needed.

<label class="pl-c-field">
  <span class="pl-c-field__label">Priority</span>
  <select
    class="pl-c-field__control pl-c-field__control--select"
    name="priority"
    required
  >
    <option value="">Choose a priority</option>
    <optgroup label="Active queue">
      <option value="normal">Normal</option>
      <option value="urgent">Urgent</option>
      <option value="critical" disabled>Critical (restricted)</option>
    </optgroup>
  </select>
</label>

Keep explicit option values and text labels. Selection, keyboard navigation, change events, form submission/reset and required validation stay native. aria-invalid describes server errors; it does not change native validity. Selects do not support readonly; use disabled only when omission from form submission is intended.

The enhancement requires appearance: base-select and ::picker(select) (Chrome 135+); unsupported browsers retain the ordinary field select. Only single selects with no size attribute or size="1" opt in. Keep multiple and listbox-sized selects native; use the combobox for application-managed search or asynchronous options.

Mobile tradeoff: the enhanced picker renders inside the browser viewport instead of opening the operating system picker. Remove the modifier to retain the platform picker. Long option labels wrap; the popup scrolls within a bounded height. Test your option lengths and application overrides at narrow widths.

Search boxes in toolbars still need a label. Keep it visually hidden and use the native type="search".

<label class="pl-c-field pl-c-field--inline">
  <span class="pl-u-sr-only">Search accounts</span>
  <input
    class="pl-c-field__control"
    type="search"
    placeholder="Search accounts..."
  />
</label>

Standalone control

Use .pl-c-field__control without a .pl-c-field wrapper when another component owns the layout, as in command dialogs or toolbar searches. Keep an accessible label. The control still receives theme tokens, density, validation, disabled styling, and keyboard focus; use the wrapper for label/hint layout.

<label>
  <span class="pl-u-sr-only">Search docs</span>
  <input
    class="pl-c-field__control"
    type="search"
    placeholder="Search docs..."
  />
</label>

HTML5 validation

Use native constraints first: required, type, minlength, pattern, min, max, and friends. Porchlight styles :user-invalid, so fields do not show as invalid until the browser considers the user to have interacted.

<label class="pl-c-field">
  <span class="pl-c-field__label">Workspace URL</span>
  <input
    class="pl-c-field__control"
    type="url"
    required
    placeholder="https://"
  />
  <span class="pl-c-field__hint">Must be a valid URL.</span>
</label>

Required marker

Use .pl-c-field__required for a visible marker when the product needs one. The native required attribute stays the semantic source of truth.

<label class="pl-c-field">
  <span class="pl-c-field__label">
    Workspace name
    <span class="pl-c-field__required" aria-hidden="true">*</span>
  </span>
  <input class="pl-c-field__control" required />
</label>

Message tones

Use .pl-c-field__hint for one message, or .pl-c-field__messages when helper, warning, success, and error messages need to stack. Add data-tone="warning", data-tone="danger", or data-tone="success" for an explicit visual tone.

<div class="pl-c-field">
  <label class="pl-c-field__label" for="workspace-slug">Workspace slug</label>
  <input
    id="workspace-slug"
    class="pl-c-field__control"
    required
    aria-invalid="true"
    aria-describedby="workspace-slug-help workspace-slug-error"
  />
  <span class="pl-c-field__messages">
    <span class="pl-c-field__hint" id="workspace-slug-help"
      >Use lowercase letters, numbers, and hyphens.</span
    >
    <span
      class="pl-c-field__hint"
      id="workspace-slug-error"
      data-tone="danger"
      role="alert"
      >Spaces are not allowed.</span
    >
  </span>
</div>

Server or framework validation

For errors returned by a server, hypermedia swap, reactive state, or component framework, set aria-invalid="true" on the native control and connect the message with aria-describedby. Porchlight treats that state the same as :user-invalid. Set aria-invalid="false" or remove the attribute for a valid server state.

<div class="pl-c-field">
  <label class="pl-c-field__label" for="invite-email">Invite email</label>
  <input
    id="invite-email"
    class="pl-c-field__control"
    type="email"
    value="already-used@example.com"
    aria-invalid="true"
    aria-describedby="invite-email-error"
  />
  <span class="pl-c-field__hint" id="invite-email-error" role="alert">
    That email is already invited.
  </span>
</div>

Framework-agnostic contract

Porchlight does not care which tool rendered the field. Plain HTML, server-rendered HATEOAS fragments, htmx-style swaps, Alpine-style attribute bindings, Vue, React, and heavier app frameworks should all emit the same native HTML contract:

<div class="pl-c-field">
  <label class="pl-c-field__label" for="email">Email</label>
  <input
    id="email"
    class="pl-c-field__control"
    name="email"
    type="email"
    required
    aria-invalid="true"
    aria-describedby="email-error"
  />
  <span
    class="pl-c-field__hint"
    id="email-error"
    data-tone="danger"
    role="alert"
  >
    Enter a valid email address.
  </span>
</div>

Framework-specific attributes may exist in application markup, but Porchlight selectors do not depend on them. Dynamic applications own validation timing and DOM updates; Porchlight only styles the resulting semantic HTML.

Tokens consumed

--pl-color-{border,surface,text,text-muted,danger,danger-text,success-text,warning-text}, --pl-focus-color, --pl-focus-size, --pl-control-{block-size,padding-inline,radius,border-width}, --pl-duration-1, --pl-ease-standard.

Tokens exposed (component-local)

Token Default Purpose
--pl-c-field-border --pl-color-border Control border; overridden on focus/invalid.
--pl-c-field-bg --pl-color-surface Control fill.
--pl-c-field-fg --pl-color-text Control text.
--pl-c-field-inline-label-size 10rem Label column size for .pl-c-field--inline.

States

State Selector Behavior
focus :focus-visible (control) crisp accent ring + soft glow (box-shadow)
invalid :user-invalid, [aria-invalid="true"] danger ring + glow, untoned hint -> danger-text
focused invalid invalid + :focus-visible danger border/hint + standard focus glow
disabled :disabled (control) field muted (0.55 opacity), not-allowed
placeholder ::placeholder text-muted
message tone .pl-c-field__hint[data-tone] success, warning, or danger message color

Accessibility

  • Label association: the wrapper is a <label>, so clicking the label focuses the control. When the field contains an action button or multiple focusable elements, use <label for> and a matching control id instead.
  • Required marker: .pl-c-field__required is visual only and should be aria-hidden; the native required attribute is what browsers and assistive technology use.
  • Focus: the control draws a crisp 2px accent ring with a soft blurred glow (a layered box-shadow). The border is intentionally NOT recolored - recoloring it alongside a ring produced a “double blue stroke” (solid border
    • translucent ring). The ring is the single focus indicator; the border stays as the neutral field edge. This reads as a modern SaaS input rather than a browser default outline.
  • Invalidity: uses :user-invalid (Baseline 2024) for HTML5 constraints and [aria-invalid="true"] for server/framework errors. Never rely on color alone: the hint text also changes to danger-text. When an invalid control has keyboard focus, the ring uses the standard focus color so the active typing target does not flash a destructive halo.
  • Error text association: for screen-reader users, add aria-describedby on the control pointing at the hint’s id (the wrapper-label association covers the label; the hint needs the explicit link).
  • Framework agnostic: hypermedia, htmx, Alpine, Vue, React, server-rendered HTML, and plain forms all use the same DOM contract: native controls, constraints, aria-invalid, and aria-describedby.
  • Read-only vs disabled: use readonly when the value can be selected or copied, and disabled when the control is unavailable and should be skipped by form submission.

Theme, density, RTL, motion

  • Light/dark: all colors via tokens (light-dark()).
  • Density: sizing from --pl-control-block-size; [data-pl-density] on an ancestor.
  • RTL: logical properties throughout (padding-inline, inline-size, min-block-size).
  • Reduced motion: the border/box-shadow transitions are zeroed by the themes layer.
  • Forced colors: the border uses ButtonBorder; keyboard focus retains a two-pixel Highlight outline when box shadows are suppressed.