Search
The Search is a specialized input field designed for text-based searching on a website. In the provided example, the block includes an additional feature that displays live hints suggestions (autocomplete) as the user types.
The Search input is primarily utilized in the global navigation (see NavBarTop block). However, variants without autocomplete functionality are commonly employed on results pages.
Accessibility notes
The Search fully supports the use of the keyboard. The transition from the search input to the hints list is handled by Tab key.
Basic search
Simple search with an autocomplete functionality. Give your users hints of what they may look for. In this example we use mock autocomplete example, make sure you provide real data.
import { type ChangeEvent, type KeyboardEvent, type FormEvent, useState, useRef } from 'react';
import { useDebounce } from 'react-use';
import { offset } from '@floating-ui/react-dom';
import {
SfInput,
SfIconSearch,
SfIconCancel,
useDisclosure,
SfListItem,
SfLoaderCircular,
useTrapFocus,
useDropdown,
} from '@storefront-ui/react';
interface Product {
id: string;
name: string;
}
const mockProducts: Product[] = [
{ id: 'ip-14', name: 'iPhone 14' },
{ id: 'ip-14-pro', name: 'iPhone 14 Pro' },
{ id: 'ip-14-pro-max', name: 'iPhone 14 Pro Max' },
{ id: 'ip-14-plus', name: 'iPhone 14 Plus' },
{ id: 'ip-13', name: 'iPhone 13' },
{ id: 'ip-13-mini', name: 'iPhone 13 mini' },
{ id: 'ip-12', name: 'iPhone 12' },
{ id: 'ip-11', name: 'iPhone 11' },
{ id: 'mb-air', name: 'MacBook Air' },
{ id: 'mb-pro-13', name: 'MacBook Pro 13"' },
{ id: 'mb-pro-14', name: 'MacBook Pro 14"' },
{ id: 'mb-pro-16', name: 'MacBook Pro 16"' },
];
// Just for presentation purposes. Replace mock request with the actual API call.
// eslint-disable-next-line no-promise-executor-return
const delay = () => new Promise((resolve) => setTimeout(resolve, Math.random() * 1000));
const mockAutocompleteRequest = async (phrase: string) => {
await delay();
const results = mockProducts
.filter((product) => product.name.toLowerCase().startsWith(phrase.toLowerCase()))
.map((product) => {
const highlight = product.name.substring(0, phrase.length);
const rest = product.name.substring(phrase.length);
return { highlight, rest, product };
});
return results;
};
export default function SearchBasic() {
const inputRef = useRef<HTMLInputElement>(null);
const dropdownListRef = useRef<HTMLUListElement>(null);
const [searchValue, setSearchValue] = useState('');
const [isLoadingSnippets, setIsLoadingSnippets] = useState(false);
const [snippets, setSnippets] = useState<{ highlight: string; rest: string; product: Product }[]>([]);
const { isOpen, close, open } = useDisclosure();
const { refs, style } = useDropdown({
isOpen,
onClose: close,
placement: 'bottom-start',
middleware: [offset(4)],
});
const { focusables: focusableElements, updateFocusableElements } = useTrapFocus(dropdownListRef, {
trapTabs: false,
initialFocus: false,
arrowKeysUpDown: true,
activeState: isOpen,
});
const isResetButton = Boolean(searchValue);
const handleSubmit = (event: FormEvent) => {
event.preventDefault();
close();
alert(`Search for phrase: ${searchValue}`);
};
const handleFocusInput = () => {
inputRef.current?.focus();
};
const handleReset = () => {
setSearchValue('');
setSnippets([]);
close();
handleFocusInput();
};
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const phrase = event.target.value;
if (phrase) {
setSearchValue(phrase);
} else {
handleReset();
}
};
const handleSelect = (phrase: string) => () => {
setSearchValue(phrase);
close();
handleFocusInput();
};
const handleInputKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Escape') handleReset();
if (event.key === 'ArrowUp') {
open();
updateFocusableElements();
if (isOpen && focusableElements.length > 0) {
focusableElements[focusableElements.length - 1].focus();
}
}
if (event.key === 'ArrowDown') {
open();
updateFocusableElements();
if (isOpen && focusableElements.length > 0) {
focusableElements[0].focus();
}
}
};
useDebounce(
() => {
if (searchValue) {
const getSnippets = async () => {
open();
setIsLoadingSnippets(true);
try {
const data = await mockAutocompleteRequest(searchValue);
setSnippets(data);
} catch (error) {
close();
console.error(error);
}
setIsLoadingSnippets(false);
};
getSnippets();
}
},
500,
[searchValue],
);
return (
<form role="search" onSubmit={handleSubmit} ref={refs.setReference} className="relative">
<SfInput
ref={inputRef}
value={searchValue}
onChange={handleChange}
onFocus={open}
aria-label="Search"
placeholder="Search 'MacBook' or 'iPhone'..."
onKeyDown={handleInputKeyDown}
slotPrefix={<SfIconSearch />}
slotSuffix={
isResetButton && (
<button
type="reset"
onClick={handleReset}
aria-label="Reset search"
className="flex rounded-md focus-visible:outline focus-visible:outline-offset"
>
<SfIconCancel />
</button>
)
}
/>
{isOpen && (
<div ref={refs.setFloating} style={style} className="left-0 right-0">
{isLoadingSnippets ? (
<div className="flex items-center justify-center w-full h-20 py-2 bg-white border border-solid rounded-md border-neutral-100 drop-shadow-md">
<SfLoaderCircular />
</div>
) : (
snippets.length > 0 && (
<ul
ref={dropdownListRef}
className="py-2 bg-white border border-solid rounded-md border-neutral-100 drop-shadow-md"
>
{snippets.map(({ highlight, rest, product }) => (
<li key={product.id}>
<SfListItem
as="button"
type="button"
onClick={handleSelect(product.name)}
className="flex justify-start"
>
<p className="text-left">
<span>{highlight}</span>
<span className="font-medium">{rest}</span>
</p>
</SfListItem>
</li>
))}
</ul>
)
)}
</div>
)}
</form>
);
}
Search with custom icon
This block enhances search functionality with a custom icon and categorized result suggestions.
import { type ChangeEvent, type FormEvent, type KeyboardEvent, useState, useRef } from 'react';
import { useDebounce } from 'react-use';
import { offset } from '@floating-ui/react-dom';
import {
SfButton,
SfInput,
SfIconSearch,
SfIconCancel,
useDisclosure,
SfListItem,
SfLoaderCircular,
useTrapFocus,
useDropdown,
} from '@storefront-ui/react';
interface Product {
id: string;
name: string;
category: string;
}
const mockProducts: Product[] = [
{ id: 'ip-14', name: 'iPhone 14', category: 'Smartphones' },
{ id: 'ip-14-pro', name: 'iPhone 14 Pro', category: 'Smartphones' },
{ id: 'ip-14-pro-max', name: 'iPhone 14 Pro Max', category: 'Smartphones' },
{ id: 'ip-14-plus', name: 'iPhone 14 Plus', category: 'Smartphones' },
{ id: 'ip-13', name: 'iPhone 13', category: 'Smartphones' },
{ id: 'ip-13-mini', name: 'iPhone 13 mini', category: 'Smartphones' },
{ id: 'ip-12', name: 'iPhone 12', category: 'Smartphones' },
{ id: 'ip-11', name: 'iPhone 11', category: 'Smartphones' },
{ id: 'mb-air', name: 'MacBook Air', category: 'Laptops' },
{ id: 'mb-pro-13', name: 'MacBook Pro 13"', category: 'Laptops' },
{ id: 'mb-pro-14', name: 'MacBook Pro 14"', category: 'Laptops' },
{ id: 'mb-pro-16', name: 'MacBook Pro 16"', category: 'Laptops' },
];
// Just for presentation purposes. Replace mock request with the actual API call.
// eslint-disable-next-line no-promise-executor-return
const delay = () => new Promise((resolve) => setTimeout(resolve, Math.random() * 1000));
const mockAutocompleteRequest = async (phrase: string) => {
await delay();
const results = mockProducts
.filter((product) => product.name.toLowerCase().startsWith(phrase.toLowerCase()))
.map((product) => {
const highlight = product.name.substring(0, phrase.length);
const rest = product.name.substring(phrase.length);
return { highlight, rest, product };
});
return results;
};
export default function SearchWithIcon() {
const inputRef = useRef<HTMLInputElement>(null);
const dropdownListRef = useRef<HTMLUListElement>(null);
const [searchValue, setSearchValue] = useState('');
const [isLoadingSnippets, setIsLoadingSnippets] = useState(false);
const [snippets, setSnippets] = useState<{ highlight: string; rest: string; product: Product }[]>([]);
const { isOpen, close, open } = useDisclosure();
const { refs, style } = useDropdown({
isOpen,
onClose: close,
placement: 'bottom-start',
middleware: [offset(4)],
});
const { focusables: focusableElements, updateFocusableElements } = useTrapFocus(dropdownListRef, {
trapTabs: false,
arrowKeysUpDown: true,
activeState: isOpen,
initialFocus: false,
});
const isResetButton = Boolean(searchValue);
const handleSubmit = (event: FormEvent) => {
event.preventDefault();
close();
alert(`Search for phrase: ${searchValue}`);
};
const handleFocusInput = () => {
inputRef.current?.focus();
};
const handleReset = () => {
setSearchValue('');
setSnippets([]);
close();
handleFocusInput();
};
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const phrase = event.target.value;
if (phrase) {
setSearchValue(phrase);
} else {
handleReset();
}
};
const handleSelect = (phrase: string) => () => {
setSearchValue(phrase);
close();
handleFocusInput();
};
const handleInputKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Escape') handleReset();
if (event.key === 'ArrowUp') {
open();
updateFocusableElements();
if (isOpen && focusableElements.length > 0) {
focusableElements[focusableElements.length - 1].focus();
}
}
if (event.key === 'ArrowDown') {
open();
updateFocusableElements();
if (isOpen && focusableElements.length > 0) {
focusableElements[0].focus();
}
}
};
useDebounce(
() => {
if (searchValue) {
const getSnippets = async () => {
open();
setIsLoadingSnippets(true);
try {
const data = await mockAutocompleteRequest(searchValue);
setSnippets(data);
} catch (error) {
close();
console.error(error);
}
setIsLoadingSnippets(false);
};
getSnippets();
}
},
500,
[searchValue],
);
return (
<form role="search" onSubmit={handleSubmit} ref={refs.setReference} className="relative">
<div className="flex">
<SfInput
ref={inputRef}
value={searchValue}
onChange={handleChange}
onFocus={open}
wrapperClassName="w-full !ring-0 active:!ring-0 hover:!ring-0 focus-within:!ring-0 border-y border-l border-neutral-200 rounded-r-none hover:border-primary-800 active:border-primary-700 active:border-y-2 active:border-l-2 focus-within:border-y-2 focus-within:border-l-2 focus-within:border-primary-700"
aria-label="Search"
placeholder="Search 'MacBook' or 'iPhone'..."
onKeyDown={handleInputKeyDown}
slotPrefix={<SfIconSearch />}
slotSuffix={
isResetButton && (
<button
type="reset"
onClick={handleReset}
aria-label="Reset search"
className="flex rounded-md focus-visible:outline focus-visible:outline-offset"
>
<SfIconCancel />
</button>
)
}
/>
<SfButton type="submit" square aria-label="Search for a specific phrase on the page" className="rounded-l-none">
<SfIconSearch />
</SfButton>
</div>
{isOpen && (
<div ref={refs.setFloating} style={style} className="left-0 right-0">
{isLoadingSnippets ? (
<div className="flex items-center justify-center bg-white w-full h-screen sm:h-20 py-2 sm:border sm:border-solid sm:rounded-md sm:border-neutral-100 sm:drop-shadow-md">
<SfLoaderCircular />
</div>
) : (
snippets.length > 0 && (
<ul
ref={dropdownListRef}
className="py-2 bg-white h-screen sm:h-auto sm:border sm:border-solid sm:rounded-md sm:border-neutral-100 sm:drop-shadow-md"
>
{snippets.map(({ highlight, rest, product }) => (
<li key={product.id}>
<SfListItem
as="button"
type="button"
onClick={handleSelect(product.name)}
className="!py-4 sm:!py-2 flex justify-start"
>
<p className="text-left">
<span>{highlight}</span>
<span className="font-medium">{rest}</span>
</p>
<p className="text-left typography-text-xs text-neutral-500">{product.category}</p>
</SfListItem>
</li>
))}
</ul>
)
)}
</div>
)}
</form>
);
}
Search with custom button
This block enhances search functionality with a custom button, categorized result suggestions and thumbnails.
import { type ChangeEvent, type FormEvent, type KeyboardEvent, useState, useRef } from 'react';
import { useDebounce } from 'react-use';
import { offset } from '@floating-ui/react-dom';
import {
SfButton,
SfInput,
SfIconSearch,
SfIconCancel,
useDisclosure,
SfListItem,
SfLoaderCircular,
useTrapFocus,
useDropdown,
SfIconGridView,
} from '@storefront-ui/react';
interface Product {
id: string;
name: string;
thumbnail?: JSX.Element;
image?: string;
}
const mockProducts: Product[] = [
{ id: 'j-avatar', name: 'jack', image: 'https://storage.googleapis.com/sfui_docs_artifacts_bucket_public/production/kid.png' },
{ id: 'j-cat', name: 'jackets', thumbnail: <SfIconGridView /> },
{ id: 'j-wom', name: 'jacket women', thumbnail: <SfIconSearch /> },
{ id: 'j-den', name: 'jacket denim', thumbnail: <SfIconSearch /> },
{ id: 'j-dre', name: 'jacket dress', thumbnail: <SfIconSearch /> },
{ id: 'dr-cat', name: 'dresses', thumbnail: <SfIconGridView /> },
{ id: 'dr-cot', name: 'cotton dresses', thumbnail: <SfIconSearch /> },
{ id: 'dr-wom', name: 'dresses women', thumbnail: <SfIconSearch /> },
{ id: 'dr-sum', name: 'summer dresses', thumbnail: <SfIconSearch /> },
];
// Just for presentation purposes. Replace mock request with the actual API call.
// eslint-disable-next-line no-promise-executor-return
const delay = () => new Promise((resolve) => setTimeout(resolve, Math.random() * 1000));
const mockAutocompleteRequest = async (phrase: string) => {
await delay();
const results = mockProducts
.filter((product) => product.name.toLowerCase().startsWith(phrase.toLowerCase()))
.map((product) => {
const highlight = product.name.substring(0, phrase.length);
const rest = product.name.substring(phrase.length);
return { highlight, rest, product };
});
return results;
};
export default function SearchWithIcon() {
const inputRef = useRef<HTMLInputElement>(null);
const dropdownListRef = useRef<HTMLUListElement>(null);
const [searchValue, setSearchValue] = useState('');
const [isLoadingSnippets, setIsLoadingSnippets] = useState(false);
const [snippets, setSnippets] = useState<{ highlight: string; rest: string; product: Product }[]>([]);
const { isOpen, close, open } = useDisclosure();
const { refs, style } = useDropdown({
isOpen,
onClose: close,
placement: 'bottom-start',
middleware: [offset(4)],
});
const { focusables: focusableElements, updateFocusableElements } = useTrapFocus(dropdownListRef, {
trapTabs: false,
arrowKeysUpDown: true,
activeState: isOpen,
initialFocus: false,
});
const isResetButton = Boolean(searchValue);
const handleSubmit = (event: FormEvent) => {
event.preventDefault();
close();
alert(`Search for phrase: ${searchValue}`);
};
const handleFocusInput = () => {
inputRef.current?.focus();
};
const handleReset = () => {
setSearchValue('');
setSnippets([]);
close();
handleFocusInput();
};
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const phrase = event.target.value;
if (phrase) {
setSearchValue(phrase);
} else {
handleReset();
}
};
const handleSelect = (phrase: string) => () => {
setSearchValue(phrase);
close();
handleFocusInput();
};
const handleInputKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Escape') handleReset();
if (event.key === 'ArrowUp') {
open();
updateFocusableElements();
if (isOpen && focusableElements.length > 0) {
focusableElements[focusableElements.length - 1].focus();
}
}
if (event.key === 'ArrowDown') {
open();
updateFocusableElements();
if (isOpen && focusableElements.length > 0) {
focusableElements[0].focus();
}
}
};
useDebounce(
() => {
if (searchValue) {
const getSnippets = async () => {
open();
setIsLoadingSnippets(true);
try {
const data = await mockAutocompleteRequest(searchValue);
setSnippets(data);
} catch (error) {
close();
console.error(error);
}
setIsLoadingSnippets(false);
};
getSnippets();
}
},
500,
[searchValue],
);
return (
<form role="search" onSubmit={handleSubmit} ref={refs.setReference} className="relative">
<div className="flex">
<SfInput
ref={inputRef}
value={searchValue}
onChange={handleChange}
onFocus={open}
wrapperClassName="w-full !ring-0 active:!ring-0 hover:!ring-0 focus-within:!ring-0 border-y border-l border-neutral-200 rounded-r-none hover:border-primary-800 active:border-primary-700 active:border-y-2 active:border-l-2 focus-within:border-y-2 focus-within:border-l-2 focus-within:border-primary-700"
aria-label="Search"
placeholder="Search 'jackets' or 'dresses'..."
onKeyDown={handleInputKeyDown}
slotPrefix={<SfIconSearch />}
slotSuffix={
isResetButton && (
<button
type="reset"
onClick={handleReset}
aria-label="Reset search"
className="flex rounded-md focus-visible:outline focus-visible:outline-offset"
>
<SfIconCancel />
</button>
)
}
/>
<SfButton type="submit" className="rounded-l-none">
Search
</SfButton>
</div>
{isOpen && (
<div ref={refs.setFloating} style={style} className="left-0 right-0">
{isLoadingSnippets ? (
<div className="flex items-center justify-center bg-white w-full h-screen sm:h-20 py-2 sm:border sm:border-solid sm:rounded-md sm:border-neutral-100 sm:drop-shadow-md">
<SfLoaderCircular />
</div>
) : (
snippets.length > 0 && (
<ul
ref={dropdownListRef}
className="py-2 bg-white sm:border border-solid sm:rounded-md sm:border-neutral-100 sm:drop-shadow-md"
>
{snippets.map(({ highlight, rest, product }) => (
<li key={product.id}>
<SfListItem
as="button"
type="button"
onClick={handleSelect(product.name)}
className="!py-4 sm:!py-2 flex justify-start"
>
<p className="flex items-center text-left text-neutral-500">
{product.image ? (
<img src={product.image} alt={product.name} className="rounded-sm" width="24" height="24" />
) : (
product.thumbnail
)}
<span className="ml-2 text-neutral-900">{highlight}</span>
<span className="font-medium text-neutral-900">{rest}</span>
</p>
</SfListItem>
</li>
))}
</ul>
)
)}
</div>
)}
</form>
);
}