searchbox
A type of textbox intended for specifying search criteria. Searchboxes are specialized text inputs that help users find content within a website or application.
Overview
The searchbox role is a specialized form of textbox specifically intended for entering search queries. It inherits all properties of the textbox role but signals to assistive technologies that the input is for searching.
Searchboxes are often paired with autocomplete functionality, using aria-autocomplete and aria-controls to reference a listbox of suggestions.
Native <input type="search"> vs role="searchbox"
The native <input type="search"> provides built-in search semantics, a clear button in most browsers, and the appropriate keyboard on mobile devices. Use role="searchbox" only when you cannot use the native element.
Live Demo: Searchbox with Suggestions
Start typing to see suggestions
Accessibility features: The searchbox uses aria-autocomplete="list" to indicate suggestions are available, and aria-expanded to communicate whether the suggestion list is visible.
Code Examples
Basic Searchbox
<!-- Basic Search Input with searchbox role -->
<div class="search-container">
<label for="site-search" id="search-label">Search this site</label>
<input
type="text"
id="site-search"
role="searchbox"
aria-labelledby="search-label"
placeholder="Search..."
/>
<button type="submit" aria-label="Submit search">
<svg aria-hidden="true"><!-- search icon --></svg>
</button>
</div>
<!-- Note: The role="searchbox" explicitly indicates search functionality -->Native HTML Search (Preferred)
<!-- Native HTML Search Input (Preferred) -->
<form role="search" action="/search" method="GET">
<label for="search-input">Search</label>
<input
type="search"
id="search-input"
name="q"
placeholder="Search articles..."
autocomplete="off"
/>
<button type="submit">Search</button>
</form>
<!-- Benefits of type="search":
- Built-in clear button in many browsers
- Correct virtual keyboard on mobile
- Semantic meaning for assistive tech
- Native form submission support -->Searchbox with Autocomplete
<!-- Searchbox with Autocomplete Suggestions -->
<div class="search-wrapper">
<label for="search" id="search-label">Search products</label>
<input
type="text"
id="search"
role="searchbox"
aria-labelledby="search-label"
aria-autocomplete="list"
aria-controls="search-suggestions"
aria-expanded="true"
aria-activedescendant="suggestion-2"
/>
<ul
id="search-suggestions"
role="listbox"
aria-label="Search suggestions"
>
<li role="option" id="suggestion-1" aria-selected="false">
Wireless headphones
</li>
<li role="option" id="suggestion-2" aria-selected="true">
Wireless keyboard
</li>
<li role="option" id="suggestion-3" aria-selected="false">
Wireless mouse
</li>
</ul>
</div>
<script>
const input = document.getElementById('search');
const listbox = document.getElementById('search-suggestions');
let currentIndex = -1;
input.addEventListener('keydown', (e) => {
const options = listbox.querySelectorAll('[role="option"]');
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
currentIndex = Math.min(currentIndex + 1, options.length - 1);
updateSelection(options);
break;
case 'ArrowUp':
e.preventDefault();
currentIndex = Math.max(currentIndex - 1, 0);
updateSelection(options);
break;
case 'Enter':
if (currentIndex >= 0) {
e.preventDefault();
selectOption(options[currentIndex]);
}
break;
case 'Escape':
closeSuggestions();
break;
}
});
function updateSelection(options) {
options.forEach((opt, i) => {
opt.setAttribute('aria-selected', i === currentIndex);
});
if (currentIndex >= 0) {
input.setAttribute('aria-activedescendant', options[currentIndex].id);
}
}
</script>Live Search with Announcements
<!-- Live Search with Results Announcement -->
<div role="search" aria-label="Site search">
<input
type="search"
role="searchbox"
aria-label="Search articles"
aria-describedby="search-status"
placeholder="Type to search..."
/>
<!-- Live region for announcing results -->
<div
id="search-status"
role="status"
aria-live="polite"
aria-atomic="true"
class="sr-only"
>
<!-- Dynamically updated with results count -->
</div>
<div id="search-results" aria-label="Search results">
<!-- Results rendered here -->
</div>
</div>
<script>
const searchInput = document.querySelector('[role="searchbox"]');
const status = document.getElementById('search-status');
let debounceTimer;
searchInput.addEventListener('input', (e) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
performSearch(e.target.value);
}, 300);
});
function performSearch(query) {
// Perform search...
const results = searchAPI(query);
// Announce results to screen readers
if (results.length === 0) {
status.textContent = 'No results found for ' + query;
} else if (results.length === 1) {
status.textContent = '1 result found';
} else {
status.textContent = results.length + ' results found';
}
renderResults(results);
}
</script>React Component
// React Search Component with Accessibility
import { useState, useRef, useCallback, useEffect } from 'react';
interface Suggestion {
id: string;
text: string;
}
interface SearchboxProps {
label: string;
placeholder?: string;
onSearch: (query: string) => void;
suggestions?: Suggestion[];
onSuggestionSelect?: (suggestion: Suggestion) => void;
}
function Searchbox({
label,
placeholder = 'Search...',
onSearch,
suggestions = [],
onSuggestionSelect,
}: SearchboxProps) {
const [query, setQuery] = useState('');
const [isOpen, setIsOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(-1);
const [announcement, setAnnouncement] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
const showSuggestions = isOpen && suggestions.length > 0;
useEffect(() => {
if (suggestions.length > 0 && query) {
setAnnouncement(
`${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'} available`
);
}
}, [suggestions, query]);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (!showSuggestions) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setActiveIndex(prev =>
prev < suggestions.length - 1 ? prev + 1 : 0
);
break;
case 'ArrowUp':
e.preventDefault();
setActiveIndex(prev =>
prev > 0 ? prev - 1 : suggestions.length - 1
);
break;
case 'Enter':
if (activeIndex >= 0) {
e.preventDefault();
handleSelect(suggestions[activeIndex]);
}
break;
case 'Escape':
setIsOpen(false);
setActiveIndex(-1);
break;
}
}, [showSuggestions, activeIndex, suggestions]);
const handleSelect = (suggestion: Suggestion) => {
setQuery(suggestion.text);
setIsOpen(false);
setActiveIndex(-1);
onSuggestionSelect?.(suggestion);
inputRef.current?.focus();
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSearch(query);
setIsOpen(false);
};
return (
<form role="search" onSubmit={handleSubmit} className="search-form">
{/* Screen reader announcements */}
<div className="sr-only" aria-live="polite" aria-atomic="true">
{announcement}
</div>
<label htmlFor="search-input" className="sr-only">
{label}
</label>
<div className="search-input-wrapper">
<input
ref={inputRef}
id="search-input"
type="search"
role="searchbox"
value={query}
onChange={(e) => {
setQuery(e.target.value);
setIsOpen(true);
setActiveIndex(-1);
}}
onFocus={() => query && setIsOpen(true)}
onBlur={() => setTimeout(() => setIsOpen(false), 200)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
aria-autocomplete="list"
aria-controls={showSuggestions ? 'suggestions' : undefined}
aria-expanded={showSuggestions}
aria-activedescendant={
activeIndex >= 0 ? `suggestion-${activeIndex}` : undefined
}
autoComplete="off"
/>
<button type="submit" aria-label="Submit search">
๐
</button>
</div>
{showSuggestions && (
<ul id="suggestions" role="listbox" aria-label="Search suggestions">
{suggestions.map((suggestion, index) => (
<li
key={suggestion.id}
id={`suggestion-${index}`}
role="option"
aria-selected={index === activeIndex}
onClick={() => handleSelect(suggestion)}
className={index === activeIndex ? 'active' : ''}
>
{suggestion.text}
</li>
))}
</ul>
)}
</form>
);
}
// Usage
<Searchbox
label="Search articles"
placeholder="Search for topics..."
onSearch={(query) => console.log('Searching:', query)}
suggestions={[
{ id: '1', text: 'Getting started' },
{ id: '2', text: 'API reference' },
]}
onSuggestionSelect={(s) => console.log('Selected:', s)}
/>Required Field with Validation
<!-- Required Search Field with Error Handling -->
<form role="search" novalidate>
<label for="search-required">
Search query <span aria-hidden="true">*</span>
</label>
<input
type="search"
id="search-required"
role="searchbox"
aria-required="true"
aria-invalid="true"
aria-describedby="search-error search-hint"
placeholder="Enter search term..."
/>
<span id="search-hint" class="hint">
Enter at least 2 characters
</span>
<span id="search-error" class="error" role="alert">
Please enter a search term
</span>
<button type="submit">Search</button>
</form>
<style>
[aria-invalid="true"] {
border-color: #dc2626;
}
.error {
color: #dc2626;
font-size: 0.875rem;
}
</style>Keyboard Support
Searchbox inherits keyboard behavior from textbox, with additional keys for autocomplete navigation:
When list is open
Wraps to last when at first
Behavior depends on context
Returns focus to input
May close suggestions
Best Practices
Use <input type="search"> when possible for native browser support
Wrap search forms in a landmark with role="search"
Announce results count to screen reader users via live region
Use aria-autocomplete and aria-controls for suggestion lists
Provide clear visual indication when suggestions are available
Debounce search requests to avoid overwhelming users with announcements
Don't auto-submit search on every keystroke without user preference
Don't hide the search button - some users rely on it
Don't forget to handle empty search submissions gracefully
Don't trap keyboard focus in autocomplete suggestions
Supported ARIA Attributes
aria-autocompleteIndicates autocomplete behavior (none, list, both, inline)
aria-controlsReferences the suggestions listbox
aria-expandedWhether the suggestions list is visible
aria-activedescendantID of the currently focused suggestion
aria-labelAccessible name for the searchbox
aria-labelledbyReferences element(s) that label the searchbox
aria-describedbyReferences additional description or hints
aria-requiredIndicates if search input is required
aria-invalidIndicates validation state
aria-placeholderPlaceholder hint (use HTML placeholder attribute instead)
Common Use Cases
Accessibility Notes
Search Landmark
Wrap your search form in a role="search" landmark. This allows screen reader users to quickly navigate to the search functionality from anywhere on the page.
Results Announcements
Use an aria-live region to announce search results count. This helps screen reader users know their search was successful without having to navigate away from the input.
Mobile Considerations
Using <input type="search"> triggers the search keyboard on mobile devices, which includes a "Search" button instead of "Enter". This provides better UX for mobile users.

