Skip to content

Text Input

A native <input> or <textarea> styled via a single class. Same .x-text-input covers three usages:

  1. Bare input<input class="unset x-text-input" />
  2. Bare textarea<textarea class="unset x-text-input" />
  3. Wrapper<div class="x-text-input"> with one input/textarea and any number of affix elements

Mode is selected by what the element is (:is(input, textarea)) and what it contains (:has(> input) → horizontal row, :has(> textarea) → vertical column). No marker class on affixes, no data-side. Source order is render order.

Install

import "elements-kit/ui/styles/theme.css";
import "elements-kit/ui/styles/scaling.css";
import "elements-kit/ui/styles/radius.css";
import "elements-kit/ui/styles/space.css";
import "elements-kit/ui/styles/typography.css";
import "elements-kit/ui/styles/cursor.css";
import "elements-kit/ui/styles/unset.css";
import "elements-kit/ui/styles/palette/gray.css";
import "elements-kit/ui/styles/neutral/gray.css";
import "elements-kit/ui/styles/palette/black-alpha.css";
import "elements-kit/ui/styles/palette/mint.css";
import "elements-kit/ui/styles/accent/mint.css";
import "elements-kit/ui/text-input/text-input.css";

API

<!-- Bare input -->
<input class="unset x-text-input" data-size="2" data-variant="surface" />
<!-- Bare textarea -->
<textarea class="unset x-text-input" data-size="2" data-variant="surface" />
<!-- Input wrapper — horizontal: leading/trailing affixes -->
<div class="x-text-input" data-size="2" data-variant="surface">
<SearchIcon />
<input class="unset" placeholder="Search…" />
<kbd>⌘K</kbd>
</div>
<!-- Textarea wrapper — vertical: top/bottom affixes -->
<div class="x-text-input" data-size="2" data-variant="surface">
<div>Markdown</div>
<textarea class="unset" />
<div>0 / 280</div>
</div>
AttributeValues
data-variantsurface (default), soft
data-size1, 2 (default), 3
data-accent (on a parent or the element)any imported color scale
disabled / readonlynative — dims background, switches cursor

Sizing

data-sizeinput heighttextarea min-heightradius
1--space-5--space-8max(--radius-2, --radius-pill)
2 (default)--space-6--space-9max(--radius-2, --radius-pill)
3--space-780pxmax(--radius-3, --radius-pill)

Variants

  • surface (default)--color-surface background, 1px --neutral-a7 border, --focus-8 focus ring.
  • soft--accent-a3 background, no border, --accent-8 focus ring.

Wrapper anatomy

Inside the wrapper, document order chooses where an affix lands.

<div class="x-text-input">
<span>A</span> <!-- before input → leading (left for input, top for textarea) -->
<input class="unset" />
<span>B</span> <!-- after input → trailing (right for input, bottom for textarea) -->
</div>
  • The wrapped <input> / <textarea> has its background, border, padding, and outline stripped — the wrapper owns the chrome.
  • Affixes get display: flex; align-items: center; cursor: text and a negative margin that extends them flush with the wrapper edge.
  • Focus-within outline wraps the whole container, so a focus ring lands once on the row/column rather than on the inner control.

Clicking an affix focuses the input because the affix carries cursor: text and the input remains the only focusable child in the row/column.

States

  • Disabled / read-onlyvar(--neutral-a11) text, dimmed background. When the input is empty (:placeholder-shown), cursor becomes var(--cursor-disabled) and propagates to affixes inside the wrapper.
  • Autofill — Chrome’s yellow autofill background is suppressed via background-clip: text while preserving the colored text.

Accessibility

  • Pair with a <label> (wrapping or for=). The wrapper does not own the label.
  • The wrapper is a presentational container; the <input> / <textarea> keeps all native semantics, form participation, and ARIA.
  • aria-invalid="true" on the input/textarea is yours to apply for error states.