Patterns/Sortable table head
Sortable table head
Clickable table-card column head with an arrow that turns with the sort direction and sets aria-sort. Button or link.
- Version
- 1.0.1
- Tier
- Pattern
- Updates taken
- Patch only
- Files
- 1
Preview
| Team | ||
|---|---|---|
| Ada Lovelace | Research | 2019 |
| Grace Hopper | Platform | 2016 |
| Katherine Johnson | Research | 2021 |
| Margaret Hamilton | Mobile | 2018 |
Install
In a project already set up on the contract, which is what registers the @ja3dan prefix. If yours isn't, set it up first.
bunx shadcn@latest add @ja3dan/sortable-table-head Also installs motion lucide-react if the project doesn't have them.
Related items
Installs alongside it
Used by
Source
What lands in your project, read from the published item rather than the working tree.
"use client";// Source: interior.dev Sortable Table, on table-card's Th. The header affordance only:// a clickable column head whose arrow turns with the direction. Sorting itself is// the caller's job (useDataTable, or the server).import type { MouseEvent, ReactNode } from "react";import { motion, useReducedMotion } from "motion/react";import { ArrowUp } from "lucide-react";import { STILL, TURN } from "@/registry/groundwork/lib/motion";import { Th } from "@/registry/groundwork/ui/table-card";import { cn } from "@/lib/utils";type SortableTableHeadProps = { /** The direction this column is sorted in, or null when it is not the sorted column. */ direction: "asc" | "desc" | null; onSort: () => void; /** Make the head a real link (URL-driven sorting). A plain click still calls `onSort`. */ href?: string; align?: "start" | "end"; className?: string; children: ReactNode;};function SortableTableHead({ direction, onSort, href, align = "start", className, children }: SortableTableHeadProps) { const reduced = useReducedMotion(); const active = direction !== null; const inner = ( <> <span>{children}</span> <motion.span aria-hidden className="shrink-0" initial={false} animate={{ rotate: direction === "desc" ? 180 : 0, opacity: active ? 1 : 0.35, scale: active ? 1 : 0.8 }} transition={reduced ? STILL : TURN} > <ArrowUp className="size-3" /> </motion.span> </> ); const controlClassName = cn( "flex w-full items-center gap-1 px-4 py-2.5 font-medium outline-none transition-colors duration-150 ease-out hover:text-foreground focus-visible:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset", align === "end" && "flex-row-reverse text-right", active && "text-foreground", ); function handleLinkClick(event: MouseEvent<HTMLAnchorElement>) { if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; event.preventDefault(); onSort(); } return ( <Th aria-sort={active ? (direction === "asc" ? "ascending" : "descending") : "none"} align={align === "end" ? "right" : "left"} className={cn("p-0", className)} > {href ? ( <a href={href} onClick={handleLinkClick} className={controlClassName}> {inner} </a> ) : ( <button type="button" onClick={onSort} className={controlClassName}> {inner} </button> )} </Th> );}export { SortableTableHead };