# Users Table

> A searchable users table with role filters, row selection, and bulk role and status actions.

A shadcn/ui block from the [@esuiss](https://suiss.app/ui?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/esuiss/users-table).

- Page, with a live preview: https://shadcn.exchange/r/esuiss/users-table
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/esuiss/users-table-light-a00d213513600092.png
- Registry item: https://suiss.app/ui/r/new-york-v4/users-table.json
- Categories: blocks, users
- Listed: 2026-10-20, source last changed 2026-10-20

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/esuiss/users-table.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `@esuiss/avatar`, `@esuiss/button`, `@esuiss/checkbox`, `@esuiss/input-group`, `@esuiss/sidebar`, `@esuiss/table`, `@esuiss/toggle-group`

## Files

### `bases/radix/components/blocks/users-table.tsx`

```tsx
"use client"

import * as React from "react"

import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/components/ui/input-group"
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
  useSidebar,
} from "@/components/ui/sidebar"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

type NavItem = {
  value: string
  label: string
  icon: React.ReactNode
  badge?: React.ReactNode
  group?: string
}

function NavSidebar({
  items,
  value: valueProp,
  defaultValue,
  onValueChange,
  header,
  footer,
}: {
  items: NavItem[]
  value?: string
  defaultValue?: string
  onValueChange?: (value: string) => void
  header?: React.ReactNode
  footer?: React.ReactNode
}) {
  const { isMobile, setOpenMobile } = useSidebar()
  const [uncontrolledValue, setUncontrolledValue] = React.useState(
    defaultValue ?? items[0]?.value
  )
  const value = valueProp ?? uncontrolledValue
  const groups = [...new Set(items.map((item) => item.group ?? ""))]

  return (
    <Sidebar collapsible="icon">
      {header && (
        <SidebarHeader className="group-data-[collapsible=icon]:hidden">
          {header}
        </SidebarHeader>
      )}
      <SidebarContent>
        {groups.map((group) => (
          <SidebarGroup key={group}>
            {group && <SidebarGroupLabel>{group}</SidebarGroupLabel>}
            <SidebarGroupContent>
              <SidebarMenu>
                {items
                  .filter((item) => (item.group ?? "") === group)
                  .map((item) => (
                    <SidebarMenuItem key={item.value}>
                      <SidebarMenuButton
                        isActive={item.value === value}
                        tooltip={item.label}
                        aria-current={item.value === value ? "page" : undefined}
                        onClick={() => {
                          if (valueProp === undefined) {
                            setUncontrolledValue(item.value)
                          }
                          onValueChange?.(item.value)
                          if (isMobile) setOpenMobile(false)
                        }}
                      >
                        {item.icon}
                        <span>{item.label}</span>
                      </SidebarMenuButton>
                      {item.badge !== undefined && (
                        <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
                      )}
                    </SidebarMenuItem>
                  ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        ))}
      </SidebarContent>
      {footer && (
        <SidebarFooter className="group-data-[collapsible=icon]:hidden">
          {footer}
        </SidebarFooter>
      )}
      <SidebarRail />
    </Sidebar>
  )
}

const nav: NavItem[] = [
  {
    value: "users",
    label: "Users",
    icon: (
      <IconPlaceholder
        lucide="UsersIcon"
        tabler="IconUsers"
        hugeicons="UserGroupIcon"
        phosphor="UsersIcon"
        remixicon="RiGroupLine"
      />
    ),
  },
  {
    value: "roles",
    label: "Roles",
    icon: (
      <IconPlaceholder
        lucide="ShieldIcon"
        tabler="IconShield"
        hugeicons="Shield01Icon"
        phosphor="ShieldIcon"
        remixicon="RiShieldLine"
      />
    ),
  },
  {
    value: "log",
    label: "Activity",
    icon: (
      <IconPlaceholder
        lucide="ChartColumnIcon"
        tabler="IconChartBar"
        hugeicons="ChartColumnIcon"
        phosphor="ChartBarIcon"
        remixicon="RiBarChartLine"
      />
    ),
  },
  {
    value: "settings",
    label: "Settings",
    icon: (
      <IconPlaceholder
        lucide="SettingsIcon"
        tabler="IconSettings"
        hugeicons="Settings01Icon"
        phosphor="GearIcon"
        remixicon="RiSettingsLine"
      />
    ),
  },
]

const roles = ["Admin", "Editor", "Support", "Viewer"]

type Status = "Active" | "Suspended" | "Invited"

type User = {
  name: string
  email: string
  role: string
  status: Status
  seen: string
  hue: number
}

const initialUsers: User[] = [
  {
    name: "Jamie Rivera",
    email: "jamie@example.com",
    role: "Admin",
    status: "Active",
    seen: "2 min ago",
    hue: 250,
  },
  {
    name: "Morgan Lee",
    email: "morgan@example.com",
    role: "Editor",
    status: "Active",
    seen: "1 hr ago",
    hue: 30,
  },
  {
    name: "Riley Chen",
    email: "riley@example.com",
    role: "Support",
    status: "Active",
    seen: "Yesterday",
    hue: 150,
  },
  {
    name: "Taylor Kim",
    email: "taylor@example.com",
    role: "Viewer",
    status: "Suspended",
    seen: "3 days ago",
    hue: 200,
  },
  {
    name: "Avery Brooks",
    email: "avery@example.com",
    role: "Editor",
    status: "Invited",
    seen: "—",
    hue: 330,
  },
  {
    name: "Jordan Park",
    email: "jordan@example.com",
    role: "Support",
    status: "Active",
    seen: "5 hr ago",
    hue: 100,
  },
]

const initials = (name: string) =>
  name
    .split(" ")
    .map((part) => part[0])
    .join("")

export function UsersTable() {
  const [users, setUsers] = React.useState(initialUsers)
  const [query, setQuery] = React.useState("")
  const [role, setRole] = React.useState("All")
  const [selected, setSelected] = React.useState<string[]>([])
  const list = users.filter(
    (user) =>
      (role === "All" || user.role === role) &&
      (!query ||
        `${user.name} ${user.email}`
          .toLowerCase()
          .includes(query.toLowerCase()))
  )

  const toggle = (email: string) =>
    setSelected((current) =>
      current.includes(email)
        ? current.filter((item) => item !== email)
        : [...current, email]
    )

  const bulk = (patch: Partial<User>) => {
    setUsers((current) =>
      current.map((user) =>
        selected.includes(user.email) ? { ...user, ...patch } : user
      )
    )
    setSelected([])
  }

  return (
    <SidebarProvider>
      <NavSidebar
        items={nav}
        defaultValue="users"
        header={
          <p className="flex items-center gap-2 px-2 pt-1 text-sm font-semibold">
            <span className="font-bold">suiss</span> Admin
          </p>
        }
      />
      <SidebarInset>
        <div className="flex flex-col gap-5 px-5 py-5.5 md:px-7">
          <div className="flex flex-wrap items-start gap-3">
            <SidebarTrigger className="-ms-2 mt-0.5" />
            <div className="flex flex-1 flex-col">
              <h1 className="text-3xl font-semibold tracking-tight">Users</h1>
              <p className="text-sm text-muted-foreground">
                {users.length} people
              </p>
            </div>
            <div className="flex w-full items-center gap-2 sm:w-auto">
              <InputGroup className="h-8 flex-1 sm:w-48">
                <InputGroupAddon>
                  <IconPlaceholder
                    lucide="SearchIcon"
                    tabler="IconSearch"
                    hugeicons="Search01Icon"
                    phosphor="MagnifyingGlassIcon"
                    remixicon="RiSearchLine"
                  />
                </InputGroupAddon>
                <InputGroupInput
                  type="search"
                  value={query}
                  onChange={(event) => setQuery(event.target.value)}
                  placeholder="Search"
                  aria-label="Search users"
                  className="text-sm"
                />
              </InputGroup>
              <Button size="sm" className="gap-1">
                <IconPlaceholder
                  lucide="PlusIcon"
                  tabler="IconPlus"
                  hugeicons="PlusSignIcon"
                  phosphor="PlusIcon"
                  remixicon="RiAddLine"
                  className="size-3.25"
                />
                Invite
              </Button>
            </div>
          </div>
          <ToggleGroup
            spacing={0}
            type="single"
            value={role}
            onValueChange={(value: string) => {
              if (!value) return
              setRole(value)
              setSelected([])
            }}
            aria-label="Role"
            className="max-w-full overflow-x-auto"
          >
            {["All", ...roles].map((item) => (
              <ToggleGroupItem
                key={item}
                value={item}
                size="sm"
                className="px-4 md:px-8"
              >
                {item}
              </ToggleGroupItem>
            ))}
          </ToggleGroup>
          <div className="overflow-hidden rounded-xl bg-muted">
            <Table>
              <TableHeader>
                <TableRow className="border-border hover:bg-transparent">
                  <TableHead className="ps-4 text-xs font-normal text-muted-foreground">
                    Name
                  </TableHead>
                  <TableHead className="hidden text-xs font-normal text-muted-foreground sm:table-cell">
                    Role
                  </TableHead>
                  <TableHead className="text-xs font-normal text-muted-foreground">
                    Status
                  </TableHead>
                  <TableHead className="hidden pe-4 text-end text-xs font-normal text-muted-foreground md:table-cell">
                    Last sign-in
                  </TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {list.map((user) => {
                  const isSelected = selected.includes(user.email)
                  return (
                    <TableRow
                      key={user.email}
                      data-selected={isSelected ? "true" : undefined}
                      onClick={() => toggle(user.email)}
                      className="group/row cursor-default border-0 select-none not-data-selected:even:bg-background/60 not-data-selected:hover:bg-foreground/5 has-focus-visible:border-ring has-focus-visible:ring-3 has-focus-visible:ring-ring/50 data-selected:bg-primary data-selected:text-primary-foreground"
                    >
                      <TableCell className="ps-4">
                        <span className="flex items-center gap-2.5">
                          <Checkbox
                            checked={isSelected}
                            onClick={(event) => event.stopPropagation()}
                            onCheckedChange={() => toggle(user.email)}
                            aria-label={`Select ${user.name}`}
                            className="absolute sr-only"
                          />
                          <Avatar className="size-7">
                            <AvatarFallback
                              style={{
                                background: `linear-gradient(160deg, oklch(0.78 0.1 ${user.hue}), oklch(0.6 0.14 ${user.hue + 30}))`,
                              }}
                              className="text-xs text-white"
                            >
                              {initials(user.name)}
                            </AvatarFallback>
                          </Avatar>
                          <span className="flex min-w-0 flex-col">
                            <span className="text-sm font-semibold">
                              {user.name}
                            </span>
                            <span className="truncate text-xs text-muted-foreground group-data-selected/row:text-primary-foreground">
                              {user.email}
                            </span>
                          </span>
                        </span>
                      </TableCell>
                      <TableCell className="hidden text-sm sm:table-cell">
                        {user.role}
                      </TableCell>
                      <TableCell
                        data-status={isSelected ? undefined : user.status}
                        className="text-sm data-[status=Invited]:text-muted-foreground data-[status=Suspended]:text-destructive group-data-selected/row:text-primary-foreground"
                      >
                        {user.status}
                      </TableCell>
                      <TableCell className="hidden pe-4 text-end text-sm text-muted-foreground group-data-selected/row:text-primary-foreground md:table-cell">
                        {user.seen}
                      </TableCell>
                    </TableRow>
                  )
                })}
              </TableBody>
            </Table>
            {list.length === 0 && (
              <p className="p-6 text-center text-sm text-muted-foreground">
                No matching users.
              </p>
            )}
            <div className="flex min-h-12 flex-wrap items-center gap-2 border-t border-border py-2 ps-4 pe-3">
              <span
                className="flex-1 text-xs text-muted-foreground"
                aria-live="polite"
              >
                {selected.length
                  ? `${selected.length} selected`
                  : "Click rows to select"}
              </span>
              {selected.length > 0 && (
                <>
                  <Button
                    size="sm"
                    variant="secondary"
                    onClick={() => bulk({ role: "Editor" })}
                  >
                    Make Editor
                  </Button>
                  <Button
                    size="sm"
                    variant="secondary"
                    onClick={() => bulk({ status: "Active" })}
                  >
                    Activate
                  </Button>
                  <Button
                    size="sm"
                    variant="destructive"
                    onClick={() => bulk({ status: "Suspended" })}
                  >
                    Suspend
                  </Button>
                </>
              )}
            </div>
          </div>
        </div>
      </SidebarInset>
    </SidebarProvider>
  )
}
```
