Kosorikōsori alpha

Calendar

A date field component that allows users to enter and edit date.

September 2024
SuMoTuWeThFrSa

About

The <Calendar /> component is built on top of React DayPicker.

Installation

Install the primitive

Install the react-day-picker and date-fns packages.

npm install react-day-picker date-fns

Add the button component

The <Calendar /> component requires the <Button /> component. Make sure you have it installed in your project.

Copy-paste the component

Copy and paste the component code in a .tsx file.

'use client';
 
import type { CalendarDay, DayPickerProps, Modifiers } from 'react-day-picker';
import {
  ChevronDownIcon,
  ChevronLeftIcon,
  ChevronRightIcon,
  ChevronUpIcon,
} from '@radix-ui/react-icons';
import { clsx } from 'clsx/lite';
import { DayPicker } from 'react-day-picker';
import { tv } from 'tailwind-variants';
 
import { Button, buttonStyles } from '@kosori/ui/button';
 
const calendarStyles = tv({
  slots: {
    base: 'p-2',
    // classes
    months: 'flex relative',
    month_caption: 'flex justify-center h-7 mx-10 relative items-center',
    weekdays: 'flex flex-row',
    weekday: 'text-grey-text select-none w-8 font-normal text-[0.8rem]',
    month: 'gap-y-4 w-full',
    caption: 'flex justify-center pt-1 relative items-center',
    caption_label: 'text-sm font-medium truncate',
    button_next: buttonStyles({
      variant: 'outline',
      icon: true,
      size: 'small',
      className: clsx(
        'absolute right-0 size-7 bg-transparent p-0 opacity-50',
        'hover:opacity-100',
      ),
    }),
    button_previous: buttonStyles({
      variant: 'outline',
      icon: true,
      size: 'small',
      className: clsx(
        'absolute left-0 size-7 bg-transparent opacity-50',
        'hover:opacity-100',
      ),
    }),
    nav: 'flex items-start',
    month_grid: 'mt-4',
    week: 'flex w-full mt-2',
    day: 'p-0 rounded-lg',
    // day button
    day_button_base: 'bg-transparent font-normal text-grey-text',
    today: clsx(
      'bg-primary-bg text-primary-solid font-medium',
      'hover:bg-primary-bg-hover',
      'active:bg-primary-bg-active',
    ),
    outside: 'pointer-events-none text-grey-text opacity-50',
    disabled: '!bg-transparent !text-grey-line',
    hidden: 'invisible',
    selected: clsx(
      'bg-primary-solid text-primary-base',
      'hover:bg-primary-solid-hover',
      'active:bg-primary-solid-hover',
    ),
    range_start: clsx(
      'rounded-e-none rounded-s-lg bg-primary-solid text-primary-base',
      'hover:bg-primary-solid-hover',
      'active:bg-primary-solid-hover',
    ),
    range_end: clsx(
      'rounded-e-lg rounded-s-none bg-primary-solid text-primary-base',
      'hover:bg-primary-solid-hover',
      'active:bg-primary-solid-hover',
    ),
    range_middle: clsx(
      'rounded-none bg-primary-bg text-primary-text-contrast',
      'hover:bg-primary-bg-hover',
      'active:bg-primary-bg-active',
    ),
    disabled_range_middle: 'bg-primary-solid',
    range_start_end: 'rounded-lg',
  },
});
 
const {
  base,
  months,
  month_caption,
  weekdays,
  weekday,
  month,
  caption,
  caption_label,
  button_next,
  button_previous,
  nav,
  month_grid,
  week,
  day,
  day_button_base,
  today,
  outside,
  disabled,
  hidden,
  selected,
  range_start,
  range_end,
  range_middle,
  disabled_range_middle,
  range_start_end,
} = calendarStyles();
 
export type CalendarProps = DayPickerProps;
 
export const Calendar = ({
  className,
  classNames,
  showOutsideDays = true,
  ...props
}: CalendarProps) => {
  return (
    <DayPicker
      className={base({ className })}
      classNames={{
        months: months(),
        month_caption: month_caption(),
        weekdays: weekdays(),
        weekday: weekday(),
        month: month(),
        caption: caption(),
        caption_label: caption_label(),
        button_next: button_next(),
        button_previous: button_previous(),
        nav: nav(),
        month_grid: month_grid(),
        week: week(),
        day: day(),
        ...classNames,
      }}
      components={{
        DayButton,
        Chevron,
      }}
      showOutsideDays={showOutsideDays}
      {...props}
    />
  );
};
 
Calendar.displayName = 'Calendar';
 
type DayButtonProps = {
  day: CalendarDay;
  modifiers: Modifiers;
} & JSX.IntrinsicElements['button'];
 
const DayButton = ({
  modifiers,
  className,
  ...buttonProps
}: DayButtonProps) => {
  return (
    <Button
      icon
      className={clsx(
        className,
        day_button_base(),
        modifiers.today && today(),
        modifiers.outside && outside(),
        modifiers.disabled && disabled(),
        modifiers.hidden && hidden(),
        modifiers.selected && selected(),
        modifiers.range_start && range_start(),
        modifiers.range_end && range_end(),
        modifiers.range_middle && range_middle(),
        modifiers.disabled && modifiers.range_middle && disabled_range_middle(),
        modifiers.range_start && modifiers.range_end && range_start_end(),
      )}
      size='small'
      variant='ghost'
      {...buttonProps}
      aria-disabled={modifiers.disabled ?? buttonProps['aria-disabled']}
      aria-hidden={modifiers.hidden ?? buttonProps['aria-hidden']}
      aria-selected={modifiers.selected ?? buttonProps['aria-selected']}
    />
  );
};
 
type ChevronProps = {
  className?: string;
  size?: number;
  disabled?: boolean;
  orientation?: 'up' | 'down' | 'left' | 'right';
};
 
const Chevron = ({ orientation, disabled, className }: ChevronProps) => {
  const Component =
    orientation === 'left'
      ? ChevronLeftIcon
      : orientation === 'right'
        ? ChevronRightIcon
        : orientation === 'up'
          ? ChevronUpIcon
          : ChevronDownIcon;
 
  return (
    <Component
      aria-disabled={disabled}
      className={clsx('h-4 w-4', className)}
    />
  );
};

Update import paths

Update the @kosori/ui import paths to fit your project structure, for example, using ~/components/ui.

Usage

import { Calendar } from '~/components/ui/calendar';
const [date, setDate] = useState<Date | undefined>(new Date())
 
return (
  <Calendar
    className='rounded-xl border shadow'
    mode='single'
    selected={selected}
    onSelect={setSelected}
  />
)

See the React DayPicker documentation for more information.

Examples

Form

Your date of birth is used to calculate your age.

On this page