Skip to content
weftGitHub

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/warp

Usage

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

PropTypeDefaultDescription
datestring—The day to plan.
attendeesAttendee[]—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.
durationnumber—The meeting's starting length, in minutes. Default 30.
durationsnumber[]—The lengths on offer.
titlestring—The heading.
linkstring—The meeting link the form starts with, such as a video call URL.
tagsstring[]—Labels to pick from.
fromnumber—The strip's hours, minutes since midnight.
tonumber—Where the strip's hours end, in minutes since midnight.
timeZonestring—The IANA zone times are shown in, such as Europe/Berlin.
todaystring—Pins today's date, for server rendering and tests. The browser's clock otherwise.
localestring—The language and region used to format values.
hourCyclestring—h12 for AM and PM, h23 for a 24-hour clock. The locale's otherwise.
EventTypeDefaultDescription
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

RelatedBooking calendarAvailability editor