Input
hc-input is applied to the standard <input>, <select>, and
<textarea> elements. State (disabled, aria-invalid) lives on the
element itself, exactly as in plain HTML.
Also known as: text field, text box.
Basic HTML
Section titled “Basic HTML”<input class="hc-input" type="text" placeholder="Your name"><input class="hc-input" type="email" placeholder="you@example.com">
<select class="hc-input"> <option>One</option> <option>Two</option> <option>Three</option></select>
<textarea class="hc-input" rows="3" placeholder="Notes…"></textarea>data-size accepts sm, md (default), and lg.
<input class="hc-input" data-size="sm" placeholder="Small"><input class="hc-input" placeholder="Default"><input class="hc-input" data-size="lg" placeholder="Large">States
Section titled “States”<input class="hc-input" placeholder="Focus me to see the ring"><input class="hc-input" aria-invalid="true" value="not-an-email"><input class="hc-input" disabled value="Disabled value">The aria-invalid="true" attribute switches the border to the error
color and changes the focus-ring color. Pair it with a message via
hc-field.
Variants
Section titled “Variants”data-variant recolors the border as a validation cue —
success (green), warning (amber), error (red) — the same
vocabulary as select,
datepicker, and the
other form fields. (textarea takes the same attribute.)
<input class="hc-input" data-variant="success" value="Looks good"><input class="hc-input" data-variant="warning" value="Double-check this"><input class="hc-input" data-variant="error" value="Fix this">Prefer aria-invalid="true" over data-variant="error" for the error
case — it paints the same border and announces the state to assistive
tech. success / warning have no native attribute, so the variant is
the only way to express them. See the cross-component
Variants matrix.
Numeric fields
Section titled “Numeric fields”Inputs render digits as tabular figures by default
(font-variant-numeric: tabular-nums) — a steady rhythm while typing
digits, and width parity with table cells; text content is unaffected.
For amount-style fields, data-numeric end-aligns the value — the same
attribute and semantics as table
cells, logical so RTL flips free:
<label class="hc-field"> <span class="hc-field__label">Amount</span> <input class="hc-input" name="amount" data-numeric inputmode="decimal"></label>Grouped amounts
Section titled “Grouped amounts”For amount fields, add data-hc-format="number" — the auto-installed
installFormat() behavior keeps the display grouped and the wire
raw:
<label class="hc-field"> <span class="hc-field__label">Amount</span> <div class="hc-input-group"> <span class="hc-input-addon">¥</span> <input class="hc-input" name="amount" type="text" inputmode="numeric" data-numeric data-hc-format="number" value="1,234,567"> </div></label>- Blur normalizes fullwidth digits (
1234→1,234, NFKC) and groups per locale; focus shows the raw value again, so editing never fights separators (and the behavior needs no caret management). - The server always receives the raw value (
amount=1234567): the behavior rewrites the entry list in theformdataevent, which fires for both the htmx request (new FormData(form)) and the native submit. data-decimals="2"pads to a minimum number of fraction digits (display only — it never rounds);data-localeoverrides the grouping locale, defaulting to the closest[lang].- Render the initial value grouped server-side if it should look grouped before the first blur — the wire value stays raw either way.
Use type="text" + inputmode="numeric", not type="number":
type="number" rejects grouped values, drops leading zeros, and
changes the value on scroll — all wrong for amounts. Unparseable input
is left exactly as typed; the server stays the validator
(field-errors).
IME normalization
Section titled “IME normalization”data-hc-normalize (the auto-installed installNormalize()) rewrites
a control’s value on commit, so fullwidth leftovers from IME typing
self-correct without re-typing:
<input class="hc-input" name="sku" data-hc-normalize="ascii"><input class="hc-input" name="furigana" data-hc-normalize="kana">ascii— fullwidth ASCII → halfwidth (AB12→AB12, NFKC) and ideographic space → plain space. Put it only on fields whose wire format is ASCII (codes, identifiers, phone digits).kana— halfwidth kana → fullwidth (タロウ→タロウ) and hiragana → katakana (やまだ→ヤマダ), for furigana fields.
The rewrite runs in a capture-phase change listener, so htmx triggers
reading target.value already see the normalized value — with a
formdata safety net for values that never fired change.
Input masks
Section titled “Input masks”data-hc-mask (the auto-installed installMask()) renders fixed-format
codes as the user types — literals appear by themselves and fullwidth
input fills the slots:
<input class="hc-input" name="postal" inputmode="numeric" placeholder="123-4567" pattern="\d{3}-\d{4}" data-hc-mask="postal-jp">
<input class="hc-input" name="product" placeholder="AB-12" data-hc-mask="AA-##">- Tokens:
#digit,aletter,Aletter (upcased),*alphanumeric; every other character is a literal.postal-jpis an alias for###-####. Characters that fit no slot are dropped. - Typing
1234shows123-4(literals render lazily); Backspace and Delete hop a literal run and always consume a raw character, so the caret never sticks on a hyphen. - The submitted value is the displayed canonical form.
data-hc-mask-submit="raw"strips literals on the wire (servers that store 7-digit postal codes) — sameformdatamechanism as grouped amounts. - Mirror the mask with
pattern+placeholder: the behavior never blocks submission, so no-JS submits stay validatable.
Variable-width formats (Japanese landline numbers) have no fixed mask — leave those fields unmasked and normalize server-side.
Multi-value conditions
Section titled “Multi-value conditions”Business screens often need to ask about a list of identifiers — a
column of order numbers pasted out of a spreadsheet. A <textarea> is
the right control for that (it takes a paste of any size), and
installMultiValue() puts each line on the wire as its own value:
<textarea class="hc-input" name="f-buyer" data-hc-multi="lines">ZAB001000000ZAB001000001ZAB001000002</textarea>?f-buyer=ZAB001000000&f-buyer=ZAB001000001&f-buyer=ZAB001000002The split happens on the formdata event, the same hook
installFormat() uses — htmx’s new FormData(form) and a native submit
both fire it, so one listener covers both transports and nothing wraps
the network. Values are trimmed and de-duplicated; data-hc-multi="commas"
splits on commas too. A control emptied of everything contributes no
entry at all — an empty condition is not a condition.
Have the server accept the raw newline-joined value as well: without JavaScript the textarea submits exactly that, and splitting it server-side keeps the no-JS path working.
The datagrid-filter recipe documents what to do when the list outgrows a URL — a stored condition set addressed by id, which must never fail open.
Auto-growing textarea
Section titled “Auto-growing textarea”A composer or a remarks field should grow with what is typed instead of
scrolling inside two rows. Add data-autosize and the textarea sizes
itself to its content — CSS only (field-sizing: content), no
script, no resize handle — from the rows floor up to
--hc-input-autosize-max (default 50vh), after which it scrolls:
<textarea class="hc-input" name="notes" rows="2" data-autosize></textarea>
<!-- Cap it lower than the default 50vh: --><textarea class="hc-input" name="notes" rows="2" data-autosize style="--hc-input-autosize-max: 12rem"></textarea>Character count
Section titled “Character count”A bounded field — a memo with maxlength, a title with a soft limit —
tells the user where they stand. Put data-hc-count on the control and
an <output for="<control id>"> beside it; installCount() keeps the
output at used / max on every keystroke:
<div class="hc-field"> <label class="hc-field__label" for="memo">Memo</label> <textarea class="hc-input" id="memo" name="memo" maxlength="80" data-hc-count aria-describedby="memo-count"></textarea> <output class="hc-field__hint" id="memo-count" for="memo">0 / 80</output></div>- The server renders the initial text (
0 / 80, or the current count of an edited record), so it is right before any script runs and without one. - The limit is
maxlength— hard: the browser refuses further input — ordata-hc-count-max="N"for a soft limit, where typing past it is allowed and the output is markeddata-count-state="over"(error colour); the last 10% of either limit is"near"(warning colour). Length is counted the waymaxlengthcounts it (UTF-16 code units — an emoji is two), so the two never disagree. - Announcing. An
<output>is a live region (role="status"), so the behavior updates it only after a 500 ms typing pause — a screen reader hears “12 / 80” once per pause, not once per keystroke. Use a plain<p class="hc-field__hint" id="…">withdata-hc-count="<id>"on the control for a count that updates immediately and is never announced. - Wording comes from the i18n catalog
(
count.of:{used} / {max}, with{remaining}available;count.usedwhen there is no limit).
htmx usage
Section titled “htmx usage”hc-input works with htmx like any other input. The live-search
recipe wires the input to send a request as the user types.
<input class="hc-input" type="search" name="q" placeholder="Search" data-hx-get="/items" data-hx-trigger="input changed delay:300ms, search" data-hx-target="#results" data-hx-swap="innerHTML">Accessibility
Section titled “Accessibility”- Always associate inputs with a label. Use
<label for="...">linked to the input’sid, or wrap the input in a<label>element. - For validation errors, set
aria-invalid="true"on the input and reference the message witharia-describedby. - Do not remove the focus outline. The component replaces the default
outline with a visible border + box-shadow ring driven by
--hc-color-focus-ring. - Prefer the native
disabledattribute when the input must not be interactive. Usearia-disabled="true"only when the control must remain focusable.
Theming tokens
Section titled “Theming tokens”Component tokens (in component.tokens.json):
| Token path | Purpose |
|---|---|
input.height / input.padding-x |
Default size. |
input.radius |
Border radius. |
input.bg / input.fg |
Default colors. |
input.border / input.focus-border |
Border in normal/focus state. |
input.success-border / input.warning-border / input.error-border |
Border for data-variant="success" / "warning" / aria-invalid. |
input.placeholder |
Placeholder color. |
input.disabled-bg |
Background when disabled. |
input.{sm,lg}.{height,padding-x,font-size} |
Size-specific overrides. |
CSS variables
Section titled “CSS variables”Show the generated CSS variables
--hc-input-height--hc-input-padding-x--hc-input-radius--hc-input-font-size--hc-input-bg | -fg | -border | -placeholder--hc-input-focus-border--hc-input-success-border | -warning-border | -error-border--hc-input-disabled-bg--hc-input-sm-height | -sm-padding-x | -sm-font-size--hc-input-lg-height | -lg-padding-x | -lg-font-size--hc-color-focus-ring--hc-color-errorRelated
Section titled “Related”- Field — pairs an input with a label and message.
- live-search recipe — input + htmx debounce + result region.
Used in recipes: Autosave · Chat messages · Live search · Postal address · Unsaved changes