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
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | The chosen preset value. "custom" shows Custom range in the trigger. Omit to run uncontrolled. | |
| defaultValue | string | "7d" | First value when uncontrolled. |
| onValueChange | (value: string) => void | Called with the chosen preset value. | |
| presets | { value: string; label: string }[] | datePresets | Replaces the five default presets: today, yesterday, 7d, 30d, month. |
| onCustom | () => void | Adds a Custom range item after a separator and runs when it is chosen. | |
| label | string | 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