J3W1 / DESIGN SYSTEM
Text field
Single-line free-text entry, covering every native input type that renders as a line box; search, number and password are variants, not separate components.
stable · LIGHT DOM · 1.1.0
Visible native controls retain FormData, labels, constraints, disabled fieldsets and reset. No duplicate form-value mechanism is installed.
Live variants
default
Shown on your profile and in the window title.
<j3w1-text-field id="example-text-field-default"><div class="text-field">
<label class="text-field-label" for="example-text-field-default-tf-name">Display name <span class="text-field-required" aria-hidden="true">*</span><span class="text-field-required sr-only"> required</span></label>
<div class="text-field-root">
<input class="text-field-input" id="example-text-field-default-tf-name" type="text" value="j3w1" placeholder="Your name" aria-describedby="example-text-field-default-tf-name-help example-text-field-default-tf-name-message" name="example-text-field-default-tf-name">
<span class="text-field-loading" aria-hidden="true">⋯</span>
</div>
<p class="text-field-help" id="example-text-field-default-tf-name-help">Shown on your profile and in the window title.</p>
<p class="text-field-message" id="example-text-field-default-tf-name-message">✕ Use 2 to 32 characters.</p>
</div></j3w1-text-field>password
Twelve characters or more; spaces allowed.
<j3w1-text-field id="example-text-field-password"><div class="text-field">
<label class="text-field-label" for="example-text-field-password-tf-pass">Passphrase <span class="text-field-required" aria-hidden="true">*</span><span class="text-field-required sr-only"> required</span></label>
<div class="text-field-root">
<input class="text-field-input" id="example-text-field-password-tf-pass" type="password" required="" value="correct horse" placeholder="At least 12 characters" aria-describedby="example-text-field-password-tf-pass-help example-text-field-password-tf-pass-message" name="example-text-field-password-tf-pass">
<button class="text-field-action" type="button" aria-pressed="false" aria-label="Reveal passphrase">reveal</button>
<span class="text-field-loading" aria-hidden="true">⋯</span>
</div>
<p class="text-field-help" id="example-text-field-password-tf-pass-help">Twelve characters or more; spaces allowed.</p>
<p class="text-field-message" id="example-text-field-password-tf-pass-message">✕ Too short.</p>
</div></j3w1-text-field>search
Escape clears the query.
<j3w1-text-field id="example-text-field-search"><div class="text-field">
<label class="text-field-label" for="example-text-field-search-tf-search">Search files <span class="text-field-required" aria-hidden="true">*</span><span class="text-field-required sr-only"> required</span></label>
<div class="text-field-root">
<span class="text-field-affix" aria-hidden="true">/</span>
<input class="text-field-input" id="example-text-field-search-tf-search" type="search" value="site.css" placeholder="Name or path" aria-describedby="example-text-field-search-tf-search-help example-text-field-search-tf-search-message">
<button class="text-field-action" type="button" aria-label="Clear search">clear</button>
<span class="text-field-loading" aria-hidden="true">⋯</span>
</div>
<p class="text-field-help" id="example-text-field-search-tf-search-help">Escape clears the query.</p>
<p class="text-field-message" id="example-text-field-search-tf-search-message">✕ No such path.</p>
</div></j3w1-text-field>number
Up and Down step by one; Shift steps by ten.
<j3w1-text-field id="example-text-field-number"><div class="text-field">
<label class="text-field-label" for="example-text-field-number-tf-gap">Inner gap <span class="text-field-required" aria-hidden="true">*</span><span class="text-field-required sr-only"> required</span></label>
<div class="text-field-root">
<input class="text-field-input text-field-input-numeric" id="example-text-field-number-tf-gap" type="number" value="14" min="0" max="64" step="1" placeholder="0" aria-describedby="example-text-field-number-tf-gap-help example-text-field-number-tf-gap-message" name="example-text-field-number-tf-gap">
<span class="text-field-affix" aria-hidden="true">px</span>
<span class="text-field-stepper">
<button class="text-field-action" type="button" aria-label="Increase inner gap">+</button>
<button class="text-field-action" type="button" aria-label="Decrease inner gap">−</button>
</span>
<span class="text-field-loading" aria-hidden="true">⋯</span>
</div>
<p class="text-field-help" id="example-text-field-number-tf-gap-help">Up and Down step by one; Shift steps by ten.</p>
<p class="text-field-message" id="example-text-field-number-tf-gap-message">✕ Between 0 and 64.</p>
</div></j3w1-text-field>affix
Host only; the path is fixed.
<j3w1-text-field id="example-text-field-affix"><div class="text-field">
<label class="text-field-label" for="example-text-field-affix-tf-site">Site <span class="text-field-required" aria-hidden="true">*</span><span class="text-field-required sr-only"> required</span></label>
<div class="text-field-root">
<span class="text-field-affix" aria-hidden="true">https://</span>
<input class="text-field-input" id="example-text-field-affix-tf-site" type="text" value="j3w1.github.io" placeholder="host" aria-describedby="example-text-field-affix-tf-site-help example-text-field-affix-tf-site-message" name="example-text-field-affix-tf-site">
<span class="text-field-affix" aria-hidden="true">/theme/</span>
<span class="text-field-loading" aria-hidden="true">⋯</span>
</div>
<p class="text-field-help" id="example-text-field-affix-tf-site-help">Host only; the path is fixed.</p>
<p class="text-field-message" id="example-text-field-affix-tf-site-message">✕ Not a valid host.</p>
</div></j3w1-text-field>Install and register
Use the exact version. If npm publication is pending, install the release tarball from the installation guide.
npm install --save-exact @j3w1/ui@1.1.0import '@j3w1/ui/tokens.css';
import '@j3w1/ui/styles/text-field.css';
import '@j3w1/ui/register/text-field';Insert the maintained markup above after loading these imports. Its native children provide accessible names and form behavior; the empty custom tag alone does not create a control.
Copy the complete implementation
npx --package=@j3w1/ui@1.1.0 j3w1-ui copy text-field --out ./vendor/j3w1/text-fieldThe CLI verifies every file before writing and refuses overwrites. Serve the output over HTTP and open index.html. Keep its modules, CSS, tokens and licence notices together.
Public API
attributes
| Name | Type / signature | Meaning |
|---|---|---|
disabled | CustomEvent | Disable the native controls owned by this instance. |
loading | CustomEvent | Mark the primary control busy and prevent duplicate activation. |
value | CustomEvent | Forwards value to the primary native control. |
required | CustomEvent | Forwards required to the primary native control. |
readonly | CustomEvent | Forwards readonly to the primary native control. |
checked | CustomEvent | Forwards checked to the primary native control. |
properties
| Name | Type / signature | Meaning |
|---|---|---|
disabled | boolean | Delegates disabled state to native children. |
name | string | Native control name; application form keys remain explicit. |
value | string | Primary native value; assignment does not fabricate user events. |
checked | boolean | Native checkbox state. |
required | boolean | Native required constraint. |
readOnly | boolean | Native readonly property. |
form | HTMLFormElement | null | Owning native form. Read only. |
methods
| Name | Type / signature | Meaning |
|---|---|---|
refresh | (): void | Reconnect behavior after replacing native child structure. |
focus | (options?: FocusOptions): void | Focus the first usable native control. |
checkValidity | (): boolean | Check owned native constraints. |
reportValidity | (): boolean | Report the first invalid owned control. |
clear | (): void | Clear an editable primary input and emit native input/change. |
stepBy | (amount: number): void | Step a number input by the supplied number of native steps. |
events
| Name | Type / signature | Meaning |
|---|---|---|
j3w1-change | CustomEvent | Native change from an owned control, alongside the original native event. |
Dependencies and verification
Registered components: none. Style dependencies: none.
Package presence establishes an available implementation. Actual passes depend on the tested artifact and environment. Read specification execution evidence; read packed-consumer evidence; inspect the complete state matrix.
Full implementation manifest, API and token dependency closure
Canonical specification
Purpose
Single-line free-text entry. Every native type that renders as a line box
(text, password, search, number, email, url, tel) is this
component; date and time inputs are the date-picker and time-picker
components because their popups are host surfaces.
Anatomy
Label above the control; the control box; optional prefix and suffix inside
the box separated by a color.border.divider rule; the input; optional
trailing actions (clear, reveal, stepper); help text below; the validation
message, which takes the help slot visually while help stays in the DOM and
aria-describedby lists both.
States
| State | Visual | Non-colour channel |
|---|---|---|
| default | 1px color.border.control on color.surface.input; text color.text.default | — |
| hover | background → color.interaction.hover.bg; border unchanged | cursor: text |
| focus-visible | border → color.border.active; ring 1px dashed color.interaction.focus.ring at −2px | the ring |
| placeholder-shown | placeholder in color.text.placeholder, italic | italic |
| filled | as default with a value | — |
| required | * after the label in color.status.danger.text plus visually hidden “required” | required attribute; the mark |
| invalid | border 2px color.status.danger.border; message with the ✕ glyph | border width 1 → 2px; glyph; aria-invalid |
| invalid+focus-visible | the 2px danger border and a 1px dashed ring in color.interaction.focus.ring-container at −4px (the control form, recoloured; never the 2px container form) | double boundary |
| invalid+hover | as invalid with the hover background | cursor |
| disabled | text color.text.disabled; border color.border.disabled; background color.interaction.disabled.bg; no hover; the label and help keep their colours | disabled; cursor: not-allowed |
| disabled+filled | as disabled with a value | disabled |
| read-only | no box: 1px dotted color.border.divider bottom edge on color.surface.canvas; text color.text.default; focusable and selectable | readonly; dotted edge |
| read-only+focus-visible | the dotted color.border.divider edge unchanged, plus the dashed ring at −2px | the ring |
| loading | size unchanged; a static ⋯ glyph in color.text.muted inside the box after the input and before any trailing action; input remains editable | glyph; aria-busy |
Precedence when several apply: disabled > loading > invalid > hover; focus-visible is always drawn.
Keyboard
Focus order is label (not focusable) → input → clear or reveal → stepper
buttons. The clear button is type="button" with aria-label="Clear <label>";
the reveal button toggles type between password and text and its own
aria-pressed. Escape in the search variant clears a non-empty value.
Accessibility
A programmatic label is required (<label for>). Help and message are linked
through aria-describedby in that order. aria-invalid="true" is set only
after the user interacted with the field or submitted the form, never on first
paint. The message is announced on submit through the form’s live region, not
on every keystroke. Contrast: text 8.65:1 on the input surface, placeholder
5.81:1, disabled text 3.33:1 (exempt), control border 4.45:1 (≥ 3:1), invalid
border 4.69:1, danger message 5.40:1. The box is at least 24×24 CSS pixels in
compact density.
Portability
Everything is expressible with border, background and outline; the only pseudo-element is the required mark. Toolkits without dashed outlines draw a solid 1px ring in the same colour and record the deviation. Toolkits without a read-only style treat read-only as default text and record it.
Non-examples
Rounded corners. A floating or animated label. An underline-only field with no box. A glow instead of the dashed ring. Placeholder text in red. Disabling by opacity. Using colour alone for the invalid state. A hover that changes the text colour.