Input
Text input fields collect single-line data from users. They are the foundation of every form in the system. All input types — text, email, password, search, number — share the same base style via the .input class.
Overview
When to Use
Use Input when
- Collecting short, single-line text data
- Email, password, name, phone number fields
- Search boxes and filter inputs
- Number or date input
- Any free-form text under ~100 characters
Don't use Input when
- Multi-line content needed — use Textarea instead
- Choosing from predefined options — use Select
- Binary true/false — use Checkbox or Toggle
- Long content (>100 chars) — use Textarea
- Searching with autocomplete — use a combobox pattern
Anatomy
Icon
Container
Enter your work email address
Hint text
1
Label
UPPERCASE 12px fw-700, color:#64748B, letter-spacing:0.5px. Always a
<label> element linked to input via for/id. Never omit label — use visually-hidden label if label must be hidden.2
Container
The
<input> element with class .input. padding:10px 14px, border-radius:10px, border:1px solid #E2E8F0, font-size:13px. Width is 100% by default.3
Icon (optional)
Absolute-positioned SVG inside
.input-wrap. Left icon uses .input-icon-left + adds .has-icon-left to input (padding-left:36px). Right icon uses .input-icon-right + .has-icon-right.4
Hint Text
Optional helper text below the input. Class
.form-hint. 11px, #94A3B8. Visible in default state. Replaced by error/success message when validation fires.5
Validation Message
Error:
.form-error-msg — 11px #F05252 fw-600, includes warning icon. Success: .form-success-msg — 11px #0E9F6E. Replaces hint text when shown.6
Focus Ring
3px soft ring via box-shadow. Primary:
0 0 0 3px rgba(59,91,219,.1) with border-color:#3B5BDB. Error: 0 0 0 3px rgba(240,82,82,.1). Never outline:none.States
Default
border:#E2E8F0, bg:#fff
Hover
border-color:#CBD5E1
Focus
border:#3B5BDB + 3px ring
Invalid email
Error
border:#F05252 + error msg
Email available
Success
border:#0E9F6E + success msg
Disabled
bg:#F8FAFC, opacity:.75
Read Only
bg:#F8FAFC, no edit cursor
Properties
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
| type | string | optional | 'text' | HTML input type. Always set explicitly. 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url' | 'date' |
| placeholder | string | optional | undefined | Ghost text shown when empty. Should hint at the expected format, not repeat the label. |
| value | string | optional | undefined | Controlled value. Use with onChange for controlled inputs. |
| disabled | boolean | optional | false | Prevents all interaction. bg:#F8FAFC, opacity:.75, cursor:not-allowed. |
| readOnly | boolean | optional | false | Prevents editing but allows focus and selection. bg:#F8FAFC. Different from disabled — still focusable. |
| error | string | optional | undefined | Error message text. When set, adds .input-error to input and renders .form-error-msg below. |
| hint | string | optional | undefined | Helper text shown below input. Hidden when error is active. |
| label | string | required | — | Label text. Always required — use visually hidden label if design requires no visible label. |
| iconLeft | ReactNode | optional | undefined | SVG icon displayed on the left. Adds padding-left:36px to input automatically. |
| iconRight | ReactNode | optional | undefined | SVG icon displayed on the right. Adds padding-right:36px to input. Often used for clear or toggle actions. |
| onChange | function | optional | undefined | Change handler for controlled inputs. (e: ChangeEvent<HTMLInputElement>) => void |
| maxLength | number | optional | undefined | Max character count. When set, show a character counter (.input-char-count) below the input. |
Accessibility
| Key | Action |
|---|---|
| Tab | Move focus to input |
| Shift+Tab | Move focus away |
| Esc | Clear search inputs (type="search") or blur |
| Type | Enters character into input |
| Ctrl+A | Select all text |
| Requirement | Implementation |
|---|---|
| Label association | Always use <label for="inputId"> linked to <input id="inputId">. Never use placeholder as substitute for label. |
| Error announcement | Add aria-invalid="true" to input in error state. Error message div gets role="alert" for immediate announcement. |
| Required fields | Add aria-required="true" and visual * indicator after label. |
| Description | Link hint text via aria-describedby="hintId" on input. |
| Password visibility toggle | Right-icon button must have aria-label="Show password" / aria-label="Hide password". |
| Focus ring | Never remove. 3px soft box-shadow ring, visible on all focus states including keyboard and mouse. |
Behavior
| Trigger | Behavior |
|---|---|
| Focus | border-color:#3B5BDB + box-shadow: 0 0 0 3px rgba(59,91,219,.1). Transition: 0.2s. |
| Hover (unfocused) | border-color:#CBD5E1. Subtle darkening signals interactivity. |
| Blur | Triggers validation if field has been touched. Reverts to default border if no error. Never validate on keystroke unless explicitly needed. |
| Input (type) | Updates value. Character counter updates if maxLength set. No validation on keystroke by default. |
| Disabled | No hover, focus, or click response. opacity:.75, cursor:not-allowed, bg:#F8FAFC. |
| Error state | border-color:#F05252, focus ring rgba(240,82,82,.1). Error message rendered with icon below. Clears on successful re-validation. |
| Password toggle | Right eye-icon toggles type between "password" and "text". Icon changes to reflect current state. |
| Clear (search) | X button in right icon clears value and re-focuses input. |
Composition
Allowed
Inside .form-group with .form-label above
Inside Modal body for forms
Inside filter bar with flex:1 min-width
With .input-wrap for icon variants
With .form-error-msg or .form-hint below
Two-column grid for paired fields (name, address)
Forbidden
Without a label (use aria-label if label hidden)
Placeholder used as label substitute
Bare input without .form-group wrapper
Inline in body text or table cells
Nested inside another input
Validation error without .form-error-msg message
Content Rules
Good Label
Email Address
Noun phrase. UPPERCASE. Specific.
Bad Label
Enter your email here
Sentence case. Too verbose for a label.
Good Placeholder
you@company.com
Shows expected format, not the label.
Bad Placeholder
Email Address
Duplicates the label. Disappears on type.
Good Error
Please enter a valid email address
Specific, tells user what to fix.
Bad Error
Invalid input
Vague, provides no guidance.
AI Implementation Guide
| Rule | Detail |
|---|---|
| Always wrap in .form-group | Every input must be inside <div class="form-group"> with margin-bottom:16px. |
| Label is mandatory | Always include <label class="form-label"> linked via for/id. UPPERCASE 12px fw-700 letter-spacing:0.5px. |
| Base class | All form fields (input, select, textarea) use .input class. Never custom-style individual fields. |
| Error state | Add .input-error to input AND render <div class="form-error-msg"> with warning SVG icon below. |
| Focus ring | Never override. box-shadow: 0 0 0 3px rgba(59,91,219,.1) on focus. For error state: rgba(240,82,82,.1). |
| Icon pattern | Wrap in .input-wrap. Add .has-icon-left or .has-icon-right to input. Place icon as sibling inside .input-wrap. |
| Never validate on keystroke | Validate on blur (when user leaves field) or on form submit. Never show errors while user is typing. |
| Placeholder vs label | Placeholder shows example format (e.g., "user@company.com"). Label names the field (e.g., "EMAIL ADDRESS"). Never use placeholder as label. |
Related Components
Design Decisions
Why 10px border-radius instead of a sharper or fully rounded style?
10px is the standard radius for interactive controls (inputs, selects, textareas, dropdown items). It feels modern without being too casual. It differs from cards (20px) to signal "this is a control, not a container" and from buttons (12px) to avoid visual confusion between the two.
Why use box-shadow for focus ring instead of outline?
box-shadow respects the border-radius, creating a soft ring that hugs the shape of the input. The native outline is rectangular and ignores border-radius. The 3px soft ring at 10% opacity adds a halo effect that is subtle on default state and visible enough for accessibility without being jarring.
Why validate on blur, not on keystroke?
Showing errors while the user is still typing is disruptive and frustrating — the input is in a valid in-progress state that shouldn't be flagged. Blur validation respects the user's flow: it checks when they signal "I'm done with this field" by moving away. This is standard UX practice and reduces false error states.
Changelog
v1.0.0July 2026Added
- Initial release — all HTML input types supported via .input class
- 5 states: default, hover, focus, error, success, disabled, read-only
- Icon support (left and right) via .input-wrap pattern
- Validation messages (.form-error-msg, .form-success-msg)
- Character counter support via .input-char-count
Best Practices
Do
Enter your work email.
Specific label, contextual placeholder showing format, helpful hint below.
Don't
No label, placeholder used as label substitute — disappears on type, inaccessible.
Do
Required indicator, type=password for security, placeholder shows constraint.
Don't
Error
type=text for password (exposes value), vague error message, no guidance.