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.
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.
Hidden label search
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 controlidinstead. - Required marker:
.pl-c-field__requiredis visual only and should bearia-hidden; the nativerequiredattribute 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 todanger-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-describedbyon the control pointing at the hint’sid(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, andaria-describedby. - Read-only vs disabled: use
readonlywhen the value can be selected or copied, anddisabledwhen 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-pixelHighlightoutline when box shadows are suppressed.