textbox
An input that allows free-form text as its value. Textboxes can be single-line like an input field or multi-line like a textarea, and may support rich text editing when used with contenteditable.
Overview
The textbox role identifies an element as an input field that accepts free-form text. By default, textboxes are single-line (like <input type="text">). Add aria-multiline="true" for multi-line behavior (like <textarea>).
Textboxes are commonly implemented using contenteditable elements for custom styling or rich text editing. However, native HTML inputs are preferred for most use cases due to built-in accessibility features.
Native Elements vs role="textbox"
Always prefer native <input> and <textarea> elements. They provide automatic keyboard support, form integration, autocomplete, mobile keyboard optimization, and proper validation. Use role="textbox" only for custom implementations like rich text editors.
Live Demo: Text Input Types
Single-line Textbox
Multi-line Textbox (textarea)
Custom Textbox (contenteditable)
Note: The native <input> and <textarea> elements provide the best accessibility. The contenteditable example demonstrates the textbox role but requires additional work for full accessibility.
Code Examples
Basic Textbox
<!-- Basic Textbox -->
<label for="username">Username</label>
<div
role="textbox"
id="username"
contenteditable="true"
aria-labelledby="username-label"
></div>
<!-- Note: For most cases, use native <input> instead -->
<label for="username-native">Username</label>
<input type="text" id="username-native" />Native HTML Inputs (Preferred)
<!-- Native HTML Text Inputs (Preferred) -->
<!-- Single-line text input -->
<label for="email">Email Address</label>
<input
type="email"
id="email"
placeholder="you@example.com"
autocomplete="email"
required
/>
<!-- Multi-line text input -->
<label for="bio">Biography</label>
<textarea
id="bio"
rows="4"
placeholder="Tell us about yourself..."
></textarea>
<!-- Password input -->
<label for="password">Password</label>
<input
type="password"
id="password"
autocomplete="current-password"
/>
<!-- Benefits of native inputs:
- Built-in keyboard support
- Form validation
- Autocomplete support
- Mobile keyboard optimization
- Browser autofill -->Multi-line Textbox
<!-- Multi-line Textbox (aria-multiline) -->
<label id="comment-label">Your Comment</label>
<div
role="textbox"
aria-labelledby="comment-label"
aria-multiline="true"
contenteditable="true"
class="multiline-textbox"
>
</div>
<!-- Native equivalent (preferred) -->
<label for="comment">Your Comment</label>
<textarea
id="comment"
rows="5"
cols="40"
></textarea>
<!-- aria-multiline="true" indicates the textbox
accepts multiple lines of text, like <textarea>.
Without it, textbox is assumed single-line like <input> -->Rich Text Editor
<!-- Rich Text Editor -->
<div class="rich-editor">
<div role="toolbar" aria-label="Text formatting">
<button aria-label="Bold" aria-pressed="false">B</button>
<button aria-label="Italic" aria-pressed="false">I</button>
<button aria-label="Underline" aria-pressed="false">U</button>
</div>
<div
role="textbox"
aria-label="Rich text editor"
aria-multiline="true"
aria-describedby="editor-instructions"
contenteditable="true"
class="editor-content"
>
<!-- Rich text content here -->
</div>
<div id="editor-instructions" class="sr-only">
Use toolbar buttons to format text.
Press Tab to exit the editor.
</div>
</div>
<!-- Note: Rich text editors require significant
additional accessibility implementation -->Validation Pattern
<!-- Textbox with Validation -->
<div class="form-field">
<label for="email" id="email-label">
Email <span aria-hidden="true">*</span>
</label>
<input
type="email"
id="email"
aria-labelledby="email-label"
aria-required="true"
aria-invalid="true"
aria-describedby="email-error email-hint"
value="invalid-email"
/>
<span id="email-hint" class="hint">
Enter a valid email address
</span>
<span id="email-error" class="error" role="alert">
Please enter a valid email address
</span>
</div>
<script>
const emailInput = document.getElementById('email');
const errorSpan = document.getElementById('email-error');
emailInput.addEventListener('blur', () => {
const isValid = emailInput.validity.valid;
emailInput.setAttribute('aria-invalid', !isValid);
errorSpan.hidden = isValid;
});
</script>
<style>
[aria-invalid="true"] {
border-color: #dc2626;
}
.error {
color: #dc2626;
font-size: 0.875rem;
}
</style>React Component
// React Textbox Component
import { useState, useRef, useId } from 'react';
interface TextboxProps {
label: string;
value: string;
onChange: (value: string) => void;
multiline?: boolean;
rows?: number;
placeholder?: string;
required?: boolean;
error?: string;
hint?: string;
maxLength?: number;
}
function Textbox({
label,
value,
onChange,
multiline = false,
rows = 4,
placeholder,
required = false,
error,
hint,
maxLength,
}: TextboxProps) {
const inputId = useId();
const hintId = useId();
const errorId = useId();
const describedBy = [
hint && hintId,
error && errorId,
].filter(Boolean).join(' ') || undefined;
const inputProps = {
id: inputId,
value,
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
onChange(e.target.value),
placeholder,
'aria-required': required || undefined,
'aria-invalid': !!error || undefined,
'aria-describedby': describedBy,
maxLength,
className: `textbox ${error ? 'error' : ''}`,
};
return (
<div className="textbox-wrapper">
<label htmlFor={inputId}>
{label}
{required && <span aria-hidden="true"> *</span>}
</label>
{multiline ? (
<textarea {...inputProps} rows={rows} />
) : (
<input type="text" {...inputProps} />
)}
{hint && !error && (
<span id={hintId} className="hint">
{hint}
</span>
)}
{error && (
<span id={errorId} className="error-message" role="alert">
{error}
</span>
)}
{maxLength && (
<span className="char-count" aria-live="polite">
{value.length}/{maxLength}
</span>
)}
</div>
);
}
// Usage
function Form() {
const [name, setName] = useState('');
const [bio, setBio] = useState('');
const [email, setEmail] = useState('');
return (
<form>
<Textbox
label="Full Name"
value={name}
onChange={setName}
required
hint="Enter your legal name"
/>
<Textbox
label="Biography"
value={bio}
onChange={setBio}
multiline
rows={5}
maxLength={500}
placeholder="Tell us about yourself..."
/>
<Textbox
label="Email"
value={email}
onChange={setEmail}
required
error={email && !email.includes('@') ? 'Invalid email' : ''}
/>
</form>
);
}Autocomplete Pattern
<!-- Textbox with Autocomplete -->
<div class="autocomplete-container">
<label for="city-input" id="city-label">City</label>
<input
type="text"
id="city-input"
role="combobox"
aria-labelledby="city-label"
aria-autocomplete="list"
aria-controls="city-listbox"
aria-expanded="true"
aria-activedescendant="city-option-2"
autocomplete="off"
/>
<ul
id="city-listbox"
role="listbox"
aria-label="City suggestions"
>
<li role="option" id="city-option-1" aria-selected="false">
New York
</li>
<li role="option" id="city-option-2" aria-selected="true">
Los Angeles
</li>
<li role="option" id="city-option-3" aria-selected="false">
Chicago
</li>
</ul>
</div>
<!-- Note: When adding autocomplete to a textbox,
use role="combobox" instead of role="textbox"
for proper semantics -->Keyboard Support
Native inputs handle keyboard automatically. Custom textboxes must implement:
Standard text input
Or selected text
Or selected text
Add Ctrl for word navigation
Only in multi-line textboxes
Ctrl+Home for start of field
Ctrl+End for end of field
Behavior depends on context
Best Practices
Use native <input> and <textarea> elements whenever possible
Always provide visible labels using <label> elements
Use aria-required for required fields and aria-invalid for validation
Provide helpful placeholder text and aria-describedby hints
Use appropriate input types (email, tel, url) for specialized data
Support autocomplete attributes for common fields
Don't use role="textbox" when native inputs work
Don't rely solely on placeholder text as labels
Don't forget to announce validation errors to screen readers
Don't use contenteditable without proper ARIA implementation
Supported ARIA Attributes
aria-multilineIndicates if textbox accepts multiple lines
aria-labelAccessible name for the textbox
aria-labelledbyReferences element(s) that label the textbox
aria-describedbyReferences hints, errors, or instructions
aria-requiredIndicates if input is required
aria-invalidIndicates validation state (true/false)
aria-readonlyIndicates if textbox is read-only
aria-disabledIndicates if textbox is disabled
aria-placeholderVisible placeholder hint text
aria-autocompleteType of autocomplete support
Common Use Cases
Accessibility Notes
Labels Are Essential
Every textbox needs an accessible label. Use a visible <label> element with the for attribute, or use aria-label/ aria-labelledby. Never rely solely on placeholder text.
Error Handling
Use aria-invalid="true" when validation fails, and ensure error messages are associated via aria-describedby. Consider using role="alert" for dynamically appearing error messages.
Mobile Considerations
Use appropriate input types (type="email", type="tel", etc.) to trigger optimized mobile keyboards. Add inputmode for more control over virtual keyboard layout.

