dateRangeField
A start date and an end date picked together, stored in two columns.
import { dateRangeField } from 'nextjs-cms/core/fields'Basic usage
dateRangeField is the one field without a name. Name its two columns instead:
dateRangeField({ startName: 'starts_on', endName: 'ends_on' })Editors pick a range in one control; the table gets two DATE columns, starts_on and ends_on.
With every option
dateRangeField({
startName: 'sale_starts_at',
endName: 'sale_ends_at',
label: { en: 'Sale period', ar: 'فترة التخفيضات' },
required: true,
order: 5,
conditionalRules: [{ field: onSale, condition: 'equals', value: true }],
format: 'datetime',
minDate: 'now',
maxDate: new Date('2027-12-31'),
defaultStartValue: new Date('2026-11-27T00:00:00Z'),
defaultEndValue: new Date('2026-11-30T23:59:59Z'),
})Options
| Option | Type | Default | Description |
|---|---|---|---|
startName | string | required | Column for the start of the range. |
endName | string | required | Column for the end of the range. |
format | 'date' | 'datetime' | 'date' | Column type for both columns. |
timezone | string | CMS dates.timezone, then UTC | IANA timezone shared by both ends. |
minDate | Date | 'now' | none | Earliest allowed value, applied to both ends. |
maxDate | Date | 'now' | none | Latest allowed value, applied to both ends. |
defaultStartValue | Date | none | Initial start date. |
defaultEndValue | Date | none | Initial end date. |
Plus the common options, except name (replaced by startName/endName) and defaultValue (replaced by the two defaults above). When label is omitted, it's generated from startName.
Both ends follow the date field timezone rules: date-only strings are preserved, datetime values use UTC storage and the configured zone for input/display, and defaults and bounds use that zone. Datetime client values are UTC ISO strings. The calendar preserves each existing endpoint's local time when its date changes; a new endpoint starts at midnight in the configured zone.
Options in detail
required
A required range needs both dates. A non-required range can be left empty (both columns NULL).
Referencing the field
Internally the field's name is "<startName>|<endName>". You don't normally need it, but it's what appears if you inspect the field config. Hook values contain the two real column names:
hooks: {
beforeCreate: ({ values }) => {
if (values.sale_starts_at && values.sale_ends_at && values.sale_ends_at < values.sale_starts_at) {
throw new Error('The sale must end after it starts.')
}
},
}The field checks each date against minDate and maxDate, but doesn't check that the end is after the start. Add a
guard like the one above when it matters.
Examples
Event dates
dateRangeField({ startName: 'starts_at', endName: 'ends_at', format: 'datetime', required: true, label: 'When' })Job application window
dateRangeField({ startName: 'opens_on', endName: 'closes_on', label: 'Application window', minDate: 'now' })Currently active items on your website
import { and, gte, lte } from 'drizzle-orm'
const today = new Date().toISOString().slice(0, 10)
const activePromotions = await db
.select()
.from(PromotionsTable)
.where(and(lte(PromotionsTable.startsOn, today), gte(PromotionsTable.endsOn, today)))Storage
| Columns | Two columns, startName and endName, DATE or DATETIME, nullable |
| Values | UTC, YYYY-MM-DD or YYYY-MM-DD HH:mm:ss |