Filter bar
Active filters as removable chips above results.
People
Northwind directory · 8 people
2 people match your filters
filter-bar.tsx
"use client";
import { useState } from "react";
import { LuBookmark, LuSearchX } from "react-icons/lu";
import {
Avatar,
Badge,
Button,
EmptyState,
FilterChip,
ListItem,
MenuItem,
SearchField,
TableToolbar,
} from "@nexera-ui/react";
// Sample data
interface Person {
id: string;
name: string;
initials: string;
role: string;
team: string;
email: string;
location: string;
status: "Active" | "On leave" | "Remote" | "Invited";
}
const PEOPLE: Person[] = [
{
id: "p1",
name: "Ayesha Khan",
initials: "AK",
role: "Senior product designer",
team: "Design",
email: "ayesha.khan@northwind.io",
location: "London",
status: "Active",
},
{
id: "p2",
name: "Sara Malik",
initials: "SM",
role: "Payroll specialist",
team: "People Ops",
email: "sara.malik@northwind.io",
location: "Lisbon",
status: "On leave",
},
{
id: "p3",
name: "Ahmed Raza",
initials: "AR",
role: "Engineering manager",
team: "Engineering",
email: "ahmed.raza@northwind.io",
location: "Lahore",
status: "Remote",
},
{
id: "p4",
name: "Mia Jensen",
initials: "MJ",
role: "Product manager",
team: "Product",
email: "mia.jensen@northwind.io",
location: "Copenhagen",
status: "Active",
},
{
id: "p5",
name: "Luis Ortega",
initials: "LO",
role: "Frontend engineer",
team: "Engineering",
email: "luis.ortega@northwind.io",
location: "Madrid",
status: "Active",
},
{
id: "p6",
name: "Yuki Tanaka",
initials: "YT",
role: "Recruiter",
team: "People Ops",
email: "yuki.tanaka@northwind.io",
location: "Tokyo",
status: "Active",
},
{
id: "p7",
name: "Chloe Martin",
initials: "CM",
role: "Data analyst",
team: "Finance",
email: "chloe.martin@northwind.io",
location: "Paris",
status: "Remote",
},
{
id: "p8",
name: "Omar Haddad",
initials: "OH",
role: "Support lead",
team: "Customer",
email: "omar.haddad@northwind.io",
location: "Dubai",
status: "Invited",
},
];
const TEAMS = [
"Design",
"Engineering",
"Finance",
"People Ops",
"Product",
"Customer",
] as const;
const TONE = { Active: "success", "On leave": "leave", Remote: "remote", Invited: "info" } as const;
/** Filter bar: a search field and filter chips above the results; selected chips show a check and press again to remove. */
export default function FilterBar() {
const [query, setQuery] = useState("");
const [remote, setRemote] = useState(true);
const [onLeave, setOnLeave] = useState(false);
const [teams, setTeams] = useState<Set<string>>(new Set(["Engineering", "Finance"]));
const active = Number(remote) + Number(onLeave) + teams.size + (query ? 1 : 0);
const clear = () => {
setQuery("");
setRemote(false);
setOnLeave(false);
setTeams(new Set());
};
const results = PEOPLE.filter((p) => {
const statuses = [remote ? "Remote" : null, onLeave ? "On leave" : null].filter(Boolean);
return (
(statuses.length === 0 || statuses.includes(p.status)) &&
(teams.size === 0 || teams.has(p.team)) &&
`${p.name} ${p.role}`.toLowerCase().includes(query.trim().toLowerCase())
);
});
return (
<div className="@container bg-page px-4 py-8 @3xl:px-8">
<div className="mx-auto grid max-w-4xl gap-4">
<header className="grid gap-1">
<h2 className="text-heading-h2 text-primary">People</h2>
<p className="text-body-default text-secondary">
Northwind directory · {PEOPLE.length} people
</p>
</header>
<div className="rounded-lg border border-default bg-surface">
<TableToolbar
aria-label="Directory filters"
search={
<SearchField
aria-label="Search people"
placeholder="Search name or role"
value={query}
onChange={setQuery}
/>
}
filters={
<>
<FilterChip label="Remote" isSelected={remote} onChange={setRemote} />
<FilterChip label="On leave" isSelected={onLeave} onChange={setOnLeave} />
<FilterChip
label={teams.size > 0 ? `Team: ${teams.size}` : "Team"}
hasDropdown
selectionMode="multiple"
selectedKeys={teams}
onSelectionChange={(keys) => {
setTeams(new Set(keys === "all" ? TEAMS : [...keys].map(String)));
}}
>
{TEAMS.map((team) => (
<MenuItem key={team} id={team} label={team} />
))}
</FilterChip>
</>
}
{...(active > 0
? { onClearFilters: clear, clearFiltersLabel: `Clear filters (${active})` }
: {})}
actions={
<Button variant="secondary" leadingIcon={<LuBookmark />}>
Save view
</Button>
}
/>
</div>
<p className="text-body-small text-secondary" aria-live="polite">
{results.length} {results.length === 1 ? "person matches" : "people match"} your filters
</p>
{results.length > 0 ? (
<ul className="grid divide-y divide-subtle rounded-lg border border-default bg-surface">
{results.map((p) => (
<li key={p.id}>
<ListItem
href={`#person-${p.id}`}
title={p.name}
subtitle={`${p.role} · ${p.team} · ${p.location}`}
leading={<Avatar size="lg" initials={p.initials} isDecorative />}
trailing={<Badge label={p.status} tone={TONE[p.status]} />}
/>
</li>
))}
</ul>
) : (
<EmptyState
icon={<LuSearchX />}
title="No one matches these filters"
description="Remove a filter or search for someone else."
headingLevel={3}
primaryAction={
<Button size="sm" variant="secondary" onPress={clear}>
Clear filters
</Button>
}
/>
)}
</div>
</div>
);
}
More lists, tables and records blocks
- Data tableSearch, sort, select rows and page through results.
- Bulk actionsA floating bar that acts on the selected rows.
- Filters drawerDetailed filters in a side panel.
- People directorySearchable list of people with presence.
- Record details drawerOpen a row to see details without leaving the list.
- Activity feedNewest first, with a 'new items' pill and paging.
- File browserFolders and files in a tree.
- Keyboard shortcutsA modal listing every shortcut.