Date Picker
A date picker component with calendar popup.
The Date Picker component combines an input field with a calendar popup. Users can click the input or calendar icon to open a calendar and select a date. It features bold borders and shadows that match the Things design system. Built from scratch using React and native HTML elements. No dependencies on any UI library.
Code
TypeScript: Copy this code into components/ui/date-picker.tsx:
tsx
"use client"
import * as React from "react"
import * as ReactDOM from "react-dom"
import { cn } from "@/lib/utils"
import { Calendar } from "./calendar"
import { Input } from "./input"
interface DatePickerContextValue {
open: boolean
setOpen: (open: boolean) => void
value: Date | undefined
setValue: (date: Date | undefined) => void
}
const DatePickerContext = React.createContext<DatePickerContextValue | undefined>(undefined)
const useDatePicker = () => {
const context = React.useContext(DatePickerContext)
if (!context) {
throw new Error("DatePicker components must be used within DatePicker")
}
return context
}
interface DatePickerProps {
value?: Date
defaultValue?: Date
onValueChange?: (date: Date | undefined) => void
placeholder?: string
disabled?: boolean
children: React.ReactNode
className?: string
}
const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>(
({ value: controlledValue, defaultValue, onValueChange, placeholder = "Pick a date", disabled = false, children, className, ...props }, ref) => {
const [uncontrolledValue, setUncontrolledValue] = React.useState<Date | undefined>(defaultValue)
const [open, setOpen] = React.useState(false)
const containerRef = React.useRef<HTMLDivElement>(null)
const isControlled = controlledValue !== undefined
const value = isControlled ? controlledValue : uncontrolledValue
const handleValueChange = React.useCallback((newValue: Date | undefined) => {
if (disabled) return
if (!isControlled) {
setUncontrolledValue(newValue)
}
onValueChange?.(newValue)
setOpen(false)
}, [disabled, isControlled, onValueChange])
React.useEffect(() => {
if (!open) return
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setOpen(false)
}
}
document.addEventListener("mousedown", handleClickOutside)
return () => document.removeEventListener("mousedown", handleClickOutside)
}, [open])
React.useEffect(() => {
if (!open) return
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") {
setOpen(false)
}
}
document.addEventListener("keydown", handleEscape)
return () => document.removeEventListener("keydown", handleEscape)
}, [open])
return (
<DatePickerContext.Provider value={{
open,
setOpen,
value,
setValue: handleValueChange,
}}>
<div ref={containerRef} className={cn("relative w-full", className)} {...props}>
{children}
</div>
</DatePickerContext.Provider>
)
}
)
DatePicker.displayName = "DatePicker"
interface DatePickerInputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const DatePickerInput = React.forwardRef<HTMLInputElement, DatePickerInputProps>(
({ className, placeholder, ...props }, ref) => {
const { open, setOpen, value } = useDatePicker()
const inputRef = React.useRef<HTMLInputElement>(null)
React.useImperativeHandle(ref, () => inputRef.current as HTMLInputElement)
const formatDate = (date: Date | undefined): string => {
if (!date) return ""
const month = date.toLocaleString("default", { month: "short" })
const day = date.getDate()
const year = date.getFullYear()
return `${month} ${day}, ${year}`
}
return (
<div className="relative">
<Input
ref={inputRef}
readOnly
value={value ? formatDate(value) : ""}
placeholder={placeholder}
onClick={() => setOpen(!open)}
className={cn("pl-10 cursor-pointer", className)}
{...props}
/>
<button
type="button"
onClick={() => setOpen(!open)}
className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
</button>
</div>
)
}
)
DatePickerInput.displayName = "DatePickerInput"
interface DatePickerContentProps extends React.HTMLAttributes<HTMLDivElement> {}
const DatePickerContent = React.forwardRef<HTMLDivElement, DatePickerContentProps>(
({ className, ...props }, ref) => {
const { open, setOpen, value, setValue } = useDatePicker()
const contentRef = React.useRef<HTMLDivElement>(null)
React.useImperativeHandle(ref, () => contentRef.current as HTMLDivElement)
React.useEffect(() => {
if (!open || !contentRef.current) return
const content = contentRef.current
const container = content.parentElement?.parentElement
if (!container) return
const input = container.querySelector("input")
if (!input) return
const inputRect = input.getBoundingClientRect()
const contentRect = content.getBoundingClientRect()
const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight
let top = inputRect.bottom + 8
let left = inputRect.left
if (left + contentRect.width > viewportWidth) {
left = viewportWidth - contentRect.width - 8
}
if (top + contentRect.height > viewportHeight) {
top = inputRect.top - contentRect.height - 8
}
if (left < 8) left = 8
if (top < 8) top = 8
content.style.position = "fixed"
content.style.top = `${top}px`
content.style.left = `${left}px`
}, [open])
if (!open) return null
const content = (
<div
ref={contentRef}
className={cn(
"absolute z-50 rounded-lg border-2 border-foreground bg-background neobrutalism-shadow",
className
)}
onClick={(e) => e.stopPropagation()}
{...props}
>
<div className="bg-background text-foreground">
<Calendar
value={value}
onValueChange={setValue}
className="bg-background text-foreground [&_button]:bg-background [&_button]:text-foreground [&_button:hover]:bg-muted [&_button[disabled]]:bg-muted/50 [&_button[disabled]]:text-muted-foreground"
/>
</div>
</div>
)
return ReactDOM.createPortal(content, document.body)
}
)
DatePickerContent.displayName = "DatePickerContent"
export { DatePicker, DatePickerInput, DatePickerContent }JavaScript: Copy this code into components/ui/date-picker.jsx:
jsx
"use client"
import * as React from "react"
import * as ReactDOM from "react-dom"
import { cn } from "@/lib/utils"
import { Calendar } from "./calendar"
import { Input } from "./input"
const DatePickerContext = React.createContext(undefined)
const useDatePicker = () => {
const context = React.useContext(DatePickerContext)
if (!context) {
throw new Error("DatePicker components must be used within DatePicker")
}
return context
}
const DatePicker = React.forwardRef(
({ value: controlledValue, defaultValue, onValueChange, placeholder = "Pick a date", disabled = false, children, className, ...props }, ref) => {
const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue)
const [open, setOpen] = React.useState(false)
const containerRef = React.useRef(null)
const contentRef = React.useRef(null)
const isControlled = controlledValue !== undefined
const value = isControlled ? controlledValue : uncontrolledValue
const handleValueChange = React.useCallback((newValue) => {
if (disabled) return
if (!isControlled) {
setUncontrolledValue(newValue)
}
onValueChange?.(newValue)
setOpen(false)
}, [disabled, isControlled, onValueChange])
React.useEffect(() => {
if (!open) return
const handleClickOutside = (event) => {
if (
containerRef.current && !containerRef.current.contains(event.target) &&
contentRef.current && !contentRef.current.contains(event.target)
) {
setOpen(false)
}
}
const timeoutId = setTimeout(() => {
document.addEventListener("mousedown", handleClickOutside)
}, 100)
return () => {
clearTimeout(timeoutId)
document.removeEventListener("mousedown", handleClickOutside)
}
}, [open])
React.useEffect(() => {
if (!open) return
const handleEscape = (e) => {
if (e.key === "Escape") {
setOpen(false)
}
}
document.addEventListener("keydown", handleEscape)
return () => document.removeEventListener("keydown", handleEscape)
}, [open])
return (
<DatePickerContext.Provider value={{
open,
setOpen,
value,
setValue: handleValueChange,
}}>
<div ref={ref} className={cn("relative w-full", className)} {...props}>
<div ref={containerRef}>
{children}
</div>
{open && ReactDOM.createPortal(
<DatePickerContent ref={contentRef} />,
document.body
)}
</div>
</DatePickerContext.Provider>
)
}
)
DatePicker.displayName = "DatePicker"
const DatePickerInput = React.forwardRef(
({ className, placeholder, ...props }, ref) => {
const { open, setOpen, value } = useDatePicker()
const inputRef = React.useRef(null)
React.useImperativeHandle(ref, () => inputRef.current)
const formatDate = (date) => {
if (!date) return ""
const month = date.toLocaleString("default", { month: "short" })
const day = date.getDate()
const year = date.getFullYear()
return `${month} ${day}, ${year}`
}
return (
<div className="relative">
<Input
ref={inputRef}
readOnly
value={value ? formatDate(value) : ""}
placeholder={placeholder}
onClick={() => setOpen(!open)}
className={cn("pl-10 cursor-pointer", className)}
disabled={props.disabled}
{...props}
/>
<button
type="button"
onClick={() => setOpen(!open)}
className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
disabled={props.disabled}
>
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
</button>
</div>
)
}
)
DatePickerInput.displayName = "DatePickerInput"
const DatePickerContent = React.forwardRef(
({ className, ...props }, ref) => {
const { value, setValue } = useDatePicker()
return (
<div
ref={ref}
className={cn(
"absolute z-50 rounded-lg border-2 border-foreground bg-background neobrutalism-shadow",
className
)}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
data-date-picker-content
{...props}
>
<div className="bg-background text-foreground">
<Calendar
value={value}
onValueChange={setValue}
className="bg-background text-foreground [&_button]:bg-background [&_button]:text-foreground [&_button:hover]:bg-muted [&_button[disabled]]:bg-muted/50 [&_button[disabled]]:text-muted-foreground"
/>
</div>
</div>
)
}
)
DatePickerContent.displayName = "DatePickerContent"
export { DatePicker, DatePickerInput, DatePickerContent }Usage
TypeScript:
tsx
import {
DatePicker,
DatePickerInput,
DatePickerContent,
} from "@/components/ui/date-picker"
import { useState } from "react"
function MyComponent() {
const [date, setDate] = useState<Date | undefined>()
return (
<DatePicker value={date} onValueChange={setDate}>
<DatePickerInput placeholder="Pick a date" />
<DatePickerContent />
</DatePicker>
)
}JavaScript:
jsx
import {
DatePicker,
DatePickerInput,
DatePickerContent,
} from "@/components/ui/date-picker"
import { useState } from "react"
function MyComponent() {
const [date, setDate] = useState(undefined)
return (
<DatePicker value={date} onValueChange={setDate}>
<DatePickerInput placeholder="Pick a date" />
<DatePickerContent />
</DatePicker>
)
}Make sure you also have the lib/utils.ts file with the cn helper function, and thecomponents/ui/calendar.tsx andcomponents/ui/input.tsx components.