Skip to content

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.

data-size accepts sm, md (default), and lg.

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.

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

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.

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>

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 (12341,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 the formdata event, 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-locale overrides 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).

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 (AB12AB12, 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.

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, a letter, A letter (upcased), * alphanumeric; every other character is a literal. postal-jp is an alias for ###-####. Characters that fit no slot are dropped.
  • Typing 1234 shows 123-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) — same formdata mechanism 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.

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">ZAB001000000
ZAB001000001
ZAB001000002</textarea>
?f-buyer=ZAB001000000&f-buyer=ZAB001000001&f-buyer=ZAB001000002

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

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">
  • Always associate inputs with a label. Use <label for="..."> linked to the input’s id, or wrap the input in a <label> element.
  • For validation errors, set aria-invalid="true" on the input and reference the message with aria-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 disabled attribute when the input must not be interactive. Use aria-disabled="true" only when the control must remain focusable.

Component tokens (in component.tokens.json):

Token pathPurpose
input.height / input.padding-xDefault size.
input.radiusBorder radius.
input.bg / input.fgDefault colors.
input.border / input.focus-borderBorder in normal/focus state.
input.success-border / input.warning-border / input.error-borderBorder for data-variant="success" / "warning" / aria-invalid.
input.placeholderPlaceholder color.
input.disabled-bgBackground when disabled.
input.{sm,lg}.{height,padding-x,font-size}Size-specific overrides.
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-error

Used in recipes: Autosave · Live search · Postal address · Unsaved changes