Loading Developer Playground

Loading ...

Skip to main content
Share:
ARIA ROLEโ€ขWidget Roles

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.

Superclass Role
textbox
Native HTML Equivalent
<input type="search">
Common Pattern
role="search" landmark

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:

โ†“
โ†’Opens suggestions list / moves to next suggestion

When list is open

โ†‘
โ†’Moves to previous suggestion

Wraps to last when at first

Enter
โ†’Selects current suggestion or submits search

Behavior depends on context

Escape
โ†’Closes suggestions list

Returns focus to input

Tab
โ†’Moves focus to next element

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-autocomplete

Indicates autocomplete behavior (none, list, both, inline)

aria-controls

References the suggestions listbox

aria-expanded

Whether the suggestions list is visible

aria-activedescendant

ID of the currently focused suggestion

aria-label

Accessible name for the searchbox

aria-labelledby

References element(s) that label the searchbox

aria-describedby

References additional description or hints

aria-required

Indicates if search input is required

aria-invalid

Indicates validation state

aria-placeholder

Placeholder hint (use HTML placeholder attribute instead)

Common Use Cases

Site-wide search
Product/catalog search
Documentation search
Address/location lookup
User/contact search
Filter tables and lists
Command palette (Cmd+K)
Tag/category autocomplete

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.

Related Roles & Attributes

Specifications & Resources