date picker

There's no DatePicker component: put the Calendar in a popover, next to a trigger of your own.

As with shadcn/ui (Popover + Calendar), the trigger, its format, its placeholder and when the popup closes are yours, and qwik-date stays free of runtime dependencies. The recipes below do the same, with full accessibility:

  • the trigger has aria-haspopup="dialog" and aria-expanded, and the popup is a dialog named after the calendar;
  • opening it focuses the selected day, or the first one that can be chosen (autoFocus);
  • Escape closes it, and so does a click outside;
  • choosing a day closes it (a range, once it has an end) and the focus goes back to the trigger, as it does after Escape;
  • the trigger shows the value with formatDate / formatDateRange.
noteonToggle$ goes inside containerProps, an object: the optimizer only turns $ props of JSX attributes into QRLs, so wrap it in $().

native popover

No dependencies and no positioning JavaScript. The calendar is itself the popover (containerProps.popover), the browser closes it on Escape and on clicks outside, and CSS anchor positioning keeps it under the trigger, or above it when there's no room below. Anchor positioning is Baseline since January 2026: Chrome and Edge 125, Safari 26, Firefox 147.

date-picker.tsx

open one: Escape, a click outside or a choice closes it

const date = useSignal<LocalDate | null>(null);

<DatePicker bind:value={date} name="due" />;

For a range, mode="range", formatDateRange and close it once the range has an end:

<button /* ... */>{range.value ? formatDateRange(range.value) : 'Pick dates'}</button>
<Calendar
  autoFocus
  mode="range"
  bind:value={range}
  onChange$={(value) => value.end && document.getElementById(id)?.hidePopover()}
  containerProps={/* the same */}
/>

qwik ui

Not yet: Qwik UI doesn't run on Qwik v2. Its latest release, @qwik-ui/headless 0.7.7, is built for Qwik v1 (@builder.io/qwik). Tried with @qwik.dev/core 2.0.0-rc.4, its Popover renders nothing in a production build (neither the trigger nor the panel, only its children), its components don't typecheck as Qwik v2 components, and in dev the first opening shows an invisible panel. Until Qwik UI supports Qwik v2, use the native popover, or floating-ui: Qwik UI's Popover is a native popover placed by floating-ui too.

floating-ui

For the browsers without CSS anchor positioning (Safari before 26, Firefox before 147), or for floating-ui's middleware: the same native popover, placed by floating-ui while it's open.

$ pnpm add @floating-ui/dom
date-picker.tsx
import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom';
import {
  $,
  type NoSerialize,
  type Signal,
  component$,
  noSerialize,
  useId,
  useSignal,
  useVisibleTask$,
} from '@qwik.dev/core';
import { Calendar, type LocalDate, formatDate } from 'qwik-date';

export const DatePicker = component$<{ 'bind:value': Signal<LocalDate | null>; name?: string }>((props) => {
  const id = useId();
  const date = props['bind:value'];
  const open = useSignal(false);
  const trigger = useSignal<HTMLButtonElement>();
  const stopFollowing = useSignal<NoSerialize<() => void>>();

  // stop following the trigger if the picker goes away while it's open
  useVisibleTask$(({ cleanup }) => cleanup(() => stopFollowing.value?.()));

  return (
    <>
      <button ref={trigger} type="button" popovertarget={id} aria-haspopup="dialog" aria-expanded={open.value}>
        {date.value ? formatDate(date.value) : 'Pick a date'}
      </button>
      <Calendar
        autoFocus
        bind:value={date}
        name={props.name}
        onChange$={() => document.getElementById(id)?.hidePopover()}
        containerProps={{
          id,
          popover: 'auto',
          role: 'dialog',
          // floating-ui sets `left` and `top`
          style: 'inset: auto; margin: 0',
          onToggle$: $((event, popover) => {
            open.value = event.newState === 'open';
            stopFollowing.value?.();
            stopFollowing.value = undefined;
            const anchor = trigger.value;
            if (!anchor) return;
            if (open.value) {
              // under the trigger (above it without room), 8px away, while the page scrolls or resizes
              const place = () =>
                computePosition(anchor, popover, {
                  strategy: 'fixed',
                  placement: 'bottom-start',
                  middleware: [offset(8), flip(), shift({ padding: 8 })],
                }).then(({ x, y }) => Object.assign(popover.style, { left: `${x}px`, top: `${y}px` }));
              stopFollowing.value = noSerialize(autoUpdate(anchor, popover, place));
              return;
            }
            // closed with Escape, a choice or the trigger: the focus goes back to the trigger, unless it went
            // elsewhere (Safari leaves it on the page, or for a moment on the hidden day)
            const focused = document.activeElement;
            if (focused === document.body || popover.contains(focused)) anchor.focus();
          }),
        }}
      />
    </>
  );
});