Deck
Browse the catalog

Build / Molecules

MemberRow

  • molecule
  • play-fn

One person on the team: who they are, where their invitation stands, what they can do, and what you can do about it.

Invited and active are the same row on purpose. A seat is spent the moment an invitation goes out, so a pending teammate belongs in the roster they are already being counted in — not in a separate list below it that makes the seat maths look wrong.

isYou is a guard as much as a label: the row you must not remove is the one worth marking.

import { MemberRow } from '@meetcard/deck'

Example

<MemberRow name="Sam Oyelaran" email="sam@meetcard.io" status="active" role={<Select label="Role" hideLabel size="sm" options={roles} />} />

Stories

Every preview below is the real component, rendered in this page from the same story Storybook runs. The width control resizes each preview's container; CSS media queries still follow the browser window, so breakpoint behaviour is worth checking in Storybook.

Default
Source
const Default = () => <Card style={{ maxWidth: 620 }}>
    <MemberRow
        name="Sam Oyelaran"
        email="sam@meetcard.io"
        status="active"
        role={roleSelect('Sam Oyelaran', 'member')} />
</Card>;

The row you must not remove is the one worth marking.

You
Source
const You = () => <Card style={{ maxWidth: 620 }}>
    <MemberRow
        name="Ben Ackles"
        email="ben@meetcard.io"
        status="active"
        isYou
        role={roleSelect('Ben Ackles', 'owner')} />
</Card>;

An invitation nobody has accepted. The email is all the team knows, so it stands in for the name — and the row stays in the roster, because the seat is already spent.

Invited
Source
const Invited = () => <Card style={{ maxWidth: 620 }}>
    <MemberRow
        name="dana@meetcard.io"
        email="Invitation pending"
        status="invited"
        actions={(<>
          <Button size="sm" variant="ghost">
            Resend
          </Button>
          <Button size="sm" variant="ghost">
            Revoke
          </Button>
        </>)} />
</Card>;

Suspended — still in the roster, with the action that brings them back.

Suspended
Source
const Suspended = () => <Card style={{ maxWidth: 620 }}>
    <MemberRow
        name="Marcus Lee"
        email="marcus@meetcard.io"
        status="suspended"
        actions={(<Button size="sm" variant="secondary">Reactivate
                </Button>)} />
</Card>;

The whole roster, active and pending in one list, as the seat count sees it.

Roster
Source
const Roster = () => (
  <Card style={{ maxWidth: 620 }}>
    <MemberRow
      name="Ben Ackles"
      email="ben@meetcard.io"
      isYou
      role={roleSelect('Ben Ackles', 'owner')}
    />
    <MemberRow
      name="Sam Oyelaran"
      email="sam@meetcard.io"
      role={roleSelect('Sam Oyelaran', 'admin')}
    />
    <MemberRow
      name="Prisha Nair"
      email="prisha@meetcard.io"
      role={roleSelect('Prisha Nair', 'member')}
    />
    <MemberRow
      name="dana@meetcard.io"
      email="Invitation pending"
      status="invited"
      actions={
        <Button size="sm" variant="ghost">
          Revoke
        </Button>
      }
    />
  </Card>
);

Props

PropTypeDefaultDescription
namerequiredstring—The person. For an invitation nobody has accepted yet this is the email address, because that is genuinely all the team knows about them.
actionsReactNode—Resend, revoke, remove — whatever this row can do.
avatarSrcstring——
emailReactNode——
isYouboolean—Marks the signed-in person, so nobody removes their own seat by mistake.
roleReactNode—The role: a Select where it can be changed, a Badge where it can't.
status'active' | 'invited' | 'suspended''active'—

Also accepts everything from Omit, so id, data-* and event handlers pass straight through.

Open in Storybookfor controls, the a11y panel and the interaction runner.