Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Tables & Lists

Members table

Tables & Lists

Members table React component for Tailwind CSS v4

ListPermissions Matrix
Live props
Behavior
confirmRemove
defaultInviteRole
searchable
responsive
locale
State
canManage
defaultTab
defaultInviteOpen
loading
skeletonCount5
Layout
showInvites
showStatus
showTwoFactor
showLastActive
Appearance
timeFormat
density
variant
tone
color
Code
import { MembersTable } from "@/components/ui/members-table"

<MembersTable />
  1. Home
  2. Components
  3. Tables & Lists
  4. Members table
Pro

Members table React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React members table for B2B team settings: a role select per row, status, activity, 2FA, pending invitations, seats and an invite dialog.

Switch between Members and Invitations with the tabs (arrow keys move between them) and type in the search to filter by name or email, with the matches highlighted and the result count announced. Click the Member, Role or Last active header to sort (a third click clears it). Pick a new role right in the row: it applies at once with a spinner in the picker and, if the request fails, rolls back with an alert icon and a red outline that stay until the picker is opened again. Your own role and the owner's are locked (hover or focus them for the reason; on a phone your own card says it). The ⋯ menu per row fades in on hover or focus and is always visible on touch; Remove from team asks first, the row slides out once it is done and focus moves to the next row. On the Invitations tab each row can be resent or revoked, and expired ones say so. Invite members opens a dialog focused on the address field: press Enter, comma or space after each address, or paste a whole list ("Ana Pop <ana@firma.ro>; ..."), and addresses that are invalid, already members or already invited are listed under Not added with the reason. Cmd+Enter or Ctrl+Enter sends from the address field or the message; more addresses than free seats keeps Send disabled and explains why, and the new invitations slide into the Invitations tab. Below 768px of its own width (a phone, a narrow settings column) every row is a card with a full-width role picker, two cards to a row from 672px, and the invite button turns into an icon; on a phone the menus, pickers and the dialog become bottom sheets.

Members table is a Pro React component in the React table and list components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add members-table, together with Alert Dialog, Avatar, Badge, Button, Dialog and 18 more and the npm packages motion and lucide-react.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add members-table

Members table is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans

Usage

Import MembersTable from @/components/ui/members-table.

import { useState } from "react"
import { MembersTable, type MemberData, type MemberRole, type PendingInvite } from "@/components/ui/members-table"

const ROLES: MemberRole[] = [
  { value: "admin", label: "Admin", description: "Full access, including billing and the team" },
  { value: "sales", label: "Sales", description: "Clients, quotes and the pipeline" },
  { value: "viewer", label: "Read only", description: "Sees everything, changes nothing" },
]

export function TeamSettings({ initial }: { initial: MemberData[] }) {
  const [members, setMembers] = useState(initial)
  const [invites, setInvites] = useState<PendingInvite[]>([])
  return (
    <MembersTable
      members={members}
      roles={ROLES}
      invites={invites}
      seats={{ used: members.length, total: 12 }}
      onRoleChange={async (member, role) => {
        await fetch(`/api/members/${member.id}`, { method: "PATCH", body: JSON.stringify({ role }) })
        setMembers((list) => list.map((m) => (m.id === member.id ? { ...m, role } : m)))
      }}
      onRemove={async (member) => {
        await fetch(`/api/members/${member.id}`, { method: "DELETE" })
        setMembers((list) => list.filter((m) => m.id !== member.id))
      }}
      onInvite={async ({ emails, role }) => {
        await fetch("/api/invites", { method: "POST", body: JSON.stringify({ emails, role }) })
        setInvites((list) => [
          ...emails.map((email) => ({ id: email, email, role, invitedAt: Date.now() })),
          ...list,
        ])
      }}
      labels={{ invite: "Add teammates", search: "Search the team" }}
    />
  )
}

Props

  • members*
    MemberData[]

    The team: { id, name, email, avatar?, role, status?: "active" | "suspended", lastActiveAt?, joinedAt?, twoFactor?, isYou?, isOwner?, title? }. isYou and isOwner lock the role and removal (owner rules win).

  • roles*
    MemberRole[]

    The roles the select offers, in rank order: { value, label, description?, disabled? }. A member whose role is not listed shows the raw value with Unknown role.

  • invitesdefault "[]"
    PendingInvite[]

    Pending invitations: { id, email, role, invitedAt, invitedBy?: { name, avatar? }, expiresAt? }. Expired ones sort last with an Expired badge and stop reserving a seat.

  • onRoleChange
    (member: MemberData, role: string) => (void | Promise<unknown>)

    Optimistic: the row shows the new role with a spinner while the promise runs; a rejection rolls it back, outlines the select and announces labels.roleError.

  • onRemove
    (member: MemberData) => (void | Promise<unknown>)

    Runs after the confirmation (unless confirmRemove is false). A returned promise keeps the confirmation loading; a rejection shows labels.removeError in it.

  • onInvite
    (input: MemberInviteInput) => (void | Promise<unknown>)

    { emails, role, message? }. Shows the invite button. A returned promise keeps the dialog loading; on success it closes and switches to the Invitations tab, on rejection the error shows under the address field. The same dialog is exported alone as InviteMembersDialog (see Parts).

  • onResendInvite
    (invite: PendingInvite) => (void | Promise<unknown>)

    Adds Resend invitation to each invitation's menu; the ⋯ button spins while it runs.

  • onRevokeInvite
    (invite: PendingInvite) => (void | Promise<unknown>)

    Adds Revoke invitation. The row leaves at once and comes back if the promise rejects.

  • getActions
    (member: MemberData) => MenuEntry[]

    Extra entries for a member's ⋯ menu, above Remove from team (the W1 MenuEntry shape).

  • confirmRemovedefault true
    boolean

    Ask before removing someone, in an alert dialog with their name and email. Off, the row leaves at once and comes back if the request fails.

  • canManagedefault true
    boolean

    false is a member's view: roles read as text, no remove, no invite, no invitation actions.

  • seats
    { used: number; total: number }

    The seats meter in the header. Pending invitations reserve seats (the lighter part of the bar), and the invite dialog refuses more addresses than are free with labels.noSeats.

  • defaultInviteRoledefault ""
    string

    The role picked when the invite dialog opens. Empty picks the last enabled role (the least access).

  • tab
    "members" | "invites"

    The active tab, controlled. Pair it with onTabChange.

  • defaultTabdefault "members"
    "members" | "invites"

    The tab shown first while uncontrolled.

  • onTabChange
    (tab: "members" | "invites") => void

    Called when the tab changes, also after invitations were sent.

  • showInvitesdefault true
    boolean

    The Invitations tab. Off shows the members alone under a heading.

  • searchabledefault true
    boolean

    The search field. It filters by name and email, ignoring case and diacritics, and highlights the matches.

  • sort
    TableSort | null

    The sort, controlled: { key: "name" | "role" | "lastActive", direction }. null is unsorted.

  • defaultSortdefault "{ key: "name", direction: "asc" }"
    TableSort | null

    The first sort while uncontrolled. Role sorts by the order of roles, with the owner first.

  • onSortChange
    (sort: TableSort | null) => void

    Called when a header is pressed.

  • showStatusdefault true
    boolean

    The Status column: Active (success) or Suspended (neutral) badges with a dot.

  • showTwoFactordefault true
    boolean

    The 2FA column: a green shield when on, an Off warning badge when not.

  • showLastActivedefault true
    boolean

    The Last active column (sortable, newest first).

  • timeFormatdefault "relative"
    "relative" | "time" | "date" | "datetime"

    How last activity and sent times read: "5 min ago", a clock time, a date or both.

  • densitydefault "default"
    "compact" | "default" | "comfortable"

    Row height 36 / 48 / 56px with a 24 / 32 / 40px avatar. Compact puts the email on the name's line. Cards (below 768px of width) keep one size.

  • variantdefault "surface"
    "surface" | "plain"

    surface sits the table in a raised panel; plain drops the panel, for a table inside a settings card.

  • tonedefault "neutral"
    "neutral" | "brand" | "success" | "warning" | "danger" | "info"

    The invite button, the active tab's bar, the seats bar and the dialog's Send button.

  • colordefault ""
    string

    Any CSS color in place of the tone.

  • inviteOpen
    boolean

    The invite dialog, controlled. Pair it with onInviteOpenChange.

  • defaultInviteOpendefault false
    boolean

    Opens the invite dialog on mount.

  • onInviteOpenChange
    (open: boolean) => void

    Called when the invite dialog opens or closes.

  • responsivedefault true
    boolean

    Below 768px the role picker, the row menus and the invite dialog open as bottom sheets.

  • loadingdefault false
    boolean

    Skeleton rows (cards on phones) with the real shape: avatar, two lines, a role pill.

  • skeletonCountdefault 5
    number

    Skeleton rows while loading (at most four cards on phones).

  • emptyState
    ReactNode

    Replaces the empty state shown when there are no members (the default offers the invite button).

  • now
    Date | number

    The reference time for relative times and expiry. Defaults to a live clock that ticks every minute; demos pin 24.09.2026 16:40.

  • localedefault ""
    string

    Formats times, dates and counts. Empty uses the UIProvider locale, then the browser's.

  • labelsdefault "DEFAULT_MEMBERS_TABLE_LABELS"
    Partial<MembersTableLabels>

    Every string, English by default: caption, members(count, formatted), invites(count, formatted), invite, search, results, member, role, status, sortAscending / sortDescending / clearSort ({column} is the header text), lastActive, twoFactor, twoFactorOn / Off, you, owner, active, suspended, never, joined(date), roleFor(name), unknownRole, selfRole, ownerRole, roleChanged(name, role), roleError, savingRole, remove, removeTitle(name), removeDescription, removeConfirm, removing, cancel, selfRemove, ownerRemove, removed(name), removeError, actions, actionsFor(name), email, invitedBy, sentAt, expiresAt, expired, expires(date), resend, revoke, resent(email), revoked(email), inviteActionError, seats(used, total), seatsReserved(count), noSeats(free), freeSeats(free), inviteTitle, inviteDescription, emails, emailsPlaceholder, emailsHint, invalidEmail, alreadyMember, alreadyInvited, notAdded, dismiss(email), inviteRole, message, messagePlaceholder, summary(count, formatted, role), send(count), sending, sent(count, formatted), inviteError, close, empty, emptyDescription, noResults, noResultsDescription(query), clearSearch, noInvites, noInvitesDescription.

  • classNames
    Partial<Record<"header" | "tabs" | "search" | "seats" | "invite" | "table" | "row" | "cards" | "card" | "invites" | "empty" | "dialog", string>>

    Extra classes for the parts. dialog goes on the invite dialog's content.

  • className
    string

    Extra classes for the root, merged last. Other props spread onto it.

Parts

  • InviteMembersDialog

    component

    The invite dialog on its own (a bottom sheet on phones), for an Invite button elsewhere in the app. Takes the Dialog props (open, defaultOpen, onOpenChange, trigger, responsive, size) plus the ones below; classNames also styles emails, rejected, role, message, summary and send.

    • roles*
      MemberRole[]

      The roles to pick from.

    • members
      MemberData[]

      Current members; their addresses are refused as already members.

    • invitesdefault "[]"
      PendingInvite[]

      Pending invitations; their addresses are refused as already invited.

    • defaultRoledefault ""
      string

      The role picked when the dialog opens; empty picks the last enabled role.

    • defaultEmailsdefault "[]"
      string[]

      Addresses already in the field when it opens.

    • freeSeats
      number

      Seats left. More valid addresses than this blocks sending; undefined means no limit.

    • onInvite
      (input: MemberInviteInput) => (void | Promise<unknown>)

      Sends the invitations. A promise keeps the dialog loading; a rejection shows its message under the field.

    • showMessagedefault true
      boolean

      An optional personal message field.

    • maxMessageLengthdefault 500
      number

      Character limit of the message.

    • tonedefault "neutral"
      Tone

      Color of the send button.

    • labels
      Partial<MembersTableLabels>

      The same copy object as the table.

  • splitEmailList

    function

    splitEmailList(text) splits a pasted list (commas, semicolons, new lines, tabs, spaces, "Name <email>") into lowercased addresses. The invite field uses it on paste.

  • DEFAULT_MEMBERS_TABLE_LABELS

    constant

    The English copy of the table and the invite dialog. Spread it into labels when you translate only a few keys.

  • MemberData

    type

    One member row: id, name, email, avatar, role, status, lastActiveAt, joinedAt, twoFactor, isYou, isOwner and an optional title.

Examples

Read-only

A member's view of the team: roles as text, no actions.

Pasted list

Four pasted addresses: two added, one invalid, one already a member.

Seats full

One free seat and two addresses: the dialog explains and keeps Send disabled.

Compact, 40 members

Dense rows with the email on one line, search and sorting.

Loading and empty

Skeleton rows, then an Invitations tab with nothing pending.

Phone cards

At 390px every row is a card with a full-width role picker.

Dependencies

motionlucide-reactalert-dialogavatarbadgebuttondialogdropdown-menuempty-stateprogresssearch-inputselecttabletabstag-inputtextareatooltipuse-controllable-stateuse-localemotionnumber-formatrelative-timetext-matchtonesui-config

Files

  • components/ui/members-table.tsx

FAQ

Is the React Members table component free?

The preview, props and docs are free; copying the source needs a Pro plan. See plans.

How do I install Members table?

Run npx wingo-ui@latest add members-table. It also adds the Wingo UI items it builds on (Alert Dialog, Avatar, Badge, Button, Dialog and 18 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/members-table.

How does Members table behave on phones?

Below 768px the role picker, the row menus and the invite dialog open as bottom sheets. The responsive prop controls this and is on by default.

See also

TableData TablePermissions MatrixSettings LayoutTag InputDialog

Used in

Users Admin Page

More React table and list components

Task ListVirtual ListKanbanDeal PipelineAudit LogEditable Grid
All 11 React table and list components

Articles using this component

  • Role Management UI in React: Members, Roles, Audit Log

    Build it: screens and apps · Nov 5, 2026 · 9 min read

PreviousListNext Permissions Matrix

8 of 12 seats1 reserved

Team members
Status2FAMore actions
Daniel Reed

daniel.reed@northwind.app · Sales manager

Active25 min. agoOn
Emma CarterYou

emma.carter@northwind.app · Operations

Admin
ActivenowOn
Grace Kim

grace.kim@northwind.app · Marketing

Active4 days agoOff
James Miller

james.miller@northwind.app · Logistics

Suspended1 wk. agoOn
Lucas Bennett

lucas.bennett@northwind.app · CEO

Owner
Active2 hr. agoOn
Olivia Brooks

olivia.brooks@northwind.app · Customer support

Active12 min. agoOn
Ryan Cooper

ryan.cooper@northwind.app · Account executive

ActiveyesterdayOn
Sophia Lee

sophia.lee@northwind.app · Accounting

Active3 hr. agoOn
  • Daniel Reed

    daniel.reed@northwind.app · Sales manager

    Status
    Active
    Last active
    25 min. ago
    2FA
    On
  • Emma CarterYou

    emma.carter@northwind.app · Operations

    Admin

    You can't change your own role

    Status
    Active
    Last active
    now
    2FA
    On
  • Grace Kim

    grace.kim@northwind.app · Marketing

    Status
    Active
    Last active
    4 days ago
    2FA
    Off
  • James Miller

    james.miller@northwind.app · Logistics

    Status
    Suspended
    Last active
    1 wk. ago
    2FA
    On
  • Lucas Bennett

    lucas.bennett@northwind.app · CEO

    Owner
    Status
    Active
    Last active
    2 hr. ago
    2FA
    On
  • Olivia Brooks

    olivia.brooks@northwind.app · Customer support

    Status
    Active
    Last active
    12 min. ago
    2FA
    On
  • Ryan Cooper

    ryan.cooper@northwind.app · Account executive

    Status
    Active
    Last active
    yesterday
    2FA
    On
  • Sophia Lee

    sophia.lee@northwind.app · Accounting

    Status
    Active
    Last active
    3 hr. ago
    2FA
    On

Change Grace Kim's role to see a failed request roll back; the second try works.

confirmRemove: true · responsive: true · defaultInviteRole: "" · locale: "en-US"