dateField
A date, date-time, or timestamp picker with optional minimum and maximum dates.
import { dateField } from 'nextjs-cms/core/fields'Basic usage
dateField({ name: 'published_on' })A date picker stored in a DATE column (YYYY-MM-DD).
With every option
dateField({
name: 'starts_at',
label: { en: 'Starts at', ar: 'يبدأ في' },
required: true,
order: 2,
localized: false,
conditionalRules: [{ field: isScheduled, condition: 'equals', value: true }],
format: 'datetime',
minDate: 'now',
maxDate: new Date('2030-12-31T23:59:59Z'),
})Options
| Option | Type | Default | Description |
|---|---|---|---|
format | 'date' | 'datetime' | 'timestamp' | 'date' | What is picked and how it's stored. |
timezone | string | CMS dates.timezone, then UTC | IANA timezone for input, display, defaults and bounds. |
minDate | Date | 'now' | none | Earliest allowed value. 'now' means "not in the past", evaluated at save time. |
maxDate | Date | 'now' | none | Latest allowed value. 'now' means "not in the future". |
defaultValue | Date | 'now' | none | Initial value on the create form. See the known issue below. |
Plus the common options. The field throws on load if minDate is after maxDate.
Options in detail
format
format | Picker | Column | Stored as |
|---|---|---|---|
'date' | Date | DATE | 2026-09-24 |
'datetime' | Date and time | DATETIME | 2026-09-24 14:30:00 |
'timestamp' | Date and time | TIMESTAMP | 2026-09-24 14:30:00.000 |
Datetime and timestamp values are converted to UTC before storage. Date-only values remain calendar dates (YYYY-MM-DD) and are never timezone-shifted. For custom frontend rendering:
import { formatZonedDate, parseStoredDate } from 'nextjs-cms/utils'
const startsAt = parseStoredDate(row.startsAt, 'datetime')
const label = startsAt ? formatZonedDate(startsAt, 'YYYY-MM-DD HH:mm', 'Europe/Istanbul') : ''Prefer 'datetime' for events and publish dates. TIMESTAMP has a range limit (up to 2038) in MySQL.
timezone
Set a default for all date fields in cms.config.ts:
import { defineConfig } from 'nextjs-cms/core/config'
export default defineConfig({ dates: { timezone: 'Europe/Istanbul' } })A field inherits that default, or overrides it:
dateField({ name: 'starts_at', format: 'datetime' })
dateField({ name: 'london_event_at', format: 'datetime', timezone: 'Europe/London' })Resolution is field timezone → CMS dates.timezone → UTC. Invalid timezone names fail configuration validation. With Istanbul selected, entering 2026-09-24 15:00 stores 2026-09-24 12:00:00 UTC and displays 15:00 on every editor's browser. The picker shows the timezone beside the controls.
Datetime form values use UTC ISO strings with Z. Submitted strings with an explicit offset preserve their instant; timezone-free date/time strings are interpreted in the field's zone. Stored SQL strings are always read as UTC. Use explicit Z or offsets when constructing JavaScript Date defaults or bounds; new Date('2026-09-24T15:00:00') already depends on the process timezone before the CMS receives it.
For format: 'date', selected strings and database Date objects assigned through setValue() / setRangeValues() remain calendar dates. Date defaults and bounds, including 'now', use their calendar day in the configured zone. Bounds compare whole days, so minDate: 'now' permits today. Datetime bounds compare instants.
Local times skipped by a daylight-saving change are rejected. When clocks repeat an hour, timezone-free input chooses the earlier occurrence. Submit an explicit offset or UTC ISO value to select the later occurrence; reopening and saving an existing value preserves it.
Upgrading existing installations
CMS database connections now use UTC for both driver conversion and the MySQL session, including pooled connections and transactions. This applies to all queries through the shared CMS pool. The session is initialized once per physical connection. Code sharing the pool must not change its session timezone. Independently created database connections need the same UTC convention.
Changing dates.timezone changes entry and display behavior without rewriting rows. Existing DATETIME values are interpreted as UTC; TIMESTAMP values are retrieved in UTC. Before rollout, audit older values written with other timezone assumptions. Any correction must account for the column type, original write path and historical timezone; do not apply a blanket offset migration. The feature does not repair historical values automatically.
minDate / maxDate
// An event can't start in the past
dateField({ name: 'event_date', minDate: 'now' })
// A birth date can't be in the future
dateField({ name: 'birth_date', maxDate: 'now' })
// Fixed window
dateField({
name: 'campaign_day',
minDate: new Date('2026-11-01'),
maxDate: new Date('2026-11-30'),
})'now' is re-evaluated every time the form is built and every time a value is saved, so it means the current day for date-only fields and the current moment for datetime fields. Saving an item whose stored date is now in the past, with minDate: 'now', fails. Use it for fields that should only hold future dates.
defaultValue
defaultValue accepts a Date or 'now' and pre-fills the create form. 'now' resolves when the form is built.
The default value is also written as the column's SQL DEFAULT, and MySQL rejects DEFAULT 'now' (and a JavaScript
date string) for date columns, so update-sections fails for a dateField with a defaultValue. Until this is
fixed, leave defaultValue out and let editors pick the date, or fill it in a beforeCreate
hook.
Examples
Publish date that defaults to "now" (workaround)
const publishedAt = dateField({ name: 'published_at', format: 'datetime' })
hooks: {
beforeCreate: ({ values }) => {
values.published_at ??= new Date().toISOString().slice(0, 19).replace('T', ' ')
},
}Sort a list by date
const publishedAt = dateField({ name: 'published_at', format: 'datetime', required: true })
db: { table: 'posts', orderByField: publishedAt, index: [{ columns: [publishedAt] }] }Only show future events on your website
import { asc, gte } from 'drizzle-orm'
const upcoming = await db
.select()
.from(EventsTable)
.where(gte(EventsTable.startsAt, new Date().toISOString().slice(0, 19).replace('T', ' ')))
.orderBy(asc(EventsTable.startsAt))Storage
| Column | DATE, DATETIME, or TIMESTAMP |
| Value | Calendar date for date; UTC for datetime and timestamp; NULL when empty |