nextjs-cms

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

OptionTypeDefaultDescription
format'date' | 'datetime' | 'timestamp''date'What is picked and how it's stored.
timezonestringCMS dates.timezone, then UTCIANA timezone for input, display, defaults and bounds.
minDateDate | 'now'noneEarliest allowed value. 'now' means "not in the past", evaluated at save time.
maxDateDate | 'now'noneLatest allowed value. 'now' means "not in the future".
defaultValueDate | 'now'noneInitial 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

formatPickerColumnStored as
'date'DateDATE2026-09-24
'datetime'Date and timeDATETIME2026-09-24 14:30:00
'timestamp'Date and timeTIMESTAMP2026-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.

Known issue:

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

ColumnDATE, DATETIME, or TIMESTAMP
ValueCalendar date for date; UTC for datetime and timestamp; NULL when empty