R RenDS v0.13.0

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

We'll send the verification link here.
1 Container

.ren-field wrapper. Stacks the parts vertically with a small gap. Use <ren-field> if you want auto-wiring.

2 Label

<label class="ren-field-label">. Always paired to its input via for. Add data-required for the asterisk.

3 Control

<input class="ren-input"> — or <textarea> / <select> with the same class. The control owns its native validation.

4 Description

.ren-field-description. Helper text under the input. Linked to the input via aria-describedby.

5 Error

.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.

We'll never share your email with anyone.
<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.

Sizes
States
Please enter a valid email address.
Required
Marked with an asterisk via data-required on the label.
With icon
Textarea
Same .ren-input class works on <textarea>.
Select
Native <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

Tab Moves focus into the input. Disabled inputs are skipped.
Shift + Tab Moves focus backward.
Click on the label Focuses the paired input — only works when the label has a 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-describedby on the input pointing at the description
  • aria-errormessage on the input pointing at the error
  • aria-invalid="true" when the field is in an invalid state
  • role="alert" and aria-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's invalid event 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>