Next.js – Custom Autocomplete Search Filter With Material UI

Table of Contents
Overview
In this article, we will create a complex search field with search results from two different data sources and their categories using React and Material UI. The ability to perform custom searches is a game-changer in any data-driven applications, enabling users to find exactly what they’re looking for in a more efficient and personalized manner. By the end of this tutorial, you’ll have the knowledge and skills to build a powerful search component that empowers your users with granular search options and unlocks the full potential of your application. So, let’s dive in and explore the advantages of custom searches and how to implement them in React using Material UI!
Using the React Material UI library, create an App Bar with a search field that show results as you type. The search bar is available at Material UI Website.
Source files for this example.
GitHubLink to JSON data files here

Package.json
Libraries used in this project
{
"name": "nextjs",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3000",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@emotion/cache": "^11.10.5",
"@emotion/react": "^11.10.6",
"@emotion/server": "^11.10.0",
"@emotion/styled": "^11.10.6",
"@mui/icons-material": "^5.11.11",
"@mui/material": "^5.11.15",
"@next/font": "^13.2.4",
"eslint": "8.35.0",
"eslint-config-next": "13.2.4",
"next": "13.2.4",
"nextjs-progressbar": "^0.0.16",
"react": "18.2.0",
"react-dom": "18.2.0",
"swr": "^2.1.1"
},
"devDependencies": {
"@types/node": "18.15.11",
"@types/react": "18.0.31",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.21",
"typescript": "5.1.3"
}
}
Next.js API
This API gets data from two local JSON files under the ‘./data’ folder. Then returns both files into JSON data.

Source code for pages/api/getData.js
import path from 'path';
import { promises as fs } from 'fs';
export default async function handler(req, res) {
//Find the absolute path of the json directory
const jsonDirectory = path.join(process.cwd(), 'data');
//Read the json data file data.json
const albumData = await fs.readFile(jsonDirectory + '/albums.json', 'utf8');
const postData = await fs.readFile(jsonDirectory + '/posts.json', 'utf8');
const allData = { albums: JSON.parse(albumData), posts: JSON.parse(postData) };
//Return the content of the data file in json format
res.status(200).json(allData);
}
Root Page
This page component has state variables that will be available for all components with the createContext() and useContext() hooks.
The data source is static JSON available at the bottom of the article. Here is a link to the data sources and the full root page component here. Not shown in the source code below due to space constraints.
Note: In the GitHub example there are two additional features available that are standard on many Next.js applications. One is a custom theme that can switch from Light to Dark. Also, a page progress bar that runs along the top of the main section using <NextNProgress/>.
Source code for _app.js
import * as React from 'react';
import PropTypes from 'prop-types';
import Head from 'next/head';
import CssBaseline from '@mui/material/CssBaseline';
import { CacheProvider } from '@emotion/react';
import createEmotionCache from '../src/createEmotionCache';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { red, grey } from '@mui/material/colors';
import AppLayout from '../components/layout';
import NextNProgress from 'nextjs-progressbar';
import '../styles/globals.css';
import { createContext, Fragment, useState } from "react";
import useSWR from 'swr';
// Fetcher function returns api call to json format
const fetcher = (url) => fetch(url).then((res) => res.json());
export const RootCompContext = createContext(null);
// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();
export default function MainApp(props) {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
const { data, error } = useSWR('/api/getData', fetcher);
const [darkState, setDarkState] = React.useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [filteredResults, setFilteredResults] = useState([]);
const [historyResults, setHistoryResults] = useState([]);
const [suggestionResults, setSuggestionResults] = useState([]);
const [maxRecordsReturned ] = useState(10);
const [selectProduct, setSelectProduct] = useState(null);
const [postsData, setPostsData] = useState([]);
const [albumData, setAlbumData] = useState([]);
const [filteredPostsData, setFilteredPostsData] = useState([]);
const [filteredAlbumsData, setFilteredAlbumsData] = useState([]);
const [filteredResultsCategories, setFilteredResultsCategories] = useState([]);
React.useEffect(() => {
if(data) {
setPostsData(data.posts);
setAlbumData(data.albums);
}
}, [data])
const handleThemeChange = () => {
setDarkState(!darkState);
};
return (
<CacheProvider value={emotionCache}>
<Head>
<meta name="viewport" content="initial-scale=1, width=device-width" />
</Head>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<NextNProgress
color={theme.palette.mode==='light'?'#CD6155': '#CD6155'}
startPosition={0.3}
stopDelayMs={200}
height={10}
showOnShallow={true}
/>
<RootCompContext.Provider
value={{
searchTerm,
setSearchTerm,
filteredResults,
setFilteredResults,
historyResults,
setHistoryResults,
suggestionResults,
setSuggestionResults,
selectProduct,
setSelectProduct,
maxRecordsReturned,
postsData,
albumData,
filteredPostsData,
setFilteredPostsData,
filteredAlbumsData,
setFilteredAlbumsData,
filteredResultsCategories,
setFilteredResultsCategories
}}
>
<AppLayout handleThemeChange={handleThemeChange} darkState={darkState} mainPage={
<>
<Component {...pageProps} />
</>}
/>
</RootCompContext.Provider>
</CacheProvider>
);
}
MainApp.propTypes = {
Component: PropTypes.elementType.isRequired,
emotionCache: PropTypes.object,
pageProps: PropTypes.object.isRequired,
allProducts: PropTypes.array.isRequired,
};
Layout Component
The Layout function component is derived from the Search & App Bar in the Material UI library. THe component below renders the layout which includes a MUI <AppBar/> and <Drawer/> components. The <AppBar/> use MUI’s styled to build a custom toolbar with flex to space everything in a way that is more aesthetically pleasing.

Source for Layout.js
import { useState, useEffect, useContext, createContext } from 'react';
import AppBarMenu from './AppBarMenu';
import InputBase from '@mui/material/InputBase';
import Toolbar from '@mui/material/Toolbar';
import MuiAppBar from '@mui/material/AppBar';
import AppBar from '@mui/material/AppBar';
import Typography from '@mui/material/Typography';
import useMediaQuery from '@mui/material/useMediaQuery';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import Box from '@mui/material/Box';
import { useRouter } from 'next/router'
import Container from '@mui/material/Container';
import Link from 'next/link'
import Button from '@mui/material/Button';
import Drawer from '@mui/material/Drawer';
import { alpha, styled, useTheme } from '@mui/material/styles';
import { Divider } from '@mui/material';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import MenuIcon from '@mui/icons-material/Menu';
import HomeIcon from '@mui/icons-material/Home'
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest';
import LayoutSearchField from './search/LayoutSearchField';
const drawerWidth = 240;
const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })(
({ theme, open }) => ({
flexGrow: 1,
padding: theme.spacing(3),
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
//marginLeft: `-${drawerWidth}px`,
marginLeft: `10px`,
...(open && {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
marginLeft: 250,
}),
}));
const MUIAppBar = styled(MuiAppBar, {
shouldForwardProp: (prop) => prop !== 'open',
})(({ theme, open }) => ({
transition: theme.transitions.create(['margin', 'width'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
...(open && {
width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(['margin', 'width'], {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
}),
opacity: 1.0,
zIndex: 999,
}));
const DrawerHeader = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
padding: theme.spacing(0, 1),
// necessary for content to be below app bar
...theme.mixins.toolbar,
justifyContent: 'flex-end',
}));
const publicNavigation = [
{ name: 'Home', href: '/', icon: <HomeIcon/>, current: true },
{ name: 'My Settings', href: '/settings', icon: <SettingsSuggestIcon/>, current: false },
]
const ColorModeContext = createContext({ toggleColorMode: () => {} });
export default function AppLayout(props) {
const theme = useTheme();
const isMdUp = useMediaQuery(theme.breakpoints.up("md"));
const router = useRouter()
const [open, setOpen] = useState(isMdUp? true : false);
const colorMode = useContext(ColorModeContext);
const [showMessageDialog, setShowMessageDialog] = useState(false);
useEffect(() => {
setOpen(isMdUp? true : false);
}, [isMdUp])
const handleDrawerOpen = () => {
setOpen(true);
};
const handleDrawerClose = () => {
setOpen(false);
};
return (
<>
<MUIAppBar
position="fixed"
open={open}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
onClick={handleDrawerOpen}
edge="start"
sx={{ mr: 2, ...(open && { display: 'none' }) }}
>
<MenuIcon />
</IconButton>
<Box sx={{ flexGrow: 1 }} />
<LayoutSearchField/>
<Box sx={{ flexGrow: 1 }} />
<>
<Tooltip
arrow
title={
<>
<Typography color="inherit"><b>Sign In</b></Typography>
{'Use Google or Github to sign in'}
</>
}>
<Button variant='outlined' color="inherit"
startIcon={<AccountCircleIcon/>}
component={Link}
// onClick={(e) => {e.preventDefault(); signIn()}}
href={'/login'}
sx={{ml: 2}}>Sign In
</Button>
</Tooltip>
</>
<>
<AppBarMenu handleDrawerOpen={handleDrawerOpen} session={''}/>
</>
</Toolbar>
</MUIAppBar>
<Main open={open}>
<DrawerHeader />
<Container maxWidth="false" sx={{marginTop: '10px'}}>
{props.mainPage}
</Container>
</Main>
<Drawer
sx={{
width: drawerWidth,
flexShrink: 0,
'& .MuiDrawer-paper': {
width: drawerWidth,
boxSizing: 'border-box',
},
}}
variant="persistent"
anchor="left"
open={open}
>
<DrawerHeader>
<IconButton onClick={handleDrawerClose}>
{theme.direction === 'ltr' ? <ChevronLeftIcon /> : <ChevronRightIcon />}
</IconButton>
</DrawerHeader>
<Divider />
<List>
{publicNavigation.map((item, index) => (
<>
<ListItem key={index} component={Link} href={item.href} disablePadding>
<ListItemButton>
<ListItemIcon style={{color: router.asPath === item.href? 'blue' : 'black'}}>
{item.icon}
</ListItemIcon>
<ListItemText primary={item.name} style={{color: router.asPath === item.href? 'blue' : 'black'}} />
</ListItemButton>
</ListItem>
</>
))}
</List>
</Drawer>
</>
);
}
Layout Search Field Component
The Layout Search Field component uses MUI’s styled function to build a custom search field with icons that has an embedded look to blend into the <AppBar/>.
Source for SearchFieldComponent.js
import * as React from "react";
import { useContext } from "react";
import {RootCompContext} from "@/pages/_app";
import { alpha, styled } from '@mui/material/styles';
import InputBase from "@mui/material/InputBase";
import SearchIcon from "@mui/icons-material/Search";
import { Box, Stack } from "@mui/material";
import ClickAwayListener from "@mui/base/ClickAwayListener";
import { useRouter } from 'next/router'
import SearchResultsGrid from "./SearchResultsGrid";
const Search = styled('div')(({ theme }) => ({
position: 'relative',
borderRadius: theme.shape.borderRadius,
backgroundColor: alpha(theme.palette.common.white, 0.15),
'&:hover': {
backgroundColor: alpha(theme.palette.common.white, 0.25),
},
marginRight: theme.spacing(2),
marginLeft: 0,
width: '100%',
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing(3),
width: 'auto',
},
[theme.breakpoints.down('sm')]: {
display: 'none',
marginLeft: theme.spacing(3),
width: 'auto',
},
}));
const SearchIconWrapper = styled('div')(({ theme }) => ({
padding: theme.spacing(0, 2),
height: '100%',
position: 'absolute',
pointerEvents: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}));
const StyledInputBase = styled(InputBase)(({ theme }) => ({
color: 'inherit',
'& .MuiInputBase-input': {
padding: theme.spacing(1, 1, 1, 0),
// vertical padding + font size from searchIcon
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
transition: theme.transitions.create('width'),
width: '100%',
[theme.breakpoints.up('md')]: {
width: '80ch',
},
},
}));
export default function LayoutSearchField(props) {
const router = useRouter()
const [showResults, setShowResults] = React.useState(false);
const {
searchTerm,
setSearchTerm,
postsData,
albumData,
setFilteredPostsData,
setFilteredAlbumsData,
setFilteredResultsCategories
} = useContext(RootCompContext);
const { selectProduct, setSelectProduct } = useContext(RootCompContext);
const handleSearchTerm = (e) => {
setSearchTerm(e.target.value);
var tmpPostsData = [];
var tmpAlbumsData = [];
var tmpCategoriesData = [];
if (e.target.value.length > 0) {
postsData.forEach((element) => {
if (
element.title
.toLowerCase()
.includes(e.target.value.toLowerCase())
) {
console.log('FOUND', element.title.toLowerCase())
tmpPostsData.push(element);
if(!tmpCategoriesData.includes(element.category)){
tmpCategoriesData.push(element.category)
}
}
});
albumData.forEach((element) => {
if (
element.title
.toLowerCase()
.includes(e.target.value.toLowerCase())
) {
tmpAlbumsData.push(element);
if(!tmpCategoriesData.includes(element.category)){
tmpCategoriesData.push(element.category)
}
}
});
setFilteredResultsCategories(tmpCategoriesData)
setFilteredPostsData(tmpPostsData);
setFilteredAlbumsData(tmpAlbumsData);
}
};
const handleOpenSearchResults = () => {
setShowResults(true);
};
return (
<>
<Box
alignContent="center"
sx={{ position: "absolute", top: "1vh", left: "4vw", zIndex: 99 }}
>
<ClickAwayListener onClickAway={() => setShowResults(false)}>
<Box textAlign="left" sx={{ width: "50vw", position: "relative" }}>
<Stack spacing={0}>
<Search>
<SearchIconWrapper>
<SearchIcon />
</SearchIconWrapper>
<StyledInputBase
size="small"
sx={{ ml: 1, flex: 1 }}
placeholder="Search Products"
inputProps={{ "aria-label": "search google maps" }}
onChange={(e) => handleSearchTerm(e)}
onFocus={() => handleOpenSearchResults()}
value={searchTerm}
/>
</Search>
<SearchResultsGrid
showResults={showResults}
searchTerm={searchTerm}
/>
</Stack>
</Box>
</ClickAwayListener>
</Box>
</>
);
}
Custom Autocomplete Search Filter
The code snippet below creates a box section that is 50vw or 1/2 the view width. Within the box there is a styled InputBase component with search icon prepened and the a close icon appended to the field. The inputs and icons are styled and aligned using MUI’s styled from @mui/material/styles. The <SearchResultGrid /> component is visible when input field is clicked on or has focus and if any characters are typed.. This will stay within 5-10% of the search input field width. The section appears
<Box
alignContent="center"
sx={{ position: "absolute", top: "1vh", left: "4vw", zIndex: 99 }}
>
<ClickAwayListener onClickAway={() => setShowResults(false)}>
<Box textAlign="left" sx={{ width: "50vw", position: "relative" }}>
<Stack spacing={0}>
<Search>
<SearchIconWrapper>
<SearchIcon />
</SearchIconWrapper>
<StyledInputBase
size="small"
sx={{ ml: 1, flex: 1 }}
placeholder="Search Products"
inputProps={{ "aria-label": "search google maps" }}
onChange={(e) => handleSearchTerm(e)}
onFocus={() => handleOpenSearchResults()}
value={searchTerm}
/>
</Search>
<SearchResultsGrid
showResults={showResults}
searchTerm={searchTerm}
/>
</Stack>
</Box>
</ClickAwayListener>
</Box>
Search Grid Results Component
This is where the magic happens. Using the MUI’s <Box/> component, the search results can drop down below the search field in a responsive outlined area (Snapshot below). Having a custom search filter, multiple results can be displayed along with suggestions or categories.

import * as React from "react";
import { styled } from "@mui/material/styles";
import Box from "@mui/material/Box";
import Paper from "@mui/material/Paper";
import Grid from "@mui/material/Grid";
import SearchResultList from "./PostSearchResultList";
import SearchHistoryList from "./SearchHistoryList";
import AlbumSearchResultList from './AlbumSearchResultList'
import Chip from '@mui/material/Chip';
import Stack from '@mui/material/Stack';
import { useContext } from 'react'
import { RootCompContext } from "@/pages/_app";
const Item = styled(Paper)(({ theme }) => ({
backgroundColor: theme.palette.mode === "dark" ? "#1A2027" : "#fff",
...theme.typography.body2,
padding: theme.spacing(1),
textAlign: "center",
color: theme.palette.text.secondary,
}));
export default function BasicGrid(props) {
console.log("BasicGrid props", props);
const {
filteredPostsData,
setFilteredPostsData,
filteredAlbumsData,
setFilteredAlbumsData,
filteredResultsCategories
} = useContext(RootCompContext);
const [searchTerm, setSearchTerm] = React.useState(null);
console.log("BasicGrid props.searchTerm", props.searchTerm);
console.log("BasicGrid filteredPostsData", filteredPostsData);
console.log("BasicGrid filteredAlbumsData", filteredAlbumsData);
console.log("BasicGrid filteredResultsCategories", filteredResultsCategories);
React.useEffect(() => {
setFilteredPostsData(props.filteredPostsData);
setFilteredAlbumsData(props.filteredAlbumsData);
}, [props.data]);
React.useEffect(() => {
setSearchTerm(props.searchTerm);
}, [props.searchTerm]);
return (
<>
<Grid container spacing={1} columns={16}>
<Grid item xs={16}>
<Box sx={{ '& > :not(style)': { m: 0.5 } }}>
{filteredResultsCategories.map((elem, index) => (
<Chip label={elem} key={index} variant="outlined" color="primary"/>
))}
</Box>
</Grid>
<Grid item xs={8}>
{Boolean(filteredPostsData) &&
<Box style={{margin: 1, maxHeight: '47vh', overflow: 'auto'}}>
{(filteredPostsData.length > 0) && (
<>
<SearchResultList
key={1}
data={filteredPostsData}
handleSelectedProduct={props.handleSelectedProduct}
searchTerm={props.searchTerm}
/>
</>
)}
</Box>
}
</Grid>
<Grid item xs={8}>
{Boolean(filteredAlbumsData) &&
<Box style={{margin: 1, maxHeight: '47vh', overflow: 'auto'}}>
{(filteredAlbumsData.length > 0) && (
<>
<AlbumSearchResultList
key={2}
data={filteredAlbumsData}
handleSelectedProduct={props.handleSelectedProduct}
searchTerm={props.searchTerm}
/>
</>
)}
</Box>
}
</Grid>
</Grid>
</>
);
}
Posts Search Filter List Component
This component is a combination of a CSS generated 3d shadow box that can grow and shrink with a Material List. The list is generated and filtered from the parent class component using posts.json data and passed down to the SearchResultsGrid.js and then the AlbumSearchResultList.js,
Source for PostSearchResultsList.js
import * as React from 'react';
import List from '@mui/material/List';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import People from '@mui/icons-material/People';
import PermMedia from '@mui/icons-material/PermMedia';
import Dns from '@mui/icons-material/Dns';
import Public from '@mui/icons-material/Public';
import { useContext } from 'react'
import { RootCompContext } from "@/pages/_app";
import { Typography } from '@mui/material';
import MessageIcon from '@mui/icons-material/Message';
import Highlighter from "react-highlight-words";
import Link from 'next/link';
const data = [
{ icon: <People />, label: 'Authentication' },
{ icon: <Dns />, label: 'Database' },
{ icon: <PermMedia />, label: 'Storage' },
{ icon: <Public />, label: 'Hosting' },
];
export default function PostSearchResultList(props) {
const [searchTerms, setSearchTerms] = React.useState([])
const {searchTerm,
filteredPostsData,
filteredResults,
setFilteredResults,
maxRecordsReturned,
arrowKeyItemIndex,
setArrowKeyItemIndex,
arrowKeyLateralItemIndex,
setArrowKeyLateralItemIndex,
arrowKeyLateralListIndex,
setArrowKeyLateralListIndex
} = useContext(RootCompContext);
React.useEffect(() => {
var searchStr = []
searchStr.push(`${props.searchTerm}`)
setSearchTerms(searchStr)
}, [props.searchTerm])
const Highlight = ({ children, highlightIndex }) => (
<strong className="highlighted-text">{children}</strong>
);
return (
<>
{(filteredPostsData.length > 0 && searchTerm.length > 0) &&
<>
<Typography variant='subtitle1' sx={{mt: 2, ml: 2, color: 'black'}}>
Post Search Results {arrowKeyItemIndex}
</Typography>
<List sx={{ width: '100%', maxWidth: '100%', bgcolor: 'background.paper' }}>
{filteredPostsData.slice(0, maxRecordsReturned).map((item, index) => (
<ListItemButton
key={item.id}
sx={{ py: 0, minHeight: 42, color: 'rgba(5,5,5,.8)', bgcolor: (arrowKeyLateralListIndex ===0 && arrowKeyItemIndex===index) ? '#EFEFEF' : 'background.paper' }}
// onClick={() => props.handleSelectedProduct(item.id, item.handle, item.title, item.images.edges[0].node.url)}
>
<ListItemIcon sx={{ color: 'inherit' }}>
<MessageIcon/>
</ListItemIcon>
<Highlighter
style={{color: 'black'}}
highlightClassName={Highlight}
searchWords={searchTerms}
autoEscape={true}
textToHighlight={item.title}
/>
</ListItemButton>
))}
</List>
</>
}
</>
);
}
Album Search Filter List Component
This component is a combination of a CSS generated 3d shadow box that can grow and shrink with a Material List. The list is generated and filtered from the parent class component using albums.json data and passed down to the SearchResultsGrid.js and then the AlbumSearchResultList.js,
Source for AlbumSearchResultList.js
import * as React from 'react';
import List from '@mui/material/List';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import AlbumIcon from '@mui/icons-material/Album';
import { useContext } from 'react'
import {RootCompContext} from "@/pages/_app";
import { Typography } from '@mui/material';
import Link from 'next/link';
export default function AlbumSearchResultList(props) {
const [searchTerms, setSearchTerms] = React.useState([])
const {searchTerm, filteredAlbumsData, maxRecordsReturned } = useContext(RootCompContext);
React.useEffect(() => {
var searchStr = []
searchStr.push(`${props.searchTerm}`)
setSearchTerms(searchStr)
}, [props.searchTerm])
const Highlight = ({ children, highlightIndex }) => (
<strong className="highlighted-text">{children}</strong>
);
return (
<>
{(filteredAlbumsData.length > 0 && searchTerm.length > 0) &&
<>
<Typography variant='subtitle1' sx={{mt: 2, ml: 2, color: 'black'}}>
Album Search Results
</Typography>
<List sx={{ width: '100%', maxWidth: '100%', bgcolor: 'background.paper' }}>
{filteredAlbumsData.slice(0, maxRecordsReturned).map((item) => (
<ListItemButton
key={item.id}
sx={{ py: 0, minHeight: 42, color: 'rgba(5,5,5,.8)' }}
// onClick={() => handeSomeEvent())}
>
<ListItemIcon sx={{ color: 'inherit' }}>
<AlbumIcon/>
</ListItemIcon>
{item.title}
</ListItemButton>
))}
</List>
</>
}
</>
);
}
Conclusion
Now that you know how to build a custom search, try adding word highlighting, search history, or a carousel of of images at the bottom or side. To really take it even further, listen for direction keys to navigate the list. Hint for the arrow keys, use the code snippet below. This was instrumental in getting it to work.
The code snippet below was copied from stackoverflow.com authored by Laith
Link to article: https://stackoverflow.com/questions/59546928/keydown-up-events-with-react-hooks-not-working-properly
const useKeyPress = (targetKey) => {
const [keyPressed, setKeyPressed] = React.useState(false);
React.useEffect(() => {
const downHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(true);
}
};
const upHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(false);
}
};
window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);
return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
};
}, [targetKey]);
return keyPressed;
};
You must be logged in to post a comment.