# Date picker

> A date picker on Vercel's UI SDK where people type a date in their language or pick it, with ranges, presets and ISO values.

Canonical: https://hextaui.com/docs/ui-sdk-date-picker
Last updated: 2026-11-13

> **Beta.** Built on Vercel’s UI SDK 0.1.0-beta.1 (`@ui-sdk/react`), a public beta. Its API may change.

```tsx title="components/examples/ui-sdk-date-picker/demo.tsx"
import {
  DatePicker,
  DatePickerClear,
  DatePickerContent,
  DatePickerControl,
  DatePickerHint,
  DatePickerInput,
  DatePickerTrigger,
} from "@/components/ui/ui-sdk-date-picker"

function UiSdkDatePickerDemo() {
  return (
    <DatePicker defaultValue="2026-11-20" className="max-w-56">
      <DatePickerControl>
        <DatePickerInput aria-label="Due date" />
        <DatePickerClear />
        <DatePickerTrigger />
      </DatePickerControl>
      <DatePickerHint />
      <DatePickerContent />
    </DatePicker>
  )
}

export { UiSdkDatePickerDemo }
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/ui-sdk-date-picker.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @ui-sdk/react@0.1.0-beta.1 @ui-sdk/core@0.1.0-beta.1 @tabler/icons-react cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/ui-sdk-date-picker.tsx"
"use client"

import * as React from "react"
import {
  IconCalendar,
  IconChevronLeft,
  IconChevronRight,
  IconX,
} from "@tabler/icons-react"
import * as DatePickerPrimitive from "@ui-sdk/react/date-picker"
import { cn } from "cn"

import { buttonVariants } from "@/components/ui/button"

type ClassName<State> =
  string | ((state: State) => string | undefined) | undefined

function mergeClassName<State>(base: string, className: ClassName<State>) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

const iconButtonClassName =
  "relative inline-flex size-7 shrink-0 items-center justify-center rounded-[calc(var(--radius-md)-var(--spacing))] text-muted-foreground transition-[color,background-color,box-shadow] duration-150 ease-out-quint outline-none after:absolute after:-inset-1 hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-focus-ring disabled:pointer-events-none disabled:opacity-50 pointer-coarse:after:-inset-2 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0"

function DatePicker({
  className,
  ...props
}: React.ComponentProps<typeof DatePickerPrimitive.DatePicker>) {
  return (
    <DatePickerPrimitive.DatePicker
      className={mergeClassName(
        "group/date-picker relative flex w-full min-w-0 flex-col gap-1.5",
        className
      )}
      {...props}
    />
  )
}

function DatePickerControl({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="date-picker-control"
      className={cn(
        "flex h-9 w-full min-w-0 items-center gap-1 rounded-md bg-transparent pe-1 text-sm text-foreground inset-ring-(length:--hairline) inset-ring-input transition-[background-color,box-shadow] duration-150 ease-out-cubic group-data-invalid/date-picker:inset-ring-destructive group-data-readonly/date-picker:bg-muted/40 group-data-disabled/date-picker:opacity-50 has-[input:focus-visible]:ring-3 has-[input:focus-visible]:ring-focus-ring has-[input:focus-visible]:inset-ring-ring group-data-invalid/date-picker:has-[input:focus-visible]:ring-destructive/20 group-data-invalid/date-picker:has-[input:focus-visible]:inset-ring-destructive motion-reduce:transition-none dark:bg-input/30 dark:group-data-readonly/date-picker:bg-input/20 forced-colors:border pointer-coarse:h-10 [@media(hover:hover)]:hover:not-has-[input:focus-visible]:not-group-data-disabled/date-picker:not-group-data-invalid/date-picker:inset-ring-ring/70",
        className
      )}
      {...props}
    />
  )
}

function DatePickerInput({
  className,
  ...props
}: React.ComponentProps<typeof DatePickerPrimitive.DatePickerInput>) {
  return (
    <DatePickerPrimitive.DatePickerInput
      className={mergeClassName(
        "h-full w-full min-w-0 flex-1 bg-transparent ps-2.5 tabular-nums outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:cursor-not-allowed pointer-coarse:text-[max(16px,1rem)]",
        className
      )}
      {...props}
    />
  )
}

function DatePickerSeparator({
  className,
  ...props
}: React.ComponentProps<"span">) {
  return (
    <span
      aria-hidden="true"
      data-slot="date-picker-separator"
      className={cn("shrink-0 text-muted-foreground", className)}
      {...props}
    >
      –
    </span>
  )
}

function DatePickerTrigger({
  className,
  children,
  ...props
}: React.ComponentProps<typeof DatePickerPrimitive.DatePickerTrigger>) {
  return (
    <DatePickerPrimitive.DatePickerTrigger
      className={mergeClassName(
        cn(iconButtonClassName, "data-open:bg-muted data-open:text-foreground"),
        className
      )}
      {...props}
    >
      {children ?? <IconCalendar aria-hidden="true" />}
    </DatePickerPrimitive.DatePickerTrigger>
  )
}

function DatePickerClear({
  className,
  children,
  ...props
}: React.ComponentProps<typeof DatePickerPrimitive.DatePickerClear>) {
  return (
    <DatePickerPrimitive.DatePickerClear
      aria-label={children ? undefined : "Clear"}
      className={mergeClassName(
        cn(iconButtonClassName, "data-empty:hidden"),
        className
      )}
      {...props}
    >
      {children ?? <IconX aria-hidden="true" />}
    </DatePickerPrimitive.DatePickerClear>
  )
}

function DatePickerHint({
  className,
  ...props
}: React.ComponentProps<typeof DatePickerPrimitive.DatePickerHint>) {
  return (
    <DatePickerPrimitive.DatePickerHint
      className={mergeClassName(
        "text-xs text-muted-foreground tabular-nums",
        className
      )}
      {...props}
    />
  )
}

function DatePickerContent({
  className,
  children,
  align = "start",
  sideOffset = 6,
  collisionPadding = 8,
  ...props
}: React.ComponentProps<typeof DatePickerPrimitive.DatePickerContent>) {
  return (
    <DatePickerPrimitive.DatePickerContent
      align={align}
      sideOffset={sideOffset}
      collisionPadding={collisionPadding}
      className={mergeClassName(
        "m-0 flex max-h-(--available-height) w-auto max-w-(--available-width) origin-(--transform-origin) gap-3 overflow-y-auto overscroll-contain rounded-xl border-0 bg-popover p-3 text-sm text-popover-foreground shadow-md ring-(length:--hairline) ring-foreground/10 transition-[opacity,scale] duration-150 ease-out-quint outline-none [--cell-radius:calc(var(--radius-xl)-var(--spacing)*3)] [--cell-size:--spacing(9)] data-ending-style:opacity-0 data-ending-style:duration-100 data-starting-style:opacity-0 motion-safe:data-ending-style:scale-96 motion-safe:data-starting-style:scale-96 motion-reduce:transition-opacity max-sm:flex-col forced-colors:border pointer-coarse:[--cell-size:--spacing(10)]",
        className
      )}
      {...props}
    >
      {children ?? <DatePickerCalendar />}
    </DatePickerPrimitive.DatePickerContent>
  )
}

function DatePickerCalendar({
  className,
  months = 1,
  ...props
}: React.ComponentProps<"div"> & { months?: number }) {
  return (
    <div
      data-slot="date-picker-calendar"
      className={cn("flex min-w-0 flex-col gap-3", className)}
      {...props}
    >
      <div className="flex items-center gap-1">
        <CalendarPrevious />
        <CalendarHeading />
        <CalendarNext />
      </div>
      <div className="flex flex-col gap-4 sm:flex-row">
        {Array.from({ length: months }, (_, offset) => (
          <CalendarGrid key={offset} offset={offset}>
            <CalendarWeekdays>
              {(index) => <CalendarWeekday index={index} />}
            </CalendarWeekdays>
            <CalendarWeeks>
              {(cell) => <CalendarDay value={cell.date} />}
            </CalendarWeeks>
          </CalendarGrid>
        ))}
      </div>
    </div>
  )
}

function CalendarPrevious({
  className,
  children,
  ...props
}: React.ComponentProps<typeof DatePickerPrimitive.CalendarPrevious>) {
  return (
    <DatePickerPrimitive.CalendarPrevious
      className={mergeClassName(iconButtonClassName, className)}
      {...props}
    >
      {children ?? (
        <IconChevronLeft aria-hidden="true" className="rtl:-scale-x-100" />
      )}
    </DatePickerPrimitive.CalendarPrevious>
  )
}

function CalendarNext({
  className,
  children,
  ...props
}: React.ComponentProps<typeof DatePickerPrimitive.CalendarNext>) {
  return (
    <DatePickerPrimitive.CalendarNext
      className={mergeClassName(iconButtonClassName, className)}
      {...props}
    >
      {children ?? (
        <IconChevronRight aria-hidden="true" className="rtl:-scale-x-100" />
      )}
    </DatePickerPrimitive.CalendarNext>
  )
}

function CalendarHeading({
  className,
  ...props
}: React.ComponentProps<typeof DatePickerPrimitive.CalendarHeading>) {
  return (
    <DatePickerPrimitive.CalendarHeading
      className={mergeClassName(
        "m-0 flex-1 text-center text-sm font-medium",
        className
      )}
      {...props}
    />
  )
}

function CalendarGrid({
  className,
  ...props
}: React.ComponentProps<typeof DatePickerPrimitive.CalendarGrid>) {
  return (
    <DatePickerPrimitive.CalendarGrid
      className={mergeClassName(
        "border-separate border-spacing-x-0 border-spacing-y-0.5",
        className
      )}
      {...props}
    />
  )
}

function CalendarWeekdays({
  className,
  ...props
}: React.ComponentProps<typeof DatePickerPrimitive.CalendarWeekdays>) {
  return (
    <DatePickerPrimitive.CalendarWeekdays className={className} {...props} />
  )
}

function CalendarWeekday({
  className,
  ...props
}: React.ComponentProps<typeof DatePickerPrimitive.CalendarWeekday>) {
  return (
    <DatePickerPrimitive.CalendarWeekday
      className={mergeClassName(
        "h-8 w-(--cell-size) p-0 text-xs font-normal text-muted-foreground",
        className
      )}
      {...props}
    />
  )
}

function CalendarWeeks(
  props: React.ComponentProps<typeof DatePickerPrimitive.CalendarWeeks>
) {
  return <DatePickerPrimitive.CalendarWeeks {...props} />
}

function CalendarDay({
  className,
  ...props
}: React.ComponentProps<typeof DatePickerPrimitive.CalendarDay>) {
  return (
    <DatePickerPrimitive.CalendarDay
      className={mergeClassName(
        "relative size-(--cell-size) cursor-default rounded-(--cell-radius) p-0 text-center text-sm tabular-nums transition-[background-color,color,box-shadow] duration-150 ease-out-quint outline-none select-none hover:bg-muted focus-visible:z-10 focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:ring-inset data-outside:text-muted-foreground/60 data-preview:rounded-none data-preview:bg-muted/60 data-today:bg-info/12 data-today:font-semibold data-today:text-info data-unavailable:text-muted-foreground data-unavailable:line-through data-unavailable:hover:bg-transparent data-[range-end]:rounded-(--cell-radius) data-[range-end]:bg-primary data-[range-end]:text-primary-foreground data-[range-end]:not-data-[range-start]:rounded-s-none data-[range-middle]:rounded-none data-[range-middle]:bg-muted data-[range-middle]:font-normal data-[range-middle]:text-foreground data-[range-start]:rounded-(--cell-radius) data-[range-start]:bg-primary data-[range-start]:text-primary-foreground data-[range-start]:not-data-[range-end]:rounded-e-none data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:hover:bg-primary/90 data-[range-middle]:data-[selected]:bg-muted data-[range-middle]:data-[selected]:text-foreground forced-colors:data-[selected]:outline-2 forced-colors:data-[selected]:-outline-offset-2 forced-colors:data-[selected]:outline-solid data-disabled:pointer-events-none data-disabled:opacity-40",
        className
      )}
      {...props}
    />
  )
}

function DatePickerPreset({
  className,
  ...props
}: React.ComponentProps<typeof DatePickerPrimitive.DatePickerPreset>) {
  return (
    <DatePickerPrimitive.DatePickerPreset
      className={mergeClassName(
        cn(
          buttonVariants({ variant: "ghost", size: "sm" }),
          "justify-start font-normal aria-pressed:bg-muted aria-pressed:font-medium"
        ),
        className
      )}
      {...props}
    />
  )
}

const createDatePickerController =
  DatePickerPrimitive.createDatePickerController

export {
  DatePicker,
  DatePickerControl,
  DatePickerInput,
  DatePickerSeparator,
  DatePickerTrigger,
  DatePickerClear,
  DatePickerHint,
  DatePickerContent,
  DatePickerCalendar,
  DatePickerPreset,
  CalendarPrevious,
  CalendarNext,
  CalendarHeading,
  CalendarGrid,
  CalendarWeekdays,
  CalendarWeekday,
  CalendarWeeks,
  CalendarDay,
  createDatePickerController,
}
export type {
  DateMatchers,
  DatePickerController,
  DatePickerValue,
} from "@ui-sdk/react/date-picker"
```

```tsx title="components/ui/button.tsx"
"use client"

import * as React from "react"
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { IconAlertCircle, IconCircleCheck } from "@tabler/icons-react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"

import { Spinner } from "@/components/ui/spinner"
import {
  useButtonFeedback,
  type ButtonFeedbackOptions,
  type ButtonStatus,
} from "@/hooks/use-button-feedback"

const buttonVariants = cva(
  "group/button relative inline-flex items-center justify-center rounded-md bg-clip-padding text-sm font-medium whitespace-nowrap inset-ring-(length:--hairline) inset-ring-transparent transition-[color,background-color,box-shadow,opacity,translate,scale] duration-200 ease-out-quint outline-none select-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:inset-ring-ring focus-visible:outline-hidden active:duration-100 active:not-aria-[haspopup]:not-aria-busy:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-busy:cursor-progress aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:inset-ring-destructive motion-safe:active:not-aria-[haspopup]:not-aria-busy:scale-[0.97] dark:aria-invalid:ring-destructive/40 dark:aria-invalid:inset-ring-destructive/50 forced-colors:border [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/80",
        outline:
          "bg-background inset-ring-border hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-input/30 dark:inset-ring-input dark:hover:bg-input/50",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
        ghost:
          "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 forced-colors:border-0",
        "ghost-destructive":
          "text-destructive hover:bg-destructive/10 hover:text-destructive aria-expanded:bg-destructive/10 focus-visible:ring-destructive/70 focus-visible:inset-ring-destructive/40 dark:hover:bg-destructive/20 forced-colors:border-0",
        destructive:
          "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:ring-destructive/70 focus-visible:inset-ring-destructive/40 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/70",
        link: "text-primary underline-offset-4 hover:underline forced-colors:border-0",
        success:
          "bg-success text-success-foreground hover:bg-success/90 focus-visible:ring-success/70 focus-visible:inset-ring-success/40",
        error:
          "bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/70 focus-visible:inset-ring-destructive/40 motion-safe:animate-button-shake",
      },
      size: {
        default:
          "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
        xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md pointer-coarse:after:absolute pointer-coarse:after:-inset-y-2.5 pointer-coarse:after:inset-x-0 pointer-coarse:in-data-[slot=button-group]:after:hidden has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3",
        sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md pointer-coarse:after:absolute pointer-coarse:after:-inset-y-1.5 pointer-coarse:after:inset-x-0 pointer-coarse:in-data-[slot=button-group]:after:hidden has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5",
        lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
        icon: "size-9 shrink-0",
        "icon-xs":
          "size-6 shrink-0 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md pointer-coarse:after:absolute pointer-coarse:after:-inset-2.5 pointer-coarse:in-data-[slot=button-group]:after:hidden [&_svg:not([class*='size-'])]:size-3",
        "icon-sm":
          "size-8 shrink-0 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md pointer-coarse:after:absolute pointer-coarse:after:-inset-1.5 pointer-coarse:in-data-[slot=button-group]:after:hidden",
        "icon-lg": "size-10 shrink-0",
        "icon-xl": "size-12 shrink-0 [&_svg:not([class*='size-'])]:size-5",
      },
      shape: {
        default: "",
        pill: "rounded-full in-data-[slot=button-group]:rounded-full",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
      shape: "default",
    },
  }
)

type ButtonVariant = Exclude<
  NonNullable<VariantProps<typeof buttonVariants>["variant"]>,
  "success" | "error"
>

type ButtonClickEvent = Parameters<
  NonNullable<ButtonPrimitive.Props["onClick"]>
>[0]

type ButtonProps = Omit<ButtonPrimitive.Props, "onClick"> &
  Omit<VariantProps<typeof buttonVariants>, "variant"> &
  ButtonFeedbackOptions & {
    variant?: ButtonVariant | null
    onClick?: (event: ButtonClickEvent) => unknown
    feedback?: boolean
    loading?: boolean
    status?: ButtonStatus
    loadingLabel?: React.ReactNode
    successLabel?: React.ReactNode
    errorLabel?: React.ReactNode | ((error: unknown) => React.ReactNode)
  }

const stateTransitionTime = 220
const statuses: ButtonStatus[] = ["idle", "loading", "success", "error"]
const quietVariants: ButtonVariant[] = ["ghost", "link"]
const quietStatusClasses: Partial<Record<ButtonStatus, string>> = {
  success: "text-success hover:text-success",
  error:
    "text-destructive hover:text-destructive motion-safe:animate-button-shake",
}

function isThenable(value: unknown): value is PromiseLike<unknown> {
  return (
    typeof value === "object" &&
    value !== null &&
    typeof (value as PromiseLike<unknown>).then === "function"
  )
}

function useStatusTransition(status: ButtonStatus) {
  const [shown, setShown] = React.useState(status)
  const [leaving, setLeaving] = React.useState<ButtonStatus | null>(null)
  const [transitionKey, setTransitionKey] = React.useState(0)

  if (status !== shown) {
    setLeaving(shown)
    setShown(status)
    setTransitionKey((key) => key + 1)
  }

  React.useEffect(() => {
    if (leaving === null) {
      return
    }
    const timer = setTimeout(() => setLeaving(null), stateTransitionTime)
    return () => clearTimeout(timer)
  }, [leaving, transitionKey])

  return { leaving, transitionKey }
}

function useLayerWidth(status: ButtonStatus, active: boolean) {
  const contentRef = React.useRef<HTMLSpanElement>(null)
  const layersRef = React.useRef(new Map<ButtonStatus, HTMLElement>())
  const observerRef = React.useRef<ResizeObserver | null>(null)
  const statusRef = React.useRef(status)

  const applyWidth = React.useCallback(() => {
    const content = contentRef.current
    const layer = layersRef.current.get(statusRef.current)
    if (!content || !layer) {
      return
    }
    const natural = parseFloat(getComputedStyle(layer).width)
    if (!Number.isFinite(natural)) {
      return
    }
    content.style.width = `${natural}px`
    if (!content.hasAttribute("data-measured")) {
      requestAnimationFrame(() => content.setAttribute("data-measured", ""))
    }
  }, [])

  React.useLayoutEffect(() => {
    statusRef.current = status
    if (active) {
      applyWidth()
    }
  }, [active, applyWidth, status])

  React.useLayoutEffect(() => {
    if (!active || typeof ResizeObserver === "undefined") {
      return
    }
    const observer = new ResizeObserver(applyWidth)
    observerRef.current = observer
    layersRef.current.forEach((layer) => observer.observe(layer))
    document.fonts?.addEventListener?.("loadingdone", applyWidth)
    return () => {
      observer.disconnect()
      observerRef.current = null
      document.fonts?.removeEventListener?.("loadingdone", applyWidth)
    }
  }, [active, applyWidth])

  const layerRef = React.useCallback((node: HTMLElement | null) => {
    if (!node) {
      return
    }
    const layer = node.dataset.layer as ButtonStatus
    layersRef.current.set(layer, node)
    observerRef.current?.observe(node)
    return () => {
      observerRef.current?.unobserve(node)
      if (layersRef.current.get(layer) === node) {
        layersRef.current.delete(layer)
      }
    }
  }, [])

  return { contentRef, layerRef }
}

function composeHandlers<Event>(
  first: ((event: Event) => void) | undefined,
  second: (event: Event) => void
) {
  return (event: Event) => {
    first?.(event)
    second(event)
  }
}

function Button({
  className,
  variant = "default",
  size = "default",
  shape = "default",
  children,
  onClick,
  disabled,
  focusableWhenDisabled,
  feedback,
  loading,
  status: statusProp,
  onStatusChange,
  onError,
  resetAfter,
  loadingLabel,
  successLabel = "Done",
  errorLabel = "Failed",
  onPointerEnter,
  onPointerLeave,
  onFocus,
  onBlur,
  ...props
}: ButtonProps) {
  const internal = useButtonFeedback({ resetAfter, onStatusChange, onError })
  const status = statusProp ?? (loading ? "loading" : internal.status)
  const requested =
    Boolean(feedback) || loading !== undefined || statusProp !== undefined
  const [enabled, setEnabled] = React.useState(requested)
  if (requested && !enabled) {
    setEnabled(true)
  }

  const { leaving, transitionKey } = useStatusTransition(status)
  const iconOnly = size?.startsWith("icon") ?? false
  const widthStatus =
    status === "loading" && loadingLabel === undefined ? "idle" : status
  const { contentRef, layerRef } = useLayerWidth(
    widthStatus,
    enabled && !iconOnly
  )
  const isLoading = status === "loading"
  const quiet = quietVariants.includes(variant ?? "default")
  const resolvedVariant =
    !quiet && status === "success"
      ? "success"
      : !quiet && status === "error"
        ? "error"
        : variant
  const resolvedErrorLabel =
    typeof errorLabel !== "function"
      ? errorLabel
      : internal.error !== undefined
        ? errorLabel(internal.error)
        : "Failed"

  const handleClick = (event: ButtonClickEvent) => {
    if (feedback && internal.isPending()) {
      return
    }
    const result = onClick?.(event)
    if (feedback && isThenable(result)) {
      internal.track(result)
    }
  }

  const layerContent = (layer: ButtonStatus, animated: boolean) => {
    if (layer === "idle") {
      return children
    }
    if (layer === "loading") {
      return (
        <>
          <Spinner aria-hidden size={null} animated={animated} />
          {iconOnly ? null : loadingLabel}
        </>
      )
    }
    if (layer === "success") {
      return (
        <>
          <IconCircleCheck
            className={cn(
              animated &&
                "motion-safe:[&>path:first-child]:animate-button-draw-circle motion-safe:[&>path:first-child]:[stroke-dasharray:57] motion-safe:[&>path:first-child]:[stroke-dashoffset:57] motion-safe:[&>path:last-child]:animate-button-draw-check motion-safe:[&>path:last-child]:[stroke-dasharray:9] motion-safe:[&>path:last-child]:[stroke-dashoffset:9]"
            )}
          />
          {iconOnly ? null : successLabel}
        </>
      )
    }
    return (
      <>
        <IconAlertCircle />
        {iconOnly ? null : resolvedErrorLabel}
      </>
    )
  }

  const button = (
    <ButtonPrimitive
      data-slot="button"
      data-shape={shape ?? "default"}
      data-status={enabled ? status : undefined}
      aria-busy={isLoading || undefined}
      disabled={disabled || isLoading}
      focusableWhenDisabled={focusableWhenDisabled ?? (isLoading || undefined)}
      className={cn(
        buttonVariants({ variant: resolvedVariant, size, shape }),
        quiet && quietStatusClasses[status],
        className
      )}
      onClick={handleClick}
      onPointerEnter={composeHandlers(
        onPointerEnter,
        internal.buttonProps.onPointerEnter
      )}
      onPointerLeave={composeHandlers(
        onPointerLeave,
        internal.buttonProps.onPointerLeave
      )}
      onFocus={composeHandlers(onFocus, internal.buttonProps.onFocus)}
      onBlur={composeHandlers(onBlur, internal.buttonProps.onBlur)}
      {...props}
    >
      {enabled ? (
        <span
          ref={contentRef}
          data-slot="button-content"
          className="relative inline-grid grid-cols-[minmax(0,1fr)] place-items-center gap-[inherit] overflow-x-clip *:col-start-1 *:row-start-1 motion-safe:data-measured:transition-[width] motion-safe:data-measured:duration-220 motion-safe:data-measured:ease-[cubic-bezier(0.2,0,0,1)]"
        >
          {statuses.map((layer) => {
            const active = layer === status
            const exiting = layer === leaving
            const namesButton =
              layer === "idle" && isLoading && loadingLabel === undefined

            return (
              <span
                key={active ? `${layer}-${transitionKey}` : layer}
                ref={layerRef}
                data-slot="button-layer"
                data-layer={layer}
                aria-hidden={active || namesButton ? undefined : true}
                className={cn(
                  "inline-flex items-center justify-center gap-[inherit] backface-hidden",
                  !active &&
                    !exiting &&
                    "pointer-events-none absolute opacity-0",
                  active &&
                    transitionKey > 0 &&
                    "origin-bottom motion-safe:animate-button-state-in motion-reduce:animate-in motion-reduce:animation-duration-150 motion-reduce:fade-in-0",
                  exiting &&
                    "pointer-events-none origin-top opacity-0 motion-safe:animate-button-state-out"
                )}
              >
                {layerContent(layer, active || exiting)}
              </span>
            )
          })}
        </span>
      ) : (
        children
      )}
    </ButtonPrimitive>
  )

  if (!enabled) {
    return button
  }

  const announcement =
    status === "loading"
      ? typeof loadingLabel === "string"
        ? loadingLabel
        : "Loading"
      : status === "success"
        ? typeof successLabel === "string"
          ? successLabel
          : "Done"
        : status === "error"
          ? typeof resolvedErrorLabel === "string"
            ? resolvedErrorLabel
            : "Failed"
          : ""

  return (
    <>
      {button}
      <span data-slot="button-status" role="status" className="sr-only">
        {announcement}
      </span>
    </>
  )
}

export { Button, buttonVariants, useButtonFeedback }
export type { ButtonProps, ButtonStatus, ButtonFeedbackOptions }
```

Update the import paths to match your project setup.

## Usage

UI SDK adds a little CSS of its own in a cascade layer named `ui-sdk`. Declare that layer at the top of your global stylesheet, above the Tailwind import, so your classes win over it.

```css
@layer ui-sdk;

@import "tailwindcss";
```

```tsx
import {
  DatePicker,
  DatePickerContent,
  DatePickerControl,
  DatePickerHint,
  DatePickerInput,
  DatePickerTrigger,
} from "@/components/ui/ui-sdk-date-picker"
```

```tsx
<DatePicker defaultValue="2026-11-20">
  <DatePickerControl>
    <DatePickerInput aria-label="Due date" />
    <DatePickerTrigger />
  </DatePickerControl>
  <DatePickerHint />
  <DatePickerContent />
</DatePicker>
```

Values are ISO date strings like `2026-11-20`, with no time or time zone, so you don’t need a date library. People type in their own date order, like 11/20/2026 or 20.11.2026, and the field formats the date when they press Enter or leave it. `DatePickerContent` with no children renders a styled calendar.

## Composition

```text
DatePicker
├── DatePickerControl
│   ├── DatePickerInput
│   ├── DatePickerSeparator
│   ├── DatePickerClear
│   └── DatePickerTrigger
├── DatePickerHint
└── DatePickerContent
    ├── DatePickerPreset
    └── DatePickerCalendar
        ├── CalendarPrevious
        ├── CalendarHeading
        ├── CalendarNext
        └── CalendarGrid
            ├── CalendarWeekdays
            │   └── CalendarWeekday
            └── CalendarWeeks
                └── CalendarDay
```

## Examples

### Range

With `mode="range"`, add a second input with `end`. The calendar stays open after the first date and closes after the second. `months={2}` shows two months side by side, stacked on small screens.

```tsx title="components/examples/ui-sdk-date-picker/range.tsx"
import {
  DatePicker,
  DatePickerCalendar,
  DatePickerContent,
  DatePickerControl,
  DatePickerInput,
  DatePickerSeparator,
  DatePickerTrigger,
} from "@/components/ui/ui-sdk-date-picker"

function UiSdkDatePickerRange() {
  return (
    <DatePicker
      mode="range"
      months={2}
      pageBy="page"
      defaultValue={["2026-11-18", "2026-11-24"]}
      className="max-w-72"
    >
      <DatePickerControl>
        <DatePickerInput aria-label="Check-in" />
        <DatePickerSeparator />
        <DatePickerInput end aria-label="Check-out" />
        <DatePickerTrigger />
      </DatePickerControl>
      <DatePickerContent>
        <DatePickerCalendar months={2} />
      </DatePickerContent>
    </DatePicker>
  )
}

export { UiSdkDatePickerRange }
```

### Presets

A `DatePickerPreset` picks its `value` and closes the popup. It shows as pressed while its value is selected.

```tsx title="components/examples/ui-sdk-date-picker/presets.tsx"
"use client"

import * as React from "react"

import {
  DatePicker,
  DatePickerCalendar,
  DatePickerContent,
  DatePickerControl,
  DatePickerInput,
  DatePickerPreset,
  DatePickerSeparator,
  DatePickerTrigger,
} from "@/components/ui/ui-sdk-date-picker"

function isoDate(date: Date) {
  const month = String(date.getMonth() + 1).padStart(2, "0")
  const day = String(date.getDate()).padStart(2, "0")
  return `${date.getFullYear()}-${month}-${day}`
}

function daysAgo(days: number) {
  const date = new Date()
  date.setDate(date.getDate() - days)
  return isoDate(date)
}

function UiSdkDatePickerPresets() {
  const [today, setToday] = React.useState<string | null>(null)

  React.useEffect(() => {
    setToday(isoDate(new Date()))
  }, [])

  const presets = today
    ? [
        { label: "Today", value: [today, today] as [string, string] },
        {
          label: "Last 7 days",
          value: [daysAgo(6), today] as [string, string],
        },
        {
          label: "Last 30 days",
          value: [daysAgo(29), today] as [string, string],
        },
        {
          label: "Last 90 days",
          value: [daysAgo(89), today] as [string, string],
        },
      ]
    : []

  return (
    <DatePicker mode="range" className="max-w-72">
      <DatePickerControl>
        <DatePickerInput aria-label="From" />
        <DatePickerSeparator />
        <DatePickerInput end aria-label="To" />
        <DatePickerTrigger />
      </DatePickerControl>
      <DatePickerContent>
        <div className="flex flex-wrap gap-0.5 border-b pb-3 sm:w-32 sm:flex-col sm:flex-nowrap sm:border-e sm:border-b-0 sm:pe-3 sm:pb-0">
          {presets.map((preset) => (
            <DatePickerPreset key={preset.label} value={preset.value}>
              {preset.label}
            </DatePickerPreset>
          ))}
        </div>
        <DatePickerCalendar />
      </DatePickerContent>
    </DatePicker>
  )
}

export { UiSdkDatePickerPresets }
```

### Min, max and unavailable dates

`min`, `max` and `unavailable` apply to typing and picking. Here weekends and two holidays can’t be picked, and a typed one is marked invalid.

```tsx title="components/examples/ui-sdk-date-picker/limits.tsx"
import {
  DatePicker,
  DatePickerContent,
  DatePickerControl,
  DatePickerHint,
  DatePickerInput,
  DatePickerTrigger,
} from "@/components/ui/ui-sdk-date-picker"

function UiSdkDatePickerLimits() {
  return (
    <DatePicker
      min="2026-11-02"
      max="2026-12-18"
      unavailable={[{ weekday: [0, 6] }, "2026-11-26", "2026-11-27"]}
      defaultMonth="2026-11"
      className="max-w-56"
    >
      <DatePickerControl>
        <DatePickerInput aria-label="Delivery date" />
        <DatePickerTrigger />
      </DatePickerControl>
      <DatePickerHint />
      <DatePickerContent />
    </DatePicker>
  )
}

export { UiSdkDatePickerLimits }
```

### Language

`locale` sets the typed format, the hint and the calendar’s names. Without it, the picker reads the page’s `lang`. The buttons have English names, so pass `aria-label` to translate them.

```tsx title="components/examples/ui-sdk-date-picker/locale.tsx"
import {
  DatePicker,
  DatePickerContent,
  DatePickerControl,
  DatePickerHint,
  DatePickerInput,
  DatePickerTrigger,
} from "@/components/ui/ui-sdk-date-picker"

function UiSdkDatePickerLocale() {
  return (
    <DatePicker locale="de" defaultValue="2026-12-03" className="max-w-56">
      <DatePickerControl>
        <DatePickerInput aria-label="Lieferdatum" />
        <DatePickerTrigger aria-label="Datum wählen" />
      </DatePickerControl>
      <DatePickerHint />
      <DatePickerContent aria-label="Datum wählen" />
    </DatePicker>
  )
}

export { UiSdkDatePickerLocale }
```

### Form

With a `name`, the date submits as an ISO string. `required` blocks an empty submit, text that isn’t a date blocks one too, and reset goes back to `defaultValue`.

```tsx title="components/examples/ui-sdk-date-picker/form.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  DatePicker,
  DatePickerContent,
  DatePickerControl,
  DatePickerHint,
  DatePickerInput,
  DatePickerTrigger,
} from "@/components/ui/ui-sdk-date-picker"

function UiSdkDatePickerForm() {
  const [submitted, setSubmitted] = React.useState<string | null>(null)

  return (
    <form
      className="flex w-full max-w-56 flex-col gap-3"
      onSubmit={(event) => {
        event.preventDefault()
        const data = new FormData(event.currentTarget)
        setSubmitted(String(data.get("start")))
      }}
    >
      <DatePicker name="start" required>
        <DatePickerControl>
          <DatePickerInput aria-label="Start date" />
          <DatePickerTrigger />
        </DatePickerControl>
        <DatePickerHint />
        <DatePickerContent />
      </DatePicker>
      <div className="flex items-center gap-3">
        <Button type="submit" size="sm">
          Submit
        </Button>
        <Button type="reset" size="sm" variant="ghost">
          Reset
        </Button>
      </div>
      <p className="text-sm text-muted-foreground tabular-nums">
        {submitted ? `Submitted ${submitted}` : "Nothing submitted yet."}
      </p>
    </form>
  )
}

export { UiSdkDatePickerForm }
```

### Controller

A controller from `createDatePickerController()` sets the date or opens the calendar from anywhere, and `onValueChange` reports each change. Leave `value` unset here, so the calendar can move to the month the controller picks.

```tsx title="components/examples/ui-sdk-date-picker/controller.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  createDatePickerController,
  DatePicker,
  DatePickerContent,
  DatePickerControl,
  DatePickerInput,
  DatePickerTrigger,
  type DatePickerValue,
} from "@/components/ui/ui-sdk-date-picker"

function UiSdkDatePickerController() {
  const [picker] = React.useState(createDatePickerController)
  const [value, setValue] = React.useState<DatePickerValue>(null)

  return (
    <div className="flex w-full max-w-56 flex-col gap-3">
      <DatePicker controller={picker} onValueChange={setValue}>
        <DatePickerControl>
          <DatePickerInput aria-label="Party date" />
          <DatePickerTrigger />
        </DatePickerControl>
        <DatePickerContent />
      </DatePicker>
      <div className="flex gap-2">
        <Button
          variant="outline"
          size="sm"
          onClick={() => picker.setValue("2026-12-25")}
        >
          Choose Christmas
        </Button>
        <Button variant="ghost" size="sm" onClick={() => picker.open()}>
          Open
        </Button>
      </div>
      <p className="text-sm text-muted-foreground tabular-nums">
        {typeof value === "string" ? value : "No date"}
      </p>
    </div>
  )
}

export { UiSdkDatePickerController }
```

### Disabled and read-only

`disabled` stops typing and opening. `readOnly` still opens the calendar so people can look, but nothing can change.

```tsx title="components/examples/ui-sdk-date-picker/disabled.tsx"
import {
  DatePicker,
  DatePickerContent,
  DatePickerControl,
  DatePickerInput,
  DatePickerTrigger,
} from "@/components/ui/ui-sdk-date-picker"

function UiSdkDatePickerDisabled() {
  return (
    <div className="flex w-full max-w-56 flex-col gap-3">
      <DatePicker defaultValue="2026-11-20" disabled>
        <DatePickerControl>
          <DatePickerInput aria-label="Disabled date" />
          <DatePickerTrigger />
        </DatePickerControl>
        <DatePickerContent />
      </DatePicker>
      <DatePicker defaultValue="2026-11-20" readOnly>
        <DatePickerControl>
          <DatePickerInput aria-label="Read-only date" />
          <DatePickerTrigger />
        </DatePickerControl>
        <DatePickerContent />
      </DatePicker>
    </div>
  )
}

export { UiSdkDatePickerDisabled }
```

### Right to left

With an Arabic locale inside dir=rtl, the field, the arrows and the calendar all run from the right.

```tsx title="components/examples/ui-sdk-date-picker/rtl.tsx"
import {
  DatePicker,
  DatePickerContent,
  DatePickerControl,
  DatePickerInput,
  DatePickerTrigger,
} from "@/components/ui/ui-sdk-date-picker"

function UiSdkDatePickerRtl() {
  return (
    <div dir="rtl" className="w-full max-w-56">
      <DatePicker locale="ar" defaultValue="2026-11-20">
        <DatePickerControl>
          <DatePickerInput aria-label="تاريخ الاستحقاق" />
          <DatePickerTrigger aria-label="اختر التاريخ" />
        </DatePickerControl>
        <DatePickerContent aria-label="اختر التاريخ" />
      </DatePicker>
    </div>
  )
}

export { UiSdkDatePickerRtl }
```

## Keyboard

| Key | Action |
| --- | --- |
| `Enter` | In the input, reads the typed date. On a day, picks it. |
| `←` `→` `↑` `↓` | Moves between days, or by a week up and down. |
| `Page Up` `Page Down` | Moves a month back or forward. |
| `Home` `End` | Moves to the start or end of the week. |
| `Esc` | Cancels an unfinished range, or else closes the calendar. |

## Accessibility

The calendar opens as a dialog with a grid of days, each named in full, like “Friday, November 20, 2026”. The hint describes the input, so screen readers hear the format to type. Text that isn’t a date sets `aria-invalid` and keeps the old value, and unavailable days stay focusable so keyboard users still hear them.

The trigger, popup and arrows have English names. Pass `aria-label` to each to translate them. Inputs use a 16px font on touch screens so iOS doesn’t zoom, and the popup only scales in when motion is allowed.

## API reference

Each part wraps the UI SDK part of the same name and accepts all of its props, including `render` and a `className` that can be a function of the part’s state. `DatePickerControl`, `DatePickerSeparator` and `DatePickerCalendar` are HextaUI layout parts.

### DatePicker

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | `"single" \| "range"` | `"single"` |  |
| `value` | `string \| [string, string] \| null` | – |  |
| `defaultValue` | `string \| [string, string] \| null` | – |  |
| `onValueChange` | `(value, details) => void` | – |  |
| `min` | `string` | – |  |
| `max` | `string` | – |  |
| `unavailable` | `DateMatchers` | – | ISO dates, { from, to }, { weekday }, { before }, { after } or a function. |
| `months` | `number` | `1` |  |
| `pageBy` | `"month" \| "page"` | `"month"` |  |
| `locale` | `string` | – |  |
| `weekStartsOn` | `number` | – |  |
| `minDays` | `number` | – |  |
| `maxDays` | `number` | – |  |
| `closeOnSelect` | `boolean` | `true` |  |
| `disabled` | `boolean` | `false` |  |
| `readOnly` | `boolean` | `false` |  |
| `name` | `string` | – |  |
| `startName` | `string` | – |  |
| `endName` | `string` | – |  |
| `required` | `boolean` | `false` |  |
| `open` | `boolean` | – |  |
| `onOpenChange` | `(open, details) => void` | – |  |
| `controller` | `DatePickerController` | – | From createDatePickerController(). |

| Attribute | Description |
| --- | --- |
| `data-slot="date-picker"` | Target the date picker in CSS. |
| `data-open` | Present while open. |
| `data-invalid` | Present while an input holds text that isn’t a date. |
| `data-disabled` | Present when disabled. |
| `data-readonly` | Present when read-only. |

### DatePickerInput

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `end` | `boolean` | `false` | Makes it a range’s end. |

### DatePickerTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | – | Defaults to a calendar icon. |
| `aria-label` | `string` | `"Choose date"` |  |

### DatePickerClear

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | – | Defaults to an X icon named Clear. It hides while there’s nothing to clear. |

### DatePickerContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | – | Defaults to a DatePickerCalendar. |
| `align` | `Align` | `"start"` |  |
| `sideOffset` | `number` | `6` |  |
| `collisionPadding` | `number` | `8` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="date-picker-content"` | Target the popup in CSS. |
| `--cell-size` | The size of each day, larger on touch screens. |

### DatePickerCalendar

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `months` | `number` | `1` | How many grids to render, side by side. |

### DatePickerPreset

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string \| [string, string]` | – |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | – |  |

### CalendarDay

| Attribute | Description |
| --- | --- |
| `data-slot="calendar-day"` | Target a day in CSS. |
| `data-today` | Present on today. |
| `data-range-start` | Present where a range starts. |
| `data-range-end` | Present where a range ends. |
| `data-unavailable` | Present on a day that can’t be picked. |
| `data-outside` | Present on a day of the next or previous month. |

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt

## Related

- [Button](https://hextaui.com/docs/button) - Buttons in every variant and size, with a built-in loading, success and error flow that skips the spinner for fast requests.
- [Calendar](https://hextaui.com/docs/calendar) - A date grid for single, range and multiple selection, with sliding months, range previews and touch-sized days.
- [Checkbox](https://hextaui.com/docs/checkbox) - A checkbox whose check draws in, with indeterminate parents, groups and labels that share its hover.
- [Combobox](https://hextaui.com/docs/combobox) - A filterable select with chips, groups and async results, in a popup that resizes as you type.
- [Date picker](https://hextaui.com/docs/date-picker) - A button that opens a calendar in a popover, or a bottom sheet on phones, for single dates and ranges.
- [Field](https://hextaui.com/docs/field) - Labels, descriptions and errors wired to their control, with validation states and layouts for forms.
