ComponentsInputs and forms

Time presets menu

A short list of named times that act on one click: snooze a thread, or remind me if they haven't replied. Pick a date opens the calendar for anything else.

Playground

Press the button, then change the controls. The code under the preview lists only the props you changed.

import { TimePresetsMenu } from '@/ui/time-presets-menu';<TimePresetsMenu title="Snooze until" />

Snooze

From the thread's Snooze button. Two presets a recruiter's week needs, each with the day and time it lands on, then Pick a date… for anything else. A pick acts at once and an Undo toast confirms it.

import { ClockIcon } from '@phosphor-icons/react';
import { Button } from '@/ui/button';
import { TimePresetsMenu } from '@/ui/time-presets-menu';
import { toast } from '@/ui/toast';

// From the thread's Snooze button. Two presets, then Pick a date… for anything else. A pick acts at once with an Undo toast.
export default function TimePresetsSnooze() {
  return (
    <TimePresetsMenu
      trigger={
        <Button>
          <ClockIcon />
          Snooze
        </Button>
      }
      title="Snooze until"
      presets={[
        { id: 'tomorrow', label: 'Tomorrow', when: 'Fri 16 Oct, 09:00' },
        { id: 'next-week', label: 'Next week', when: 'Mon 19 Oct, 09:00' },
      ]}
      onSelect={(preset) => toast.add({ title: `Snoozed until ${preset.when}`, actionProps: { children: 'Undo' } })}
      onPickDate={() => toast.add({ title: 'The calendar opens here' })}
    />
  );
}

Remind me, with a default

From Send and remind me. The member's own default sits third, marked "· default" on the selected fill, with one line on what happens next.

import { Button } from '@/ui/button';
import { TimePresetsMenu } from '@/ui/time-presets-menu';
import { toast } from '@/ui/toast';

// From Send and remind me. The member's own default sits third, marked, with one line on what happens next.
export default function TimePresetsRemind() {
  return (
    <TimePresetsMenu
      trigger={<Button variant="primary">Send and remind me</Button>}
      title="Remind me if Maya hasn’t replied"
      presets={[
        { id: 'tomorrow', label: 'Tomorrow', when: 'Fri 16 Oct, 09:00' },
        { id: 'next-week', label: 'Next week', when: 'Mon 19 Oct, 09:00' },
        { id: 'default', label: 'In 3 working days', when: 'Tue 20 Oct, 09:00', isDefault: true },
      ]}
      help="If no reply by then, it comes back to Needs reply with a nudge drafted. A reply cancels it. Weekends don’t count."
      onSelect={(preset) => toast.add({ title: `Sent. Reminder on ${preset.when}`, actionProps: { children: 'Undo' } })}
      onPickDate={() => toast.add({ title: 'The calendar opens here' })}
    />
  );
}

When things go wrong

A long trigger label cuts off; the menu keeps its width. When the snooze fails to save, the error toast says what happened and what to do, and stays until closed. Each preset acts once: the menu closes on the first press.

import { ClockIcon } from '@phosphor-icons/react';
import { Button } from '@/ui/button';
import { TimePresetsMenu } from '@/ui/time-presets-menu';
import { toast } from '@/ui/toast';

// The snooze fails to save: the error toast stays until closed and says what to do. The thread stays where it was.
export default function TimePresetsUnhappy() {
  return (
    <TimePresetsMenu
      trigger={
        <Button>
          <ClockIcon />
          Snooze Zoë O’Brien-Smith’s reply about the Senior Data Engineer role
        </Button>
      }
      title="Snooze until"
      presets={[
        { id: 'tomorrow', label: 'Tomorrow', when: 'Fri 16 Oct, 09:00' },
        { id: 'next-week', label: 'Next week', when: 'Mon 19 Oct, 09:00' },
      ]}
      onSelect={() => toast.add({ type: 'error', title: 'Couldn’t snooze this thread', description: 'Check your connection and try again.' })}
    />
  );
}

Motion

The menu uses the Popup motion, as every menu. Press Replay to close and open it again.

import { ClockIcon } from '@phosphor-icons/react';
import { useState } from 'react';
import { Button } from '@/ui/button';
import { TimePresetsMenu } from '@/ui/time-presets-menu';

// Replay closes the menu, then opens it again, so the 120ms exit and the 160ms entry run back to back.
export default function TimePresetsMotion() {
  const [open, setOpen] = useState(false);
  const replay = () => {
    setOpen(false);
    setTimeout(() => setOpen(true), 400);
  };
  return (
    <div className="flex items-start gap-150">
      <TimePresetsMenu
        trigger={
          <Button>
            <ClockIcon />
            Snooze
          </Button>
        }
        title="Snooze until"
        presets={[
          { id: 'tomorrow', label: 'Tomorrow', when: 'Fri 16 Oct, 09:00' },
          { id: 'next-week', label: 'Next week', when: 'Mon 19 Oct, 09:00' },
        ]}
        onSelect={() => {}}
        open={open}
        onOpenChange={setOpen}
        modal={false}
      />
      <Button variant="ghost" onClick={replay}>
        Replay
      </Button>
    </div>
  );
}

On this page