Component Porchlight CSS

Field

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

52 components 51 stable 1 experimental

Command

Search Porchlight

Field

The .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="c-field">
  <span class="c-field__label">
    Workspace name
    <span class="c-field__required" aria-hidden="true">*</span>
  </span>
  <input
    class="c-field__control"
    required
    placeholder="Acme Ops"
    aria-describedby="workspace-name-help"
  />
  <span class="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="c-field">
  <label class="c-field__label" for="workspace-slug">Workspace URL</label>
  <div class="c-input-group">
    <input
      id="workspace-slug"
      class="c-field__control"
      placeholder="acme-ops"
    />
    <span class="c-input-group__addon">.porchlight.app</span>
  </div>
  <span class="c-field__hint"
    >Use lowercase letters, numbers, and hyphens.</span
  >
</div>

Class contract

SelectorRole
.c-fieldField wrapper: usually a <label>, or a container with for/id.
.c-field--inlineTwo-column label/control layout that stacks on narrow viewports.
.c-field__labelThe visible label text.
.c-field__controlThe native input/select/textarea.
.c-field__requiredOptional visual required marker inside the label.
.c-field__hintHelper, warning, success, or error text below the control.
.c-field__messagesOptional stack for multiple .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="c-field">
  <span class="c-field__label">Billing email</span>
  <input class="c-field__control" type="email" placeholder="ops@example.com" />
  <span class="c-field__hint">Invoices and receipts are sent here.</span>
</label>

Inline field

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

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

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

<label class="c-field c-field--inline">
  <span class="u-sr-only">Search accounts</span>
  <input
    class="c-field__control"
    type="search"
    placeholder="Search accounts..."
  />
</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="c-field">
  <span class="c-field__label">Workspace URL</span>
  <input class="c-field__control" type="url" required placeholder="https://" />
  <span class="c-field__hint">Must be a valid URL.</span>
</label>

Required marker

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

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

Message tones

Use .c-field__hint for one message, or .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="c-field">
  <label class="c-field__label" for="workspace-slug">Workspace slug</label>
  <input
    id="workspace-slug"
    class="c-field__control"
    required
    aria-invalid="true"
    aria-describedby="workspace-slug-help workspace-slug-error"
  />
  <span class="c-field__messages">
    <span class="c-field__hint" id="workspace-slug-help"
      >Use lowercase letters, numbers, and hyphens.</span
    >
    <span
      class="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="c-field">
  <label class="c-field__label" for="invite-email">Invite email</label>
  <input
    id="invite-email"
    class="c-field__control"
    type="email"
    value="already-used@example.com"
    aria-invalid="true"
    aria-describedby="invite-email-error"
  />
  <span class="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="c-field">
  <label class="c-field__label" for="email">Email</label>
  <input
    id="email"
    class="c-field__control"
    name="email"
    type="email"
    required
    aria-invalid="true"
    aria-describedby="email-error"
  />
  <span class="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)

TokenDefaultPurpose
--c-field-border--pl-color-borderControl border; overridden on focus/invalid.
--c-field-bg--pl-color-surfaceControl fill.
--c-field-fg--pl-color-textControl text.
--c-field-inline-label-size10remLabel column size for .c-field--inline.

States

StateSelectorBehavior
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 invalidinvalid + :focus-visibledanger border/hint + standard focus glow
disabled:disabled (control)field muted (0.55 opacity), not-allowed
placeholder::placeholdertext-muted
message tone.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: .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-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: border falls back to ButtonBorder.