Skip to content
oks-ui

App screens

Team members table

The team-settings list: a checkbox-selectable Table whose cells render an Avatar + name/email, a status Chip, and a group Chip, plus a per-row Dropdown of actions, a rows-per-page Select, and a real Pagination control.

team-members.tsx

How it's built

1

`selectionMode="multiple"` on Table gives the header/row checkboxes and `selectedKeys` state for free.

2

Two Chip colors do double duty — `statusTone` and `groupTone` both map a raw string to a real Chip `color`, so adding a new status or group is a one-line change.

3

The numbered page buttons are hidden via `classNames={{ page: "hidden", ellipsis: "hidden" }}`, leaving Pagination's real prev/next arrows as the only controls — a supported customization, not a different component.

Source

One file · copy & paste
team-members.tsx
"use client";

import { useMemo, useState } from "react";
import { Avatar } from "oks-ui/avatar";
import { Button } from "oks-ui/button";
import { Chip } from "oks-ui/chip";
import { Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from "oks-ui/dropdown";
import { FormFieldSet } from "oks-ui/form-field-set";
import type { NavItemData } from "oks-ui/nav";
import { PageTitle } from "oks-ui/page-title";
import { Pagination, PaginationSummary } from "oks-ui/pagination";
import { Table } from "oks-ui/table";
import "oks-ui/avatar.css";
import "oks-ui/button.css";
import "oks-ui/chip.css";
import "oks-ui/dropdown.css";
import "oks-ui/form-field-set.css";
import "oks-ui/page-title.css";
import "oks-ui/pagination.css";
import "oks-ui/table.css";
import { PatternAppShell } from "../PatternAppShell";
import {
  BriefcaseIcon,
  CreditCardIcon,
  DownloadIcon,
  GridIcon,
  MoreIcon,
  PlusIcon,
  SettingsIcon,
  UsersIcon,
} from "../../showcase/icons";

const NAV_ITEMS: NavItemData[] = [
  {
    key: "main",
    label: "Main menu",
    isSection: true,
    children: [
      { key: "dashboard", label: "Dashboard", icon: <GridIcon size={18} /> },
      { key: "team", label: "Team members", icon: <UsersIcon size={18} /> },
      { key: "billing", label: "Billing", icon: <CreditCardIcon size={18} /> },
      { key: "projects", label: "Projects", icon: <BriefcaseIcon size={18} /> },
    ],
  },
  {
    key: "other",
    label: "Other",
    isSection: true,
    children: [{ key: "settings", label: "Settings", icon: <SettingsIcon size={18} /> }],
  },
];

type Status = "Active" | "Inactive";
type Group = "Design" | "Development" | "Marketing";

type Member = {
  id: number;
  name: string;
  email: string;
  status: Status;
  location: string;
  phone: string;
  group: Group;
} & Record<string, unknown>;

// A handful of rows get a real photo — two are the same stock-free portrait
// assets used elsewhere on this site, three are AI-synthesized
// (non-real-person) faces — the rest fall back to initials.
const PHOTOS: Record<string, string> = {
  "Tom Cooper": "/hero/person-man.jpg",
  "Leslie Lawson": "/hero/person-woman.jpg",
  "Annette Black": "/avatars/synthetic-1.jpg",
  "Floyd Miles": "/avatars/synthetic-2.jpg",
  "Theresa Web": "/avatars/synthetic-3.jpg",
};

const MEMBERS: Member[] = [
  { id: 1, name: "Tom Cooper", email: "cooper@gmail.com", status: "Active", location: "United States", phone: "+65 9308 4744", group: "Design" },
  { id: 2, name: "Leslie Lawson", email: "lawson@gmail.com", status: "Active", location: "Canada", phone: "+65 8689 9346", group: "Development" },
  { id: 3, name: "Kristin Watson", email: "watson@gmail.com", status: "Inactive", location: "Germany", phone: "+62-896-5554-32", group: "Marketing" },
  { id: 4, name: "Annette Black", email: "a.black@gmail.com", status: "Active", location: "United States", phone: "+62-838-5558-34", group: "Design" },
  { id: 5, name: "Floyd Miles", email: "miles@gmail.com", status: "Inactive", location: "United States", phone: "+1-555-8701-158", group: "Development" },
  { id: 6, name: "Cody Fisher", email: "fisher@gmail.com", status: "Inactive", location: "United States", phone: "+61480013910", group: "Design" },
  { id: 7, name: "Theresa Web", email: "theresa@gmail.com", status: "Active", location: "France", phone: "+91 9163337392", group: "Development" },
  { id: 8, name: "Tim Simmons", email: "simmons@gmail.com", status: "Active", location: "United States", phone: "+49 1590 12345678", group: "Marketing" },
  { id: 9, name: "Jenny Wilson", email: "j.wilson@gmail.com", status: "Active", location: "Australia", phone: "+61 412 345 678", group: "Development" },
  { id: 10, name: "Robert Fox", email: "fox@gmail.com", status: "Inactive", location: "United Kingdom", phone: "+44 7911 123456", group: "Marketing" },
  { id: 11, name: "Savannah Nguyen", email: "nguyen@gmail.com", status: "Active", location: "United States", phone: "+1-555-0192", group: "Design" },
  { id: 12, name: "Darrell Steward", email: "steward@gmail.com", status: "Active", location: "Canada", phone: "+1-647-555-0142", group: "Development" },
  { id: 13, name: "Courtney Henry", email: "henry@gmail.com", status: "Inactive", location: "Germany", phone: "+49 30 1234567", group: "Marketing" },
  { id: 14, name: "Wade Warren", email: "warren@gmail.com", status: "Active", location: "United States", phone: "+1-555-0148", group: "Design" },
  { id: 15, name: "Esther Howard", email: "howard@gmail.com", status: "Active", location: "France", phone: "+33 6 12 34 56 78", group: "Development" },
  { id: 16, name: "Jacob Jones", email: "jones@gmail.com", status: "Inactive", location: "United States", phone: "+1-555-0177", group: "Marketing" },
];

const statusTone: Record<Status, "success" | "default"> = { Active: "success", Inactive: "default" };
const groupTone: Record<Group, "info" | "success" | "secondary"> = {
  Design: "info",
  Development: "success",
  Marketing: "secondary",
};

const PAGE_SIZE_OPTIONS = [
  { label: "8", value: "8" },
  { label: "10", value: "10" },
  { label: "20", value: "20" },
];

function RowMenu({ label }: { label: string }) {
  return (
    <Dropdown placement="bottom-end">
      <DropdownTrigger>
        <Button isIconOnly aria-label={label} variant="ghost" radius="full" size="sm">
          <MoreIcon size={16} />
        </Button>
      </DropdownTrigger>
      <DropdownMenu aria-label={label}>
        <DropdownItem key="edit">Edit member</DropdownItem>
        <DropdownItem key="resend">Resend invite</DropdownItem>
        <DropdownItem key="remove" classNames={{ title: "text-[color:var(--oks-color-danger-700)]" }}>
          Remove
        </DropdownItem>
      </DropdownMenu>
    </Dropdown>
  );
}

export function TeamMembers() {
  const [selected, setSelected] = useState<Set<number>>(new Set());
  const [page, setPage] = useState(1);
  const [pageSize, setPageSize] = useState(8);

  const pageRows = useMemo(() => MEMBERS.slice((page - 1) * pageSize, page * pageSize), [page, pageSize]);

  return (
    <PatternAppShell navItems={NAV_ITEMS} selectedNavKey="team">
      <div className="flex shrink-0 flex-wrap items-center justify-between gap-3">
        <PageTitle
          as="h2"
          title="Team members"
          classNames={{ base: "flex-col items-start", title: "text-2xl font-bold tracking-tight text-ink" }}
        />
        <div className="flex items-center gap-2">
          <Button variant="bordered" radius="full" size="sm" startContent={<DownloadIcon size={15} />}>
            Download CSV
          </Button>
          <Button color="primary" radius="full" size="sm" startContent={<PlusIcon size={14} />}>
            Add user
          </Button>
        </div>
      </div>

      <Table
        aria-label="Team members"
        classNames={{ cell: "whitespace-nowrap" }}
        selectionMode="multiple"
        selectedKeys={selected}
        onSelectionChange={(keys) => setSelected(keys as Set<number>)}
        columns={[
          {
            key: "name",
            header: "User",
            sortable: true,
            render: (m) => (
              <div className="flex items-center gap-2.5">
                <Avatar src={PHOTOS[m.name]} name={m.name} size="sm" radius="full" />
                <div className="min-w-0">
                  <p className="truncate font-medium text-ink">{m.name}</p>
                  <p className="truncate text-[0.78rem] text-ink-faint">{m.email}</p>
                </div>
              </div>
            ),
          },
          {
            key: "status",
            header: "Status",
            render: (m) => (
              <Chip size="sm" variant="soft" color={statusTone[m.status]}>
                {m.status}
              </Chip>
            ),
          },
          { key: "location", header: "Location" },
          { key: "phone", header: "Phone" },
          {
            key: "group",
            header: "Group",
            render: (m) => (
              <Chip size="sm" variant="soft" color={groupTone[m.group]}>
                {m.group}
              </Chip>
            ),
          },
          {
            key: "actions",
            header: "",
            ariaLabel: "Actions",
            align: "end",
            render: (m) => <RowMenu label={`${m.name} options`} />,
          },
        ]}
        rows={pageRows}
        getRowKey={(m) => m.id}
      />

      <div className="flex flex-wrap items-center justify-between gap-3">
        <div className="flex items-center gap-2">
          <span className="text-[0.85rem] text-ink-muted">Show rows per page</span>
          <FormFieldSet
            type="select"
            name="pageSize"
            label="Rows per page"
            value={String(pageSize)}
            onChange={(v: unknown) => {
              setPageSize(Number(v) || 8);
              setPage(1);
            }}
            options={PAGE_SIZE_OPTIONS}
            classNames={{ base: "w-20", label: "sr-only" }}
          />
        </div>
        <div className="flex items-center gap-4">
          <PaginationSummary page={page} pageSize={pageSize} total={MEMBERS.length} />
          <Pagination
            page={page}
            pageCount={Math.max(1, Math.ceil(MEMBERS.length / pageSize))}
            onChange={setPage}
            size="sm"
            classNames={{ page: "hidden", ellipsis: "hidden" }}
          />
        </div>
      </div>
    </PatternAppShell>
  );
}

More patterns

App screens
Analytics dashboardA full fintech app screen — collapsible sidebar, header search, wallet cards, an earnings chart, savings goals, and a transactions table.
App screens
Settings panelA real settings screen — top tabs, two-column sections, and a sortable, selectable billing history table.
App screens
Data table viewA full documents list screen — status Tabs, a filter toolbar, and a sortable, selectable Table with real recipient avatars.