Xzero UI
Pro library

Seat managerPro

A team seats and billing card where avatars and a seat meter show members, invites and open seats against what you pay for, a stepper rolls the prorated charge for today and the new monthly total live, and changes go through a confirm step before anyone is charged. Pending invites can be resent or revoked with undo, and when every seat is taken the card says so and offers to add one.

$ pnpm dlx shadcn@4.23.0 add @xzero-ui-pro/seat-manager
Live · keyboard ready

Put it in the seats or members section of a team billing page. Pass purchased (the subscription quantity), members, invites, plan ({ name, pricePerSeat, interval, currency, maxSeats }) and the real billing period so the proration matches your billing system. Then wire updateSeats(quote, signal), resendInvite and revokeInvite to your backend. Revoke runs after the undo window closes. If updateSeats rejects with an Error, its message appears in the footer with a Try again button. buildQuote is exported so your server can check the same numbers, and onQuoteChange lets you swap in your billing system's own invoice preview if you need it.

When to use

When not to use

Installation

Register both Xzero UI namespaces in your project's components.json:

{
  "registries": {
    "@xzero-ui": "https://mcp.xzero.co.uk/r/{name}.json",
    "@xzero-ui-pro": "https://mcp.xzero.co.uk/r/pro/{name}.json"
  }
}
npx shadcn@4.23.0 add @xzero-ui-pro/seat-manager

Install xzero-foundation once, and import @/components/xzero/foundation.css in your app root. No Xzero token is used by this snapshot. For sample portraits, photos, fonts, or logos, download https://mcp.xzero.co.uk/install-assets.mjs and run node xzero-install-assets.mjs <receiving-project>.

Usage

import { SeatManager } from "@/components/xzero/blocks/seat-manager/seat-manager";

export function TeamBilling({ team }: { team: Team }) {
  return <SeatManager
    workspace={team.name}
    plan={{ name: "Pro", pricePerSeat: 16, interval: "month", maxSeats: 25 }}
    purchased={team.seats}
    members={team.members.map(m => ({ id: m.id, name: m.name, avatar: m.imageUrl }))}
    invites={team.invites.map(i => ({ id: i.id, email: i.email, invitedBy: i.inviter, note: i.sentAgo }))}
    period={{ start: team.periodStart, end: team.periodEnd }}
    paymentMethod={{ brand: "Visa", last4: team.cardLast4 }}
    updateSeats={(quote, signal) => fetch("/api/billing/seats", { method: "POST", body: JSON.stringify({ seats: quote.target }), signal }).then(r => { if (!r.ok) throw new Error("Your card was declined. Nothing was charged."); })}
    resendInvite={(invite, signal) => fetch(`/api/invites/${invite.id}/resend`, { method: "POST", signal }).then(() => undefined)}
    revokeInvite={(invite, signal) => fetch(`/api/invites/${invite.id}`, { method: "DELETE", signal }).then(() => undefined)}
  />;
}

API reference

SeatManager

Team seats and billing in one card. Avatars and a seat meter show members, pending invites and open seats against what is purchased; a stepper changes the seat count while the prorated charge for today and the new recurring total roll live; pending invites can be resent or revoked with undo; and Update seats asks for a confirm step before paying. When every seat is taken it says so and offers one tap to add a seat. With no props it plays a simulated loop on a Pro workspace, and nothing is charged or sent.

PropTypeDefaultDescription
workspacestring"Halcyon"Workspace name shown under the title.
plan{ name: string; pricePerSeat: number; interval?: "month" | "year"; currency?: string; maxSeats?: number }Pro, $16 per seat per month, up to 25 seatsPlan badge, seat price in major units, billing interval, ISO currency and the seat cap. The plus button stops at maxSeats and explains why.
purchasednumber12Seats paid for right now. Becomes the new count after a successful update.
members{ id: string; name: string; avatar?: string }[]10 sample membersMembers holding a seat. The first four show as avatars, the rest as a +N count.
memberCountnumbermembers.lengthTotal members when members is only a sample for the avatars.
invites{ id: string; email: string; invitedBy?: string; note?: string }[]2 sample invitesPending invites. Each reserves a seat until accepted or revoked. note is a short muted suffix such as "2 days ago".
period{ start: Date | string; end: Date | string }Oct 18 to Nov 18 2026Current billing period. Added seats are prorated by the days left; the new total starts at end.
nowDatenew Date() (Oct 27 2026 in the demo)"Today" for proration.
paymentMethod{ brand: string; last4: string } | nullVisa ending 4242Named in the footer and the confirm step. Pass null to hide it.
receiptEmailstring"billing@halcyon.so"Named in the done message.
updateSeats(quote: SeatQuote, signal: AbortSignal) => Promise<void>–Applies the change. SeatQuote is { current, target, delta, dueToday, nextTotal, daysLeft, periodDays, renewsOn }. Reject with an Error and its message shows in the footer with Try again.
resendInvite(invite: SeatInvite, signal: AbortSignal) => Promise<void>–Sends the invite email again. The button shows a spinner, then Sent.
revokeInvite(invite: SeatInvite, signal: AbortSignal) => Promise<void>–Called when the undo window closes. Reject to put the invite back.
undoSecondsnumber3How long Undo stays on a revoked invite.
onQuoteChange(quote: SeatQuote) => void–Called whenever the stepper changes the quote.
onSeatsChange(seats: number, quote: SeatQuote) => void–Called after a change is applied.
onPhaseChange(phase: "idle" | "confirm" | "saving" | "done" | "error") => void–Called on every footer phase change.
autoPlaybooleantrue when no handlers are passedPlays add three seats, pay, resend and revoke on a loop, pausing off screen. Any pointer or key press inside the card hands control to the person.
classNamestring–Extra class on the card.

buildQuote

The proration math the card uses, exported so a server can check the same numbers: buildQuote(current, target, plan, period, now) returns a SeatQuote. Added seats cost delta x price x daysLeft / periodDays, rounded to cents; removals cost nothing today and apply at renewal.

No props.

Keyboard interactions

KeysAction
TabMoves through the stepper buttons, the seat count, invite actions and the footer buttons.
Arrow up / Arrow downOn the seat count (a spinbutton), adds or removes one seat. Arrow right and left do the same.
Page up / Page downOn the seat count, changes by five seats within the limits.
Home / EndOn the seat count, jumps to the fewest seats allowed (members plus invites) or the plan maximum.
Space / EnterActivates the focused button. Focus moves to Pay when the confirm step opens and back to Update seats after Back or a successful update.

Accessibility

Motion

Responsive behavior

Performance

Notes for AI

Guidance for AI tools

Blocks are complete, self-contained screens with sample data. Replace the sample data and connect the callbacks described above. Follow the declared prop types and do not invent props. Keep keyboard access, reduced motion support, and both light and dark themes intact when adapting it.

Full library index: https://ui.xzero.co.uk/llms.txt

Browse all components