Date/time features
By default, React Query Builder components and utilities handle dates and times in a generic, unopinionated way. We recommend storing dates as strings in ISO 8601-compatible format and taking advantage of built-in date/time functionality like "date" and "datetime-local" inputs (see inputType).
The @react-querybuilder/datetime package augments React Query Builder with enhanced date/time functionality.
Initialization
A date/time processor library with parsing and formatting capabilities must be used with @react-querybuilder/datetime. Ready-to-use plugins are provided for Day.js, date-fns, and Luxon. Other third-party or custom date/time libraries can be used (see below).
A plugin using only native JavaScript
DateandStringfunctionality is available, but we don't recommend it except as a last resort since it has limited formatting capability (full ISO strings in UTC only) and hasn't passed rigorous testing like the popular libraries.
The documentation below assumes the use of the Day.js plugin. To use one of the others, replace @react-querybuilder/datetime/dayjs with @react-querybuilder/datetime/date-fns or @react-querybuilder/datetime/luxon.
Export
import { datetimeRuleProcessorSQL } from '@react-querybuilder/datetime/dayjs';
// Other options:
// import { datetimeRuleProcessorSQL } from '@react-querybuilder/datetime/date-fns';
// import { datetimeRuleProcessorSQL } from '@react-querybuilder/datetime/luxon';
The date/time package provides formatQuery rule processors that handle date/time fields appropriately for the target platform.
Conditional use
By default, the date/time rule processors only treat a rule value as a date (or series of dates) if the field configuration has a datatype property starting with "date", "datetime", "datetimeoffset", or "timestamp" (using the defaultIsDateField function). Otherwise, rule processing is passed to the default rule processor for that export format.
You can customize this algorithm by passing a context.isDateField configuration in the formatQuery options. isDateField can be a boolean, a function that returns a boolean, an object matching field properties, or an array of objects matching field properties.
- As a
boolean,truetreats the rule value as a date, andfalsefalls back to the default rule processor. - As a
function, the function receives the rule object and options object (the same two arguments as the rule processor). The function should return abooleanindicating whether the rule value should be treated as a date. - As an object, fields that match all properties of the object are treated as dates.
- As an array of objects, fields that match all properties of at least one object in the array are treated as dates.
In the example below, the value in the "birthDate" rule matches the regular expression in the isDateField function, so the corresponding SQL output has the date keyword prepended to the value string (per the "postgresql" preset). The "mathNotDate" rule value does not match the pattern and is processed by the default SQL rule processor.
// Returns true if the value appears to be an ISO date-only string (YYYY-MM-DD)
const isDateField = (rule, opts) => /^\d\d\d\d-\d\d-\d\d$/.test(rule.value);
const query: RuleGroupType = {
combinator: 'and',
rules: [
{ field: 'birthDate', operator: '<', value: '1950-01-01' },
{ field: 'mathNotDate', operator: '=', value: '1950-1-1' },
],
};
formatQuery(query, {
preset: 'postgresql',
ruleProcessor: datetimeRuleProcessorSQL,
context: { isDateField },
});
// `(birthDate < date'1950-01-01' and mathNotDate = '1950-1-1')`
In the next example, isDateField is an array of objects. If the field object (options.fieldData) matches all properties of any element in the array, the field is treated as a date. Note that this method depends on the fields array being passed in the formatQuery options.
// Triggers date processing if the field has `datatype: "date"` _or_ `inputType: "datetime-local"`
const isDateField = [{ datatype: 'date' }, { inputType: 'datetime-local' }];
const fields: Field[] = [
{ name: 'birthDate', label: 'Birth Date', datatype: 'date' },
{ name: 'mathNotDate', label: 'Math, Not Date', datatype: 'number' },
];
const query: RuleGroupType = {
combinator: 'and',
rules: [
{ field: 'birthDate', operator: '<', value: '1950-01-01' },
{ field: 'mathNotDate', operator: '=', value: '1950-1-1' },
],
};
formatQuery(query, {
preset: 'postgresql',
fields,
ruleProcessor: datetimeRuleProcessorSQL,
context: { isDateField },
});
// `(birthDate < date'1950-01-01' and mathNotDate = '1950-1-1')`
SQL
The datetimeRuleProcessorSQL rule processor produces different output based on the preset option, which defaults to "ansi". For example, if preset is "postgresql", date values are prefixed with date (e.g., date'2000-01-01'), but for "mssql" they're wrapped in cast([...] as date) (e.g., cast('2000-01-01' as date)).
MongoDB
Since the datetimeRuleProcessorMongoDBQuery rule processor handles real date/time values (as Date objects), it should be used with the "mongodb_query" format, not "mongodb".
import { datetimeRuleProcessorMongoDBQuery } from '@react-querybuilder/datetime/dayjs';
const mongodbQuery = formatQuery(query, {
format: 'mongodb_query',
ruleProcessor: datetimeRuleProcessorMongoDBQuery,
});
JsonLogic
The datetimeRuleProcessorJsonLogic rule processor produces custom JsonLogic operations to indicate that rules should be handled as date/time values. Like jsonLogicAdditionalOperators from the react-querybuilder package, the date/time package provides an easy way to add support for its custom operations with the jsonLogicDateTimeOperations object.
import { add_operation, apply } from 'json-logic-js';
import { jsonLogicDateTimeOperations } from '@react-querybuilder/datetime/dayjs';
for (const [op, func] of Object.entries(jsonLogicDateTimeOperations)) {
add_operation(op, func);
}
const jsonLogic = formatQuery(query, {
format: 'jsonlogic',
ruleProcessor: datetimeRuleProcessorJsonLogic,
});
const results = data.filter(d => apply(jsonLogic, d));
Common Expression Language (CEL)
import { datetimeRuleProcessorCEL } from '@react-querybuilder/datetime/dayjs';
const cel = formatQuery(query, { format: 'cel', ruleProcessor: datetimeRuleProcessorCEL });
JSONata
import { datetimeRuleProcessorJSONata } from '@react-querybuilder/datetime/dayjs';
const jsonata = formatQuery(query, {
format: 'jsonata',
ruleProcessor: datetimeRuleProcessorJSONata,
});
Cypher / GQL
The datetimeRuleProcessorCypher rule processor handles the "cypher" (and equivalent "gql") export format. Date values are wrapped in Cypher temporal constructors: date('...') for date-only values and datetime('...') for full timestamps.
Two custom operators provide duration math support:
olderThanDuration— checks if the elapsed time since a field value exceeds an ISO 8601 duration:datetime() - field > duration('P30D')withinDuration— checks if the elapsed time is within a duration:datetime() - field <= duration('P7D')
import { datetimeRuleProcessorCypher } from '@react-querybuilder/datetime/dayjs';
// Other options:
// import { datetimeRuleProcessorCypher } from '@react-querybuilder/datetime/date-fns';
// import { datetimeRuleProcessorCypher } from '@react-querybuilder/datetime/luxon';
const cypher = formatQuery(query, {
format: 'cypher',
ruleProcessor: datetimeRuleProcessorCypher,
});
// e.g. `n.birthDate < date('1950-01-01')`
// or: `datetime() - n.lastLogin > duration('P30D')`
SPARQL
The datetimeRuleProcessorSPARQL rule processor handles the "sparql" export format. Date values are emitted as XSD typed literals: "..."^^xsd:date for date-only values and "..."^^xsd:dateTime for full timestamps.
Duration math operators work similarly to the Cypher variant, using NOW() and xsd:duration:
olderThanDuration→NOW() - ?field > "P30D"^^xsd:durationwithinDuration→NOW() - ?field <= "P7D"^^xsd:duration
import { datetimeRuleProcessorSPARQL } from '@react-querybuilder/datetime/dayjs';
// Other options:
// import { datetimeRuleProcessorSPARQL } from '@react-querybuilder/datetime/date-fns';
// import { datetimeRuleProcessorSPARQL } from '@react-querybuilder/datetime/luxon';
const sparql = formatQuery(query, {
format: 'sparql',
ruleProcessor: datetimeRuleProcessorSPARQL,
});
// e.g. `?birthDate < "1950-01-01"^^xsd:date`
// or: `NOW() - ?lastLogin > "P30D"^^xsd:duration`
Natural language
The datetimeRuleProcessorNL formats date/time values using Intl.DateTimeFormat. By default, the formatter is instantiated as follows:
// For date-only values, e.g. "1969-01-01":
new Intl.DateTimeFormat(undefined, { dateStyle: 'full' });
// For date+time values, e.g. "1969-01-01T12:14:26.052Z":
new Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' });
To customize the output, use the context option. The locales property is passed as the first argument to the Intl.DateTimeFormat constructor, and either dateFormat or dateTimeFormat—depending on the rule—is passed as the second argument.
import { datetimeRuleProcessorNL } from '@react-querybuilder/datetime/dayjs';
const nl = formatQuery(query, {
format: 'natural_language',
ruleProcessor: datetimeRuleProcessorNL,
context: {
locales: 'en-GB',
dateFormat: { dateStyle: 'full' },
dateTimeFormat: { dateStyle: 'full', timeStyle: 'long' },
},
});
Components
The QueryBuilderDateTime context provider augments ValueEditor with date/time functionality. Wrap your QueryBuilder with it to enable enhanced date/time editing for any field whose inputType is "date"/"datetime-local" or whose datatype begins with "date", "datetime", or "timestamp". Non-date fields delegate to the value editor inherited from a surrounding compat/theme provider (e.g. QueryBuilderMantine), falling back to the standard value editor when none is present. The themed editor is likewise used for the absolute date input and the relative offset input, so the whole control stays consistent with your theme.
import { QueryBuilderDateTime } from '@react-querybuilder/datetime';
const App = () => (
<QueryBuilderDateTime>
<QueryBuilder fields={fields} query={query} onQueryChange={setQuery} />
</QueryBuilderDateTime>
);
QueryBuilderDateTime composes with other context providers (compat/theme packages and @react-querybuilder/dnd). Nest it inside the theme provider so themed sub-controls (selectors, inputs) are used:
<QueryBuilderMantine>
<QueryBuilderDateTime>
<QueryBuilderDnD dnd={dnd}>
<QueryBuilder /* ... */ />
</QueryBuilderDnD>
</QueryBuilderDateTime>
</QueryBuilderMantine>
Relative date/time values
Date/time fields support relative values such as "three months ago" or "the beginning of this year" in addition to absolute (ISO 8601 string) values. By default the value editor renders a compact absolute/relative toggle button; in relative mode it shows an anchor selector, a signed offset, and a unit selector (the unit is hidden when the offset is 0). How the user switches modes is pluggable — see Mode controllers below.
A relative value is stored as a structured object (absolute values remain plain strings, preserving backward compatibility):
interface RelativeDateTimeValue {
mode: 'relative';
// Zero-offset reference point:
anchor:
| 'now'
| 'startOfDay'
| 'endOfDay'
| 'startOfWeek'
| 'endOfWeek'
| 'startOfMonth'
| 'endOfMonth'
| 'startOfYear'
| 'endOfYear';
// Signed offset from the anchor (negative = past, positive = future, 0 = exact anchor):
offset: number;
unit: 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year';
}
The final date/time is computed as: apply the anchor to the current date/time, then add offset units of unit.
The recommended way to enable the relative-capable editor is to wrap your QueryBuilder in QueryBuilderDateTime. With no props it provides the zero-config toggle experience:
import { QueryBuilderDateTime } from '@react-querybuilder/datetime';
<QueryBuilderDateTime>
<QueryBuilder fields={fields} />
</QueryBuilderDateTime>;
To use the relative-capable editor on its own (e.g. when providing custom controlElements), import RelativeDateTimeValueEditor directly. Without a surrounding QueryBuilderDateTime it falls back to the default config (toggle controller, default anchors/units/labels):
import { RelativeDateTimeValueEditor } from '@react-querybuilder/datetime';
<QueryBuilder fields={fields} controlElements={{ valueEditor: RelativeDateTimeValueEditor }} />;
Mode controllers
The strategy for switching a rule between absolute and relative entry is delegated to a pluggable RelativeDateTimeModeController. A controller decides the current mode and optionally renders an affordance to change it — the stored value shape is never affected (absolute = ISO string, relative = RelativeDateTimeValue object), so all processors behave identically regardless of how the mode was chosen.
interface RelativeDateTimeModeController {
// Derive the current mode from the rule (its operator and/or value):
isRelative: (props: ValueEditorProps) => boolean;
// Optional affordance rendered inside the editor to switch modes. Omit when
// the mode is selected elsewhere (e.g. via the operator selector):
ModeControl?: ComponentType<RelativeDateTimeModeControlProps>;
}
Two controllers ship with the package:
Toggle controller (default)
toggleModeController derives the mode from the value shape and renders a compact role="switch" toggle button. Its glyphs are supplied by CSS pseudo-elements (calendar/clock) keyed on aria-checked, and can be overridden via toggleLabels. This is the default — no configuration needed.
Operator-driven controller
An operator-driven controller derives the mode from the rule's operator instead of rendering a toggle button — the operator selector is the mode affordance. Build one with createOperatorModeController, passing the operator name(s) (or a predicate) that should put the editor in relative mode:
import { createOperatorModeController } from '@react-querybuilder/datetime';
// From an operator list:
const controller = createOperatorModeController(['relativeEq', 'relativeBefore']);
// Or a predicate:
const controller = createOperatorModeController(op => op.startsWith('relative'));
Register those operators on your date fields with withRelativeOperators(fields, operators), then pass the controller to QueryBuilderDateTime:
import {
QueryBuilderDateTime,
createOperatorModeController,
withRelativeOperators,
} from '@react-querybuilder/datetime';
const relativeOperators = [
{ name: 'relativeEq', value: 'relativeEq', label: 'is (relative)' },
{ name: 'relativeBefore', value: 'relativeBefore', label: 'is before (relative)' },
];
const controller = createOperatorModeController(relativeOperators.map(o => o.name));
const fieldsWithRelative = withRelativeOperators(fields, relativeOperators);
<QueryBuilderDateTime modeController={controller}>
<QueryBuilder fields={fieldsWithRelative} />
</QueryBuilderDateTime>;
withRelativeOperators appends the given operators to each field's operators list (falling back to the core defaultOperators for fields without an explicit list). Selecting one of those operators puts the editor in relative mode; the stored value remains a RelativeDateTimeValue.
Exporting operator-driven values
The operator names registered above signal mode only — they aren't real comparison operators, so processors don't know how to export them on their own. Map each one to a real operator at format time via context.relativeOperatorMap:
formatQuery(query, {
format: 'sql',
preset: 'postgresql',
ruleProcessor: datetimeRuleProcessorSQL,
context: {
relativeOperatorMap: { relativeEq: '=', relativeBefore: '<' },
},
});
Each date/time processor resolves a rule's operator through this map before exporting. Operators absent from the map pass through unchanged (no warning is emitted), so you only need to map the operators you introduced. To inspect how operators and values are being resolved, use the format: 'diagnostics' output.
Custom controllers
Supply your own object to fully control the UX — for example, a controller that derives the mode purely from the value shape and renders no affordance:
const valueShapeController = {
isRelative: props => typeof props.value === 'object' && props.value?.mode === 'relative',
};
Configuration
QueryBuilderDateTime accepts optional RelativeDateTimeEditorConfig props. Omitting all of them yields the zero-config toggle experience; any provided option merges over the defaults.
| Prop | Type | Default | Description |
|---|---|---|---|
modeController | RelativeDateTimeModeController | toggleModeController | Absolute/relative switching strategy. |
anchors | FullOption[] | defaultRelativeDateTimeAnchors | Anchor (reference-point) options. |
units | FullOption[] | defaultRelativeDateTimeUnits | Unit options. |
toggleLabels | RelativeDateTimeToggleLabels | (see below) | Content/labels for the toggle controller's button. |
dateTimeAPI | RQBDateTimeLibraryAPI | Day.js adapter | Library adapter used to parse/validate editor input. |
By default the absolute date editor parses and validates its input with Day.js. If you export queries with a processor backed by a different library (Luxon, date-fns, native Date, etc.), pass the matching adapter as dateTimeAPI so editor parsing and export stay consistent. See Custom plugins for the RQBDateTimeLibraryAPI shape.
toggleLabels lets you override the toggle's accessible label, tooltips, and rendered content (e.g. to use icon components or localized text instead of the default CSS glyphs):
<QueryBuilderDateTime
toggleLabels={{
label: 'Switch date entry mode',
absoluteTitle: 'Absolute date (click for relative)',
relativeTitle: 'Relative date (click for absolute)',
absoluteContent: <CalendarIcon />,
relativeContent: <ClockIcon />,
}}
anchors={[
{ name: 'now', value: 'now', label: 'right now' },
{ name: 'startOfDay', value: 'startOfDay', label: 'midnight' },
]}>
<QueryBuilder fields={fields} />
</QueryBuilderDateTime>
Between ranges
For the between and notBetween operators the editor renders two independent value editors, one per bound, each with its own mode affordance. This lets the "from" and "to" bounds be set independently — either can be absolute or relative, and they can be mixed (e.g. an absolute "from" and a relative "to"):
const query: RuleGroupType = {
combinator: 'and',
rules: [
{
field: 'lastLogin',
operator: 'between',
value: [
'2024-01-01', // absolute "from"
{ mode: 'relative', anchor: 'now', offset: 0, unit: 'day' }, // relative "to"
],
},
],
};
The bounds are always stored as a two-element array, regardless of the listsAsArrays option. Relative bounds are objects, which can't survive the comma-joining used for ordinary between values, so the array is emitted directly. All processors already handle relative values in either (or both) array slots.
Materializing relative values
When exporting a query, relative values are resolved according to the target format:
- SQL emits a live, dialect-specific symbolic expression by default (e.g. PostgreSQL
cast(date_trunc('year', current_date) as date), SQLitedate('now', 'start of year')), so the value stays relative to query-execution time. - JsonLogic is always live, via a custom
dateRelativeoperation resolved at evaluation time. - All other formats materialize the relative value to a concrete ISO 8601 string at format time.
Two context options control this behavior:
context.materializeRelativeDateTime— whentrue, relative values are resolved to concrete literals for all formats, including SQL.context.relativeDateTimeBase— aDatethat pins "now" for deterministic output (useful for testing or reproducible exports).
formatQuery(query, {
format: 'sql',
preset: 'postgresql',
ruleProcessor: datetimeRuleProcessorSQL,
context: {
materializeRelativeDateTime: true,
relativeDateTimeBase: new Date('2024-01-15T00:00:00Z'),
},
});
Custom plugins
If the official date/time processor plugins do not meet your requirements, you can provide a custom plugin that conforms to the RQBDateTimeLibraryAPI interface:
type DateOrString = string | Date;
interface RQBDateTimeLibraryAPI {
/** Format a `Date` or ISO 8601 string with format `fmt` */
format: (d: DateOrString, fmt: string) => string;
/** `a` is after `b`. */
isAfter: (a: DateOrString, b: DateOrString) => boolean;
/** `a` is before `b`. */
isBefore: (a: DateOrString, b: DateOrString) => boolean;
/**
* `a` evaluates to the same timestamp as `b`. If either `a` or `b` is an
* ISO date-only string, they are the same date (time component is ignored).
*/
isSame: (a: DateOrString, b: DateOrString) => boolean;
/** `d` is, or evaluates to, a valid `Date` object */
isValid: (d: DateOrString) => boolean;
/** Convert a string to a `Date` object (returns a `Date` unchanged) */
toDate: (d: DateOrString) => Date;
/** 'YYYY-MM-DDTHH:mm:ss.SSSZ' format */
toISOString: (d: DateOrString) => string;
/** Format `Date` or ISO 8601 string in ISO date-only format ('YYYY-MM-DD') */
toISOStringDateOnly: (d: DateOrString) => string;
/** Truncate `d` to the start of the given period (used for relative values) */
startOf: (d: DateOrString, unit: 'day' | 'week' | 'month' | 'year') => Date;
/** Advance `d` to the end of the given period (used for relative values) */
endOf: (d: DateOrString, unit: 'day' | 'week' | 'month' | 'year') => Date;
/** Offset `d` by `amount` units of `unit` (negative `amount` moves into the past) */
add: (d: DateOrString, amount: number, unit: RelativeDateTimeUnit) => Date;
}
Most exports from the date/time library have a corresponding get* method that accepts a processor plugin and returns a method or component ready to use in the typical fashion within React Query Builder. For example, to generate a rule processor for formatQuery that uses a custom date/time plugin myDateTimeLibraryAPI, pass it to the getDatetimeRuleProcessorSQL function like this:
const mySQLRuleProcessor = getDatetimeRuleProcessorSQL(myDateTimeLibraryAPI);
const sql = formatQuery(query, { format: 'sql', ruleProcessor: mySQLRuleProcessor });