Sortable Table
Searchable and sortable name, email and company table with an empty state.
Install
One command copies the item's source into your project, where you own and edit it.
npx manteen add @house/table-sortDependencies
@mantine/core@^9npm@tabler/icons-react@^3npm
Installing this item also installs @house/mantine-ui-license from the registry.
Preview
Usage
A copy-ready example published by the registry author. Imports use the aliases Manteen configures at init.
import { TableSort } from "@/components/ui/table-sort";
export function TeamDirectoryTable() { return ( <TableSort data={[ { name: "Robert Wolfkisser", email: "rob_wolf@gmail.com", company: "Canyon Realty" }, { name: "Jill Jailbreaker", email: "jj@breaker.com", company: "Fishing Corp" }, { name: "Henry Silkeater", email: "henry@silkeater.io", company: "Wool Charts" }, { name: "Bill Horsefighter", email: "bhorsefighter@royal.net", company: "Combat Farms" }, { name: "Jeremy Footviewer", email: "jeremy@foot.dev", company: "Footwork Inc" }, ]} searchPlaceholder="Search team members" onRowClick={(row) => console.log("row clicked", row)} /> );}Props
Author-documented props, carried verbatim from the registry item.
| Prop | Type | Default | Description |
|---|---|---|---|
data* | TableSortRow[] | — | Rows to display, searched and sorted in place before rendering. |
searchPlaceholder | string | "Search by any field" | Placeholder text for the search input above the table. |
emptyMessage | ReactNode | "Nothing found" | Message rendered in a full-width row when the filtered data is empty. |
onRowClick | (row: TableSortRow) => void | — | Called with the row data when a table row is clicked; rows become clickable only when this is provided. |
* required
Styling
No public Styles API is declared. Installed CSS remains editable because your project owns the source; internal class names are implementation details, not a customization contract.
Source
2 installable files ship with this item.
registry/mantine-ui/table-sort/table-sort.tsxcomponent · 4.7 kB · tsx
/** * Adapted from Mantine UI's TableSort at * ffbf61c559f374a7ea28fcf00355e84dcbe9a908. MIT licensed; see * LICENSES/MANTINE-UI.txt after installation. */
import { Center, Group, ScrollArea, Table, Text, TextInput, UnstyledButton } from "@mantine/core";import { IconChevronDown, IconChevronUp, IconSearch, IconSelector } from "@tabler/icons-react";import type { ChangeEvent, ReactNode } from "react";import { useMemo, useState } from "react";
import classes from "./table-sort.module.css";
export interface TableSortRow { name: string; email: string; company: string;}
export interface TableSortProps { data: TableSortRow[]; searchPlaceholder?: string; emptyMessage?: ReactNode; onRowClick?: (row: TableSortRow) => void;}
interface SortableHeaderProps { children: ReactNode; reversed: boolean; sorted: boolean; onSort: () => void;}
function SortableHeader({ children, reversed, sorted, onSort }: SortableHeaderProps) { const Icon = sorted ? (reversed ? IconChevronUp : IconChevronDown) : IconSelector;
return ( <Table.Th className={classes.th} aria-sort={sorted ? (reversed ? "descending" : "ascending") : "none"} > <UnstyledButton onClick={onSort} className={classes.control}> <Group justify="space-between"> <Text fw={500} fz="sm"> {children} </Text> <Center className={classes.icon}> <Icon size={16} stroke={1.5} /> </Center> </Group> </UnstyledButton> </Table.Th> );}
function filterData(data: TableSortRow[], search: string): TableSortRow[] { const query = search.toLowerCase().trim(); if (!query) return data;
return data.filter((row) => [row.name, row.email, row.company].some((value) => value.toLowerCase().includes(query)), );}
function sortData( data: TableSortRow[], sortBy: keyof TableSortRow | null, reversed: boolean, search: string,): TableSortRow[] { const filtered = filterData(data, search); if (!sortBy) return filtered;
return [...filtered].sort((left, right) => { const result = left[sortBy].localeCompare(right[sortBy]); return reversed ? -result : result; });}
export function TableSort({ data, searchPlaceholder = "Search by any field", emptyMessage = "Nothing found", onRowClick,}: TableSortProps) { const [search, setSearch] = useState(""); const [sortBy, setSortBy] = useState<keyof TableSortRow | null>(null); const [reversed, setReversed] = useState(false); const sortedData = useMemo( () => sortData(data, sortBy, reversed, search), [data, reversed, search, sortBy], );
const setSorting = (field: keyof TableSortRow) => { const nextReversed = field === sortBy ? !reversed : false; setReversed(nextReversed); setSortBy(field); };
const handleSearchChange = (event: ChangeEvent<HTMLInputElement>) => { setSearch(event.currentTarget.value); };
const rows = sortedData.map((row) => ( <Table.Tr key={`${row.email}\u0000${row.name}`} onClick={onRowClick ? () => onRowClick(row) : undefined} className={onRowClick ? classes.clickableRow : undefined} > <Table.Td>{row.name}</Table.Td> <Table.Td>{row.email}</Table.Td> <Table.Td>{row.company}</Table.Td> </Table.Tr> ));
return ( <ScrollArea> <TextInput placeholder={searchPlaceholder} mb="md" leftSection={<IconSearch size={16} stroke={1.5} />} value={search} onChange={handleSearchChange} /> <Table horizontalSpacing="md" verticalSpacing="xs" miw={560} layout="fixed"> <Table.Thead> <Table.Tr> <SortableHeader sorted={sortBy === "name"} reversed={reversed} onSort={() => setSorting("name")} > Name </SortableHeader> <SortableHeader sorted={sortBy === "email"} reversed={reversed} onSort={() => setSorting("email")} > Email </SortableHeader> <SortableHeader sorted={sortBy === "company"} reversed={reversed} onSort={() => setSorting("company")} > Company </SortableHeader> </Table.Tr> </Table.Thead> <Table.Tbody> {rows.length > 0 ? ( rows ) : ( <Table.Tr> <Table.Td colSpan={3}> <Text fw={500} ta="center"> {emptyMessage} </Text> </Table.Td> </Table.Tr> )} </Table.Tbody> </Table> </ScrollArea> );}registry/mantine-ui/table-sort/table-sort.module.cssstyle · 487 B · css
/* * Adapted from Mantine UI's TableSort at * ffbf61c559f374a7ea28fcf00355e84dcbe9a908. MIT licensed; see * LICENSES/MANTINE-UI.txt after installation. */.th { padding: 0;}
.control { width: 100%; padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);}
.control:hover { background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));}
.icon { width: 21px; height: 21px; border-radius: 21px;}
.clickableRow { cursor: pointer;}Author notes
Adapted from Mantine UI TableSort at ffbf61c559…; accepts consumer data and derives filtered rows without duplicating prop state.