Seat Reduce

Makes lowering a seat count an explicit choice of who loses access, with the new bill and what happens to their work.

StopsFewer seats locked out an active admin

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/seat-reduce.json

The files land in your project. They're yours to edit.

Usage

seat-reduce-usage.tsx
import { SeatReduce, type SeatMember, type SeatReduction } from "@/components/ui/seat-reduce"

<SeatReduce
  key={run}
  seats={12}
  defaultValue={8}
  members={team.members}
  invites={INVITES}
  pricePerSeat={600}
  currency="INR"
  locale="en-IN"
  renewsAt={team.renewsAt}
  onConfirm={(r) => new Promise<void>((resolve) => setTimeout(() => (setResult(r), resolve()), 600))}
  onCancel={() => setRun((n) => n + 1)}
/>

What it catches

  • More members than the new seat count, until you choose who goes
  • Unaccepted invites suggested first, then least-active plain members
  • Picking an admin, owner or someone active today
  • New bill and when it starts
  • What happens to each removed person's work

Props

SeatReduce props
PropTypeDefault
seats* (required)

Seats paid for today.

number—
value

New seat count. Controlled.

number—
defaultValue

Starting seat count when uncontrolled.

numberseats
onValueChange

Called with the new seat count.

(seats: number) => void—
members* (required)

Current members, with role, last activity and work summary.

SeatMember[]—
invites

Unaccepted invites; they hold seats too.

PendingInvite[][]
pricePerSeat* (required)

Price per seat per period.

number—
currency

ISO 4217 code.

string"USD"
locale

Locale for money and dates.

string"en-US"
period

e.g. "month".

string"month"
renewsAt* (required)

When the new bill (and the loss of access) starts.

When—
workFate

What happens to a removed member's work.

(work: string) => string(work) => `${work} stay in the workspace, owned by you`
onConfirm* (required)

A returned promise shows a pending state; a rejection is caught.

(reduction: SeatReduction) => void | Promise<void>—
onCancel

Called when the user cancels.

() => void—
messages

Overrides any of the sentences in `SEAT_REDUCE_MESSAGES`, e.g. to translate them.

Partial<SeatReduceMessages>—

* required. Also accepts the native props of <section> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Section labelled by its title; member checkboxes have <label>s
  • Seat count input has an aria-label; arrow keys change it and +/- buttons have aria-labels
  • Selection status and warnings render in polite live regions
  • Bill highlight animation skipped under reduced motion

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

This is a UX guard rail. Enforce on the server: Recheck seats, roles and billing server-side; never let a client choice remove the last owner or admin.

  • Activity and work summaries come from the props you pass
  • Does not change billing; onConfirm receives the seats and who goes

What it stops

Fewer seats locked out an active admin

Also known as

seats · licenses · team plan · billing · downgrade · remove members · per seat pricing

Field type

Dependencies

Pairs well with