Skip to content
goodtabs UI
BrowseBrowse components

Date preset select

A pill that opens a menu of date range presets, with an optional custom range.

npx shadcn@latest add https://ui.goodtabs.co/r/date-preset-select.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/date-preset-select

Five presets and a Custom range item. Last 7 days is picked.

Without custom

Presets only, no menu heading, running uncontrolled.

In a chart header

Requests

41,208

Right of a card title, switching the figure and bars below.

Usage

import { DatePresetSelect } from "@/components/ui/date-preset-select"

<DatePresetSelect value={range} onValueChange={setRange} onCustom={() => setPickerOpen(true)} />

Props

PropTypeDefaultDescription
valuestringThe chosen preset value. "custom" shows Custom range in the trigger. Omit to run uncontrolled.
defaultValuestring"7d"First value when uncontrolled.
onValueChange(value: string) => voidCalled with the chosen preset value.
presets{ value: string; label: string }[]datePresetsReplaces the five default presets: today, yesterday, 7d, 30d, month.
onCustom() => voidAdds a Custom range item after a separator and runs when it is chosen.
labelstring | null"Date range"Heading inside the menu. null hides it.

Source

date-preset-select/date-preset-select.tsx
"use client";

import * as React from "react";
import { CalendarIcon, ChevronDownIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";

export type DatePreset = {
  value: string;
  label: string;
};

/** The default presets, exported so consumers can extend them. */
export const datePresets: DatePreset[] = [
  { value: "today", label: "Today" },
  { value: "yesterday", label: "Yesterday" },
  { value: "7d", label: "Last 7 days" },
  { value: "30d", label: "Last 30 days" },
  { value: "month", label: "This month" },
];

type DatePresetSelectProps = Omit<
  React.ComponentProps<typeof Button>,
  "value" | "defaultValue" | "size" | "variant" | "children"
> & {
  /** The chosen preset value. "custom" shows Custom range in the trigger. */
  value?: string;
  /** First value when uncontrolled. */
  defaultValue?: string;
  /** Called with the chosen preset value. */
  onValueChange?: (value: string) => void;
  /** Replaces the five default presets. */
  presets?: DatePreset[];
  /** Adds a Custom range item after a separator and runs when it is chosen. */
  onCustom?: () => void;
  /** Heading inside the menu. Pass null to hide it. */
  label?: string | null;
};

const itemClass = "h-8 rounded-sm focus:bg-muted focus:text-foreground";

/**
 * A pill that opens a menu of date range presets as radio items.
 * An optional Custom range item hands off to your own date picker.
 */
export function DatePresetSelect({
  value,
  defaultValue = "7d",
  onValueChange,
  presets = datePresets,
  onCustom,
  label = "Date range",
  className,
  ...props
}: DatePresetSelectProps) {
  const [internal, setInternal] = React.useState(defaultValue);
  const current = value === undefined ? internal : value;
  const currentLabel =
    presets.find((p) => p.value === current)?.label ??
    (current === "custom" ? "Custom range" : (label ?? "Date range"));

  const choose = (next: string) => {
    if (value === undefined) setInternal(next);
    onValueChange?.(next);
  };

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button
          type="button"
          variant="outline"
          size="sm"
          data-slot="date-preset-select"
          className={cn("rounded-full", className)}
          {...props}
        >
          <CalendarIcon data-icon="inline-start" aria-hidden="true" />
          {currentLabel}
          <ChevronDownIcon data-icon="inline-end" className="text-muted-foreground" aria-hidden="true" />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start" className="min-w-44 rounded-md border shadow-md ring-0">
        {label ? <DropdownMenuLabel>{label}</DropdownMenuLabel> : null}
        <DropdownMenuRadioGroup value={current} onValueChange={choose}>
          {presets.map((preset) => (
            <DropdownMenuRadioItem key={preset.value} value={preset.value} className={itemClass}>
              {preset.label}
            </DropdownMenuRadioItem>
          ))}
        </DropdownMenuRadioGroup>
        {onCustom ? (
          <>
            <DropdownMenuSeparator />
            <DropdownMenuItem onSelect={onCustom} className={itemClass}>
              Custom range
            </DropdownMenuItem>
          </>
        ) : null}
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/date-preset-select/prompt.md, or the Copy AI prompt button above.

When to use

  • Above a chart, table or report to switch the period in one click
  • In a toolbar where a full calendar would be too heavy for the common cases
  • When five or so named ranges cover almost every need

When not to use

  • When people pick arbitrary dates most of the time; use a calendar picker directly
  • For comparing two periods; build a dedicated control
  • As a form field that must submit a value; wrap a native select instead

Agent notes

  • A demo or parent that passes onValueChange or onCustom must be a client component
  • Custom range does not change the value; set value to "custom" yourself once your picker has a range and the trigger reads Custom range
  • datePresets is exported; spread it to add or rename presets and pass the result as presets
  • The trigger is an outline sm Button made a pill; any Button prop such as disabled passes through