This doc will walk through building a job search experience with custom result cards, facets, custom styling, and Search Headless hooks. It continues from Get Started with Search UI React so complete that before starting here.
Step 1: Switch Verticals
In src/templates/search.tsx, change the verticalKey in your headlessConfig from "faqs" to "jobs":
// src/templates/search.tsx
const headlessConfig: HeadlessConfig = {
apiKey: "Your API Key Here",
experienceKey: "turtlehead",
locale: "en",
verticalKey: "jobs",
environment: Environment.SANDBOX,
};
Run npm run dev if your application isn't already running. Searching for "waiter" or "chef" will now return jobs with name and description displayed on each card.
Step 2: Add a Heading and Search Bar Placeholder
Add a heading and placeholder text to the SearchBar:
const Search: Template<TemplateRenderProps> = () => {
return (
<SearchHeadlessProvider searcher={searcher}>
<div className="px-4 py-8">
<div className="mx-auto flex max-w-5xl flex-col">
<h1 className="pb-4 text-center text-3xl font-bold text-red-700">
Turtlehead Tacos Careers
</h1>
<SearchBar placeholder="Search job title, department, or employment type" />
<SpellCheck />
<ResultsCount />
<VerticalResults
CardComponent={StandardCard}
displayAllOnNoResults={false}
/>
</div>
<Pagination />
</div>
</SearchHeadlessProvider>
);
};
Step 3: Search Result Types
Search results represent Knowledge Graph entities and can take any shape. Rather than defining your own TypeScript interfaces, use the Yext CLI to generate types:
yext types generate search src/types --experienceKey turtlehead
This generates a new file for each vertical in your search experience inside the src/types folder.
Step 4: Create a Custom Job Card
Create a new folder src/components with a new file JobCard.tsx:
// src/components/JobCard.tsx
import { CardComponent, CardProps } from "@yext/search-ui-react";
import Job from "../types/jobs";
const JobCard: CardComponent<Job> = ({
result,
}: CardProps<Job>): JSX.Element => {
const job = result.rawData;
const formatDate = (date: string): string => {
if (!date) return "";
const dateObj = new Date(date);
const month = dateObj.toLocaleString("default", { month: "long" });
const day = dateObj.getDate();
const year = dateObj.getFullYear();
return `${month} ${day}, ${year}`;
};
return (
<div className="mb-4 justify-between rounded-lg border bg-zinc-100 p-4 text-stone-900 shadow-sm">
<div className="flex flex-col">
<div className="text-lg font-semibold text-red-700">{job.name}</div>
<div>{job.c_jobDepartment}</div>
<div className="flex gap-1">
{job.employmentType && (
<div className="flex rounded bg-gray-600 px-1 text-sm text-gray-100">
{`${job.employmentType}`}
</div>
)}
{job.c_salary && (
<div className="flex rounded bg-gray-600 px-1 text-sm text-gray-100">
{`$${job.c_salary}/hour`}
</div>
)}
</div>
<div className="py-2">{job.description}</div>
{job.datePosted && (
<div className="text-sm">{`Date Posted: ${formatDate(job.datePosted)}`}</div>
)}
</div>
</div>
);
};
export default JobCard;
Let’s review what you just added:
-
JobCardis aCardComponentwhich is a generic type. This means when theCardPropsare providedJobas a type parameter,result.rawDatawill be typed as aJob. - In addition to the Job
nameanddescription, you’re also displaying thec_jobDepartment,employmentType,c_salary, and formatteddatePostedfield. - You’ve also added some custom styling to your card. In future units, you’ll add some custom styles to other components to match.
Step 5: Add the Custom Card to VerticalResults
In search.tsx, import JobCard and pass it as the CardComponent prop to VerticalResults:
// src/templates/search.tsx
import JobCard from "../components/JobCard";
import Job from "../types/jobs";
const Search: Template<TemplateRenderProps> = () => {
return (
<SearchHeadlessProvider searcher={searcher}>
<div className="px-4 py-8">
<div className="mx-auto flex max-w-5xl flex-col">
<h1 className="pb-4 text-center text-3xl font-bold text-red-700">
Turtlehead Tacos Careers
</h1>
<SearchBar placeholder="Search job title, department, or employment type" />
<SpellCheck />
<ResultsCount />
<VerticalResults<Job>
CardComponent={JobCard}
displayAllOnNoResults={false}
/>
</div>
<Pagination />
</div>
</SearchHeadlessProvider>
);
};
Refresh the page and run a new search to see your custom Job results.

Step 6: Add Facets
In src/templates/search.tsx, import Facets, StandardFacet, and NumericalFacet from @yext/search-ui-react.
// src/templates/search.tsx
import {
SearchBar,
VerticalResults,
SpellCheck,
ResultsCount,
Pagination,
Facets,
} from "@yext/search-ui-react";
const Search: Template<TemplateRenderProps> = () => {
return (
<SearchHeadlessProvider searcher={searcher}>
<div className="px-4 py-8">
<div className="mx-auto flex max-w-5xl flex-col">
<h1 className="pb-4 text-center text-3xl font-bold text-red-700">
Turtlehead Tacos Careers
</h1>
<SearchBar placeholder="Search job title, department, or employment type" />
<SpellCheck />
<ResultsCount />
<div className="flex">
<div className="mr-5 w-56 shrink-0">
<div className="flex flex-col rounded border bg-zinc-100 p-4 shadow-sm">
<Facets />
</div>
</div>
<VerticalResults<Job>
CardComponent={JobCard}
displayAllOnNoResults={false}
/>
</div>
</div>
<Pagination />
</div>
</SearchHeadlessProvider>
);
};
Search for "jobs" and use the facets to filter by Employment Type, Job Department, and Salary.

Step 7: Style Components with customCssClasses
Every Search UI React component can be customized with the customCssClasses prop. Styles passed in this object override the default component styles. Under the hood, components use tailwind-merge to ensure Tailwind CSS classes merge with no conflicts.
Add customCssClasses to SpellCheck, Facets, and Pagination:
const Search: Template<TemplateRenderProps> = () => {
return (
<SearchHeadlessProvider searcher={searcher}>
<div className="px-4 py-8">
<div className="mx-auto flex max-w-5xl flex-col">
<h1 className="pb-4 text-center text-3xl font-bold text-red-700">
Turtlehead Tacos Careers
</h1>
<SearchBar placeholder="Search job title, department, or employment type" />
<SpellCheck
customCssClasses={{
link: "text-red-700 underline",
}}
/>
<ResultsCount />
<div className="flex">
<div className="mr-5 w-56 shrink-0">
<div className="flex flex-col rounded border bg-zinc-100 p-4 shadow-sm">
<Facets
customCssClasses={{
optionInput: "text-red-700 focus:ring-red-700",
optionLabel: "text-stone-900",
}}
/>
</div>
</div>
<VerticalResults<Job>
CardComponent={JobCard}
displayAllOnNoResults={false}
/>
</div>
</div>
<Pagination
customCssClasses={{
icon: "text-stone-900",
label: "text-stone-900",
selectedLabel: "text-red-700 border-red-700 bg-red-100",
}}
/>
</div>
</SearchHeadlessProvider>
);
};
What the styling changes do:
-
SpellChecklink matches the red heading color with an underline. -
Facetslabel text matches theJobCardcolor and checkboxes have a red ring when selected. - The selected page in
Paginationhas a red border and light red background.



Step 8: Use Search Hooks
Under the hood, Search UI components use SearchHeadless hooks to read the search state and trigger actions. You can import and use these same hooks directly in custom components — as long as the component is wrapped in a SearchHeadlessProvider.
Refactor to a JobSearch Component
Create a new file src/components/JobSearch.tsx and move the search UI code out of search.tsx into it:
// src/components/JobSearch.tsx
import {
SearchBar,
SpellCheck,
ResultsCount,
Facets,
VerticalResults,
Pagination,
} from "@yext/search-ui-react";
import JobCard from "./JobCard";
import Job from "../types/jobs";
const JobSearch = (): JSX.Element => {
return (
<div className="px-4 py-8">
<div className="mx-auto flex max-w-5xl flex-col">
<h1 className="pb-4 text-center text-3xl font-bold text-red-700">
Turtlehead Tacos Careers
</h1>
<SearchBar placeholder="Search job title, department, or employment type" />
<SpellCheck
customCssClasses={{
link: "text-red-700 underline",
}}
/>
<ResultsCount />
<div className="flex">
<div className="mr-5 w-56 shrink-0">
<div className="flex flex-col rounded border bg-zinc-100 p-4 shadow-sm">
<Facets
customCssClasses={{
optionInput: "text-red-700 focus:ring-red-700",
optionLabel: "text-stone-900",
}}
/>
</div>
</div>
<VerticalResults<Job>
CardComponent={JobCard}
displayAllOnNoResults={false}
/>
</div>
</div>
<Pagination
customCssClasses={{
icon: "text-stone-900",
label: "text-stone-900",
selectedLabel: "text-red-700 border-red-700 bg-red-100",
}}
/>
</div>
);
};
export default JobSearch;
Update search.tsx to render JobSearch:
// src/templates/search.tsx
import JobSearch from "../components/JobSearch";
const Search: Template<TemplateRenderProps> = () => {
const searcher = provideHeadless(headlessConfig);
return (
<SearchHeadlessProvider searcher={searcher}>
<JobSearch />
</SearchHeadlessProvider>
);
};
export default Search;
You can also remove the unused @yext/search-ui-react imports from search.tsx. Run a search on "jobs" to confirm everything still works.
Add a "No Results" Message
Use useSearchState to read the most recent search query and results count, then conditionally render a message when there are no results:
// src/components/JobSearch.tsx
import { useSearchState } from "@yext/search-headless-react";
const JobSearch = (): JSX.Element => {
const mostRecentSearch = useSearchState(
(state) => state.query.mostRecentSearch
);
const resultsCount =
useSearchState((state) => state.vertical.resultsCount) ?? 0;
return (
<div className="px-4 py-8">
<div className="mx-auto flex max-w-5xl flex-col">
<h1 className="pb-4 text-center text-3xl font-bold text-red-700">
Turtlehead Tacos Careers
</h1>
<SearchBar placeholder="Search job title, department, or employment type" />
<SpellCheck
customCssClasses={{
link: "text-red-700 underline",
}}
/>
<ResultsCount />
{resultsCount > 0 && (
<div className="flex">
<div className="mr-5 w-56 shrink-0">
<div className="flex flex-col rounded border bg-zinc-100 p-4 shadow-sm">
<Facets
customCssClasses={{
optionInput: "text-red-700 focus:ring-red-700",
optionLabel: "text-stone-900",
}}
/>
</div>
</div>
<VerticalResults<Job>
CardComponent={JobCard}
displayAllOnNoResults={false}
/>
</div>
)}
{mostRecentSearch && resultsCount === 0 && (
<div>
<p>
The search
<span className="mx-1 font-semibold">{mostRecentSearch}</span>
did not match any jobs.
</p>
</div>
)}
</div>
<Pagination
customCssClasses={{
icon: "text-stone-900",
label: "text-stone-900",
selectedLabel: "text-red-700 border-red-700 bg-red-100",
}}
/>
</div>
);
};
Search for "developer" — no results should appear, along with an informative message.
Trigger Search on Render
Use useSearchActions wrapped in a useEffect to trigger a blank vertical query when the page loads, so users see jobs immediately on arrival:
// src/components/JobSearch.tsx
import React from "react";
import { useSearchState, useSearchActions } from "@yext/search-headless-react";
const JobSearch = (): JSX.Element => {
const mostRecentSearch = useSearchState(
(state) => state.query.mostRecentSearch
);
const resultsCount =
useSearchState((state) => state.vertical.resultsCount) ?? 0;
const searchActions = useSearchActions();
React.useEffect(() => {
searchActions.executeVerticalQuery();
}, []);
// ...rest of component
};
Refresh your page — job results and facets should now appear immediately without requiring a search query.
Note: To confirm your final code, see the
module-2branch in the pages-starter-search-ui-react repo.