MeetingPlanner
Find a time that works for everyone, then send the invite.
SourcePackage @weft-js/warp
Preview
tsx
import { MeetingPlanner, Toaster, toast } from '@weft-js/warp'import type { Attendee } from '@weft-js/warp-core'export function Review() { const sent = () => { toast.show({ title: 'Invite sent', description: 'Everyone has it on their calendar.' }); return pause(400) } const search = (q: string) => Promise.resolve(TEAM_MORE.filter(p => p.name.toLowerCase().includes(q.toLowerCase()))) return ( <> <MeetingPlanner date={TODAY} attendees={TEAM_ROOM} loadAttendees={search} title="Capacity review" link="meet.weft.dev/capacity" tags={['Internal', 'Incident', 'Planning']} timeZone={TEAM_ZONE} today={TODAY} locale="en-GB" hourCycle="h23" onSubmit={sent} onUndo={() => toast.show({ title: 'Invite withdrawn' })} /> <Toaster /> </> )}const TODAY = '2026-11-03'const TEAM_ZONE = 'Europe/Stockholm'const TEAM_ROOM: Attendee[] = [ { id: 'mara', name: 'Mara Lindqvist', organizer: true, zone: TEAM_ZONE, busy: [[540, 555], [600, 660], [780, 825]] }, { id: 'jun', name: 'Jun Tanaka', zone: 'Asia/Tokyo', hours: [540, 1080], busy: [[540, 555], [780, 825]] }, { id: 'priya', name: 'Priya Raman', zone: 'Asia/Kolkata', hours: [540, 1080], busy: [[600, 660], [870, 930]] }, { id: 'sam', name: 'Sam Okafor', zone: 'Africa/Lagos', hours: [540, 1080], busy: [[690, 750], [780, 825]] },]const TEAM_MORE: Attendee[] = [{ id: 'lena', name: 'Lena Fischer', zone: 'Europe/Berlin', busy: [[900, 960]] }, { id: 'tomas', name: 'Tomás Oliveira', zone: 'Europe/Lisbon', busy: [[600, 660]] }]const pause = (ms: number) => new Promise(done => { setTimeout(done, ms) })Installation
npm install @weft-js/warpUsage
tsx
import { MeetingPlanner } from '@weft-js/warp'Accessibility
- The suggested times are a radio group, and the availability is a table, so it reads without the colours.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
date | string | — | The day to plan. |
attendees | Attendee[] | — | Who is invited, with their busy ranges in minutes. |
loadAttendees | ((q: string, signal: AbortSignal) => Promise<Attendee[]>) | — | Search for people to add. |
loadSlots | ((input: { attendees: Attendee[]; date: string; duration: number; }) => … | — | Your own suggestions, in place of working them out from busy. |
duration | number | — | The meeting's starting length, in minutes. Default 30. |
durations | number[] | — | The lengths on offer. |
title | string | — | The heading. |
link | string | — | The meeting link the form starts with, such as a video call URL. |
tags | string[] | — | Labels to pick from. |
from | number | — | The strip's hours, minutes since midnight. |
to | number | — | Where the strip's hours end, in minutes since midnight. |
timeZone | string | — | The IANA zone times are shown in, such as Europe/Berlin. |
today | string | — | Pins today's date, for server rendering and tests. The browser's clock otherwise. |
locale | string | — | The language and region used to format values. |
hourCycle | string | — | h12 for AM and PM, h23 for a 24-hour clock. The locale's otherwise. |
| Event | Type | Default | Description |
|---|---|---|---|
onSubmit | (m: MeetingDetails) => void | Promise<unknown> | — | Send was pressed. |
onUndo | (() => void) | — | Adds Undo after sending. |
Parts
Every element carries a stable name, so you can style it from your own CSS: [data-scope="calendar"] [data-part="eyebrow"]
eyebrowtimelinelane-labellanerootslotsslot