Primitive
Field Hybrid
The smallest accessible unit of a form: label, input, helper text, error message — wired together. Use the CSS classes directly, or wrap your markup in <ren-field> and let the component generate IDs and ARIA attributes for you.
About
Overview
A form field is the building block of every form in your app. RenDS gives you two ways to compose it: write the classes manually if you like full control, or wrap semantic HTML in <ren-field> and the component will fill in the parts that humans regularly forget — generated ids, the for attribute on the label, aria-describedby for the helper text, aria-errormessage for validation messages, and data-required on the label when the input is required.
The visual styling lives entirely in CSS. The Web Component is a thin layer of accessibility wiring on top — your form still works without it, just with fewer ARIA hooks.
Use one field per question. A field bundles a label and one input. Don't reuse the wrapper to group several inputs — for that, use a fieldset with a legend instead.
Parts
Anatomy
A complete field has four content parts plus the container. Description and error are optional.
Assembled
.ren-field wrapper. Stacks the parts vertically with a small gap. Use <ren-field> if you want auto-wiring.
<label class="ren-field-label">. Always paired to its input via for. Add data-required for the asterisk.
<input class="ren-input"> — or <textarea> / <select> with the same class. The control owns its native validation.
.ren-field-description. Helper text under the input. Linked to the input via aria-describedby.
.ren-field-error with role="alert". Hidden until the field is invalid. Replaces / supplements the description on error.
Live
Demo
Type something invalid and the error appears. Click the field to focus and the focus ring follows the accent color.
<ren-field>
<label>Email address</label>
<input type="email" placeholder="name@example.com">
<span data-description>We'll never share your email with anyone.</span>
</ren-field>
Same field written without the Web Component — explicit classes, manual for and aria-describedby wiring:
<div class="ren-field">
<label class="ren-field-label" for="email">Email address</label>
<input class="ren-input" type="email" id="email"
placeholder="name@example.com"
aria-describedby="email-desc">
<span class="ren-field-description" id="email-desc">
We'll never share your email with anyone.
</span>
</div>
Shapes
Variants
The same field structure adapts to different control types and visual densities.
data-required on the label.
.ren-input class works on <textarea>.
<select> with the same class. For a custom-styled dropdown, use ren-select.
Reference
API
CSS classes
Apply directly if you don't use the Web Component. The visual styling is identical either way.
| Class | Effect |
|---|---|
.ren-field |
Container. Vertical flex stack with a small gap between label, control, and description / error. |
.ren-field-label |
Label styling. Use on the actual <label> tag so the click-to-focus behavior works natively. |
.ren-field-label[data-required] |
Adds a red asterisk after the label text via ::after. Set automatically when the input has required. |
.ren-input |
Base input style. Works on <input>, <textarea>, and <select>. 44 px touch target, focus ring, hover, disabled. |
.ren-input-sm / .ren-input-lg |
Size modifiers. Default (no modifier) is medium (44 px). Small is denser; large is more prominent. |
.ren-input-wrapper |
Use to nest an icon (or any decoration) alongside the input. Acts as a relative container. |
.ren-input-icon / .ren-input-icon-end |
Place an icon before (default) or after (-end) the input. The input is automatically padded so the icon doesn't overlap text. |
.ren-field-description |
Helper text. Muted color, smaller font. Should be linked to the input via aria-describedby. |
.ren-field-error |
Error message. Danger color. Hidden by default; shown when the field has data-invalid. |
.ren-field[data-invalid] |
Invalid container state. Recolors the input border and shows the error region. |
.ren-input-error |
Apply on the input directly to force the invalid look without using data-invalid on the wrapper. |
Web Component attributes
Set on the <ren-field> wrapper. The component watches them and re-syncs ARIA state when changed.
| Attribute | Type | Default | Notes |
|---|---|---|---|
data-invalid |
boolean | false | Presence shows the error region and sets aria-invalid="true" on the input. Removed automatically when the input becomes valid again on input. |
Auto-wiring (the value prop)
What <ren-field> sets automatically when it mounts. You can write the simplest possible HTML — semantic tags, no IDs — and the component fills in the rest.
| Wired up | How |
|---|---|
| Label ↔ Input | Input gets a generated id; label gets a matching for attribute. |
| Description ↔ Input | Description gets a generated id; input's aria-describedby is appended (existing values preserved). |
| Error ↔ Input | Error gets a generated id; input's aria-errormessage is set. Error gets role="alert" + aria-live="polite". |
| Required indicator | If the input has required, the label gets data-required (renders the asterisk). |
| Validation listener | Listens for native invalid events; sets data-invalid on the field and reveals the error message. Clears on next valid input. |
JavaScript methods
Available on every <ren-field> instance.
| Method | Description |
|---|---|
setError(message) |
Marks the field invalid and writes the error message into the error region. Use for server-side validation results. |
clearError() |
Removes the invalid state and hides the error region. |
invalid |
Boolean getter / setter. Reads the current invalid state, or assigns one (same as setError / clearError). |
const field = document.querySelector('ren-field');
// After a server response
if (response.error) {
field.setError('That email is already taken.');
} else {
field.clearError();
}Inclusive by default
Accessibility
Form accessibility lives in three relationships: label-to-input, description-to-input, and error-to-input. The Web Component wires all three. If you're using the CSS classes alone, you set them by hand — this section is your checklist.
Keyboard
for attribute matching the input's id (or wraps the input directly).
What the component sets
When you wrap your markup in <ren-field>, the following ARIA wiring is automatic:
label[for]↔input[id]aria-describedbyon the input pointing at the descriptionaria-errormessageon the input pointing at the erroraria-invalid="true"when the field is in an invalid staterole="alert"andaria-live="polite"on the error so screen readers announce it
Validation messaging
Two paths, both supported:
- Native HTML5: add
required,type="email",pattern, etc. to the input. The browser'sinvalidevent flips the field into the invalid state automatically. - Programmatic: call
field.setError("custom message")after a server response. The same DOM hooks render the same way.
Always pair the label. A bare input with a placeholder is not a labelled input. Screen readers announce nothing meaningful. Even a visually hidden label (.ren-sr-only) is better than no label.
Don't trap focus inside the field
The field is just a wrapper. Tab and Shift+Tab move out of it normally. Don't add tabindex to the wrapper — focus belongs on the input itself.
Patterns
Examples
Login form
Two required fields, primary submit. Native HTML5 validation handles the empty state.
<form>
<ren-field>
<label>Email</label>
<input type="email" required autocomplete="email">
</ren-field>
<ren-field>
<label>Password</label>
<input type="password" required autocomplete="current-password">
</ren-field>
<button type="submit" class="ren-btn ren-btn-primary">Sign in</button>
</form>
Server-side error
Submit, get an error from the API, attach it to the right field.
const form = document.querySelector('form');
const emailField = form.querySelector('ren-field:has(input[type="email"])');
form.addEventListener('submit', async (e) => {
e.preventDefault();
emailField.clearError();
const res = await fetch('/api/signup', { method: 'POST', body: new FormData(form) });
const data = await res.json();
if (data.error?.field === 'email') {
emailField.setError(data.error.message);
emailField.querySelector('input').focus();
return;
}
location.assign('/dashboard');
});
Search box with icon
Wrap the input in .ren-input-wrapper with an icon for a leading affordance.
<ren-field>
<label class="ren-sr-only">Search</label>
<div class="ren-input-wrapper">
<span class="ren-input-icon" aria-hidden="true">
<!-- search SVG -->
</span>
<input type="search" placeholder="Search projects…">
</div>
</ren-field>
Two fields side by side
The field is a normal block. Drop them into any layout primitive — here, a 2-column grid.
<div class="ren-grid-2">
<ren-field>
<label>First name</label>
<input autocomplete="given-name">
</ren-field>
<ren-field>
<label>Last name</label>
<input autocomplete="family-name">
</ren-field>
</div>