Skip to main content

Lesson 4: Selection Modes

Implementing single, range, and multi-select with useCalendar.

The useCalendar hook

useCalendar wraps the grid builder and selection logic in a stateful React hook. It returns state, actions, and prop getters.

import { NativeAdapter, useCalendar } from '@gentleduck/calendar'
 
const adapter = new NativeAdapter()
 
function MyCalendar() {
  const { state, getDayProps, getGridProps, getHeaderProps, getNavProps } = useCalendar({
    adapter,
    mode: 'single',
  })
 
  // ...render
}
import { NativeAdapter, useCalendar } from '@gentleduck/calendar'
 
const adapter = new NativeAdapter()
 
function MyCalendar() {
  const { state, getDayProps, getGridProps, getHeaderProps, getNavProps } = useCalendar({
    adapter,
    mode: 'single',
  })
 
  // ...render
}

Single selection

The simplest mode. Click a day to select it, click again to deselect.

const { state } = useCalendar({ adapter, mode: 'single' })
// state.value: Date | null
const { state } = useCalendar({ adapter, mode: 'single' })
// state.value: Date | null
function SingleSelect() {
  const { state, getDayProps, getGridProps } = useCalendar({
    adapter,
    mode: 'single',
    onSelect: (date) => console.log('Selected:', date),
  })
 
  return (
    <div {...getGridProps()}>
      {state.weeks.map(week =>
        week.days.map(day => (
          <button key={day.date.getTime()} {...getDayProps(day)}>
            {day.date.getDate()}
          </button>
        ))
      )}
    </div>
  )
}
function SingleSelect() {
  const { state, getDayProps, getGridProps } = useCalendar({
    adapter,
    mode: 'single',
    onSelect: (date) => console.log('Selected:', date),
  })
 
  return (
    <div {...getGridProps()}>
      {state.weeks.map(week =>
        week.days.map(day => (
          <button key={day.date.getTime()} {...getDayProps(day)}>
            {day.date.getDate()}
          </button>
        ))
      )}
    </div>
  )
}

Range selection

Click the start date, then the end date. The range fills in between.

const { state } = useCalendar({ adapter, mode: 'range' })
// state.value: { from: Date, to: Date | null } | null
const { state } = useCalendar({ adapter, mode: 'range' })
// state.value: { from: Date, to: Date | null } | null

When only from is set (one click in), hovering over days shows a preview range. Style the preview through the data-range-middle attribute.


Multi selection

Click to toggle dates on or off.

const { state } = useCalendar({ adapter, mode: 'multi' })
// state.value: Date[]
const { state } = useCalendar({ adapter, mode: 'multi' })
// state.value: Date[]

Controlled vs uncontrolled

Standard React patterns. Control selection externally if needed:

// Uncontrolled (manages its own state)
useCalendar({
  adapter,
  mode: 'single',
  defaultSelected: new Date(2026, 2, 15),
  onSelect: (value) => console.log(value),
})
 
// Controlled (you manage the state)
const [selected, setSelected] = React.useState<Date | null>(null)
useCalendar({
  adapter,
  mode: 'single',
  selected,
  onSelect: setSelected,
})
// Uncontrolled (manages its own state)
useCalendar({
  adapter,
  mode: 'single',
  defaultSelected: new Date(2026, 2, 15),
  onSelect: (value) => console.log(value),
})
 
// Controlled (you manage the state)
const [selected, setSelected] = React.useState<Date | null>(null)
useCalendar({
  adapter,
  mode: 'single',
  selected,
  onSelect: setSelected,
})

Constraining selection

Disable specific dates or set boundaries:

useCalendar({
  adapter,
  mode: 'single',
  // Disable weekends
  disabled: (date) => date.getDay() === 0 || date.getDay() === 6,
  // Only allow dates in March 2026
  fromDate: new Date(2026, 2, 1),
  toDate: new Date(2026, 2, 31),
})
useCalendar({
  adapter,
  mode: 'single',
  // Disable weekends
  disabled: (date) => date.getDay() === 0 || date.getDay() === 6,
  // Only allow dates in March 2026
  fromDate: new Date(2026, 2, 1),
  toDate: new Date(2026, 2, 31),
})