Dates and time
Every date in ganttlane is a wall-clock date with no time zone. '2026-03-02T09:30' is 09:30
for every viewer, in every zone, in every season. This page lists what a date can be, how the end
of a task is read, how durations are written, what comes back from the chart and how to avoid the
one mistake that shifts dates: passing instants where the chart expects wall-clock time.
Floating wall-clock time
Section titled “Floating wall-clock time”The chart keeps no zone and no daylight saving. A day is always 24 hours, a week 7 days, and arithmetic on dates never depends on where the code runs. Three consequences:
- A plan is the same for everybody. A kick-off at 09:00 is at 09:00 in Kyiv and in New York.
- A string with a zone is rejected.
'2026-03-02T09:30Z'and'2026-03-02T09:30+02:00'give anunsupported-offsetissue and the task is invalid. Convert the value first. - A
Dateor an epoch number is read through the local clock of the runtime: its year, month, day, hour and minute asgetFullYear(),getHours()and so on. They become the wall-clock values of the task. Different viewers can read the same instant as different wall-clock times, so prefer strings for a fixed plan (see the trap below).
What a date can be
Section titled “What a date can be”A date input (DateInput) is a string, a number or a Date. Objects are not accepted.
| Input | Example | Precision | What it becomes |
|---|---|---|---|
| Date-only string | '2026-03-02' |
day | Midnight of that day |
Date and time, T |
'2026-03-02T09:30' |
time | 09:30 that day |
| Date and time, a space | '2026-03-02 09:30' |
time | The same |
| With seconds | '2026-03-02T09:30:15' |
time | Read, then reported as misaligned and rounded: see below |
Date at local midnight |
new Date(2026, 2, 2) |
day | The local day, midnight |
Date with a time |
new Date(2026, 2, 2, 9, 30) |
time | The local wall-clock time, 09:30 |
| Epoch milliseconds | Date.now() |
as Date |
new Date(ms) read through the local clock |
| A zone designator or an offset | '2026-03-02T09:30Z' |
none | unsupported-offset: the task is invalid |
| Another format | '2026-3-2', '03/02/2026' |
none | invalid-date |
| Not a real date | '2026-02-30', an invalid Date, NaN |
none | invalid-date |
Strings need a four-digit year, a two-digit month and day, and, for a time, hours and minutes.
Seconds and milliseconds are accepted by the parser, but the smallest step of the chart is one
minute, so a value with seconds is reported as misaligned and aligned
(Granularity and alignment). A bad date never throws: the task
becomes a row without a bar and carries the issue.
Day precision and time precision
Section titled “Day precision and time precision”Every task has a precision:
'day': the task sits on whole days. All its values are midnights.'time': the task has exact moments, aligned to the granularity.
The chart derives it from the input. A task is 'time' when any start or end it uses carries a
time of day, or when its duration has a part shorter than a day ('6h', 0.5); otherwise it is
'day'. task.precision overrides that:
| Input | Derived precision | Note |
|---|---|---|
start: '2026-03-02', end: '2026-03-06' |
day | |
start: '2026-03-02T00:00', end: '2026-03-06' |
time | T00:00 is a time, even at midnight |
start: new Date(2026, 2, 2) |
day | A Date at local midnight counts as date-only |
start: '2026-03-02', duration: '1d 4h' |
time | A part shorter than a day |
start: new Date(2026, 2, 2), precision: 'time' |
time | Forced: the Date is an exact moment |
start: '2026-03-02T10:00', precision: 'day' |
day | Forced: 10:00 is rounded to the day, with an issue |
What the precision changes:
- Alignment. A day task is always aligned to whole days, even when the granularity is
'1h'. A time task is aligned to the granularity. - The end of a task. The inclusive date-only
end(next section) is a rule for day tasks. Withprecision: 'time'set on the task, every value is an exact moment, so a date-onlyendis that midnight and no longer covers its day. - Dragging. With
scheduling.keepDayPrecision(on by default) a day task stays on whole days when dragged, whatever the snap step. Withfalsea drag can turn it into a time task. - Working time. A working-day count in
'working'mode uses the day rule for day tasks and exact working time for time tasks: The work calendar.
record.precision shows the result. A task that has both date-only and timed values, such as
start: '2026-03-02T10:00', end: '2026-03-04', is a time task whose date-only end still covers
its whole day: it stops at the start of March 5.
The end of a task
Section titled “The end of a task”A bar covers the half-open interval from start to endExclusive. How you write the end differs:
- A date-only
endis the last day, inclusive.start: '2026-03-02', end: '2026-03-06'covers five days, Monday to Friday, and the bar stops at the start of March 7. This is how people write ranges. - An
endwith a time is the exact moment the task ends, exclusive.end: '2026-03-02T11:00'stops at 11:00.
dates.endInclusive (default true) switches the first rule: with false a date-only end is
the first day after the task, like an exclusive end.
| Input | endInclusive: true (default) |
endInclusive: false |
|---|---|---|
start: '2026-03-02', end: '2026-03-06' |
Mar 2 to Mar 6: 5 days, stops Mar 7 00:00 | Mar 2 to Mar 5: 4 days, stops Mar 6 00:00 |
start: '2026-03-02', duration: 3 |
Mar 2 to Mar 4, stops Mar 5 00:00 | The same |
start: '2026-03-02T09:00', end: '2026-03-02T11:00' |
Two hours, stops 11:00 | The same |
start: '2026-03-02T09:00', end: '2026-03-04' |
Stops Mar 5 00:00: the date covers its day | Stops Mar 4 00:00 |
start: '2026-03-02', end: '2026-03-02' |
One day | Zero length |
The rows with a time of day assume a sub-day scheduling.granularity; at the default '1d' the
times are rounded to whole days. The same semantics apply when the chart gives dates back:
record.endandTaskDates.end(the second argument ofonTaskDatesChange) are the last day, at midnight, for a day task of non-zero length, and the exact end otherwise.record.endExclusiveis always the half-open end, whatever the option says. Draw and compute with it; showendto people.updateTask(id, { end })reads a date-onlyendas an input does.patchTaskswrites dates back in the form the task already has (aDatestays aDate, a string stays a string of the same precision), so a round trip keeps yourendform.
An end before the start is an end-before-start error. Equal values are fine: with the default
option a date-only pair on one day is a one-day task.
Durations
Section titled “Durations”A duration (and a link lag) is how long something lasts. Four forms are accepted:
| Form | Examples | Rules |
|---|---|---|
| A number | 3, 0.5 |
Days. Only in a task duration and a link lag. A lag can be negative |
| A string | '3d', '6h', '1d 4h', '30min' |
Whole numbers followed by a unit, separated by single spaces, each unit at most once |
| An ISO 8601 duration | 'P1DT4H', 'PT90M', 'P2W' |
Years, months, weeks, days, hours, minutes. No seconds, no fractions, no sign |
| An object | { days: 1, hours: 4 } |
Fields years, quarters, months, weeks, days, hours, minutes: whole numbers, at least one |
The string units:
| Unit | Means | Unit | Means |
|---|---|---|---|
y |
calendar years (12 months) | d |
days (24 hours) |
q |
calendar quarters (3 months) | h |
hours |
mo |
calendar months | min |
minutes |
w |
weeks of 7 days |
mois months andminis minutes. A bare'm'is rejected as ambiguous, as are seconds ('30s'), fractions ('1.5d') and signs ('-2d').- Months, quarters and years are calendar parts. They are added first, with the day clamped to
the end of the month:
start: '2026-01-31', duration: '1mo'stops at the start of February 28 (Durationkeeps them apart from the fixed part for this reason). They are not allowed in a lag. - A number is days only where it says so. A task
durationand a linklagtake a number. Other durations, such asscheduling.granularity, take a string or an object, sogranularity: 1is aninvalid-durationissue. - A malformed duration is an
invalid-durationissue and the task is invalid, or the link is dropped.
A task has either an end or a duration, never both in effect: with both, end wins. The length
of a duration depends on durationMode: calendar time by default, or only working time with
'working'. A task cannot be longer than ten years (too-long).
Minute resolution
Section titled “Minute resolution”The smallest unit is the minute. scheduling.granularity goes down to '1min', and a duration
cannot have seconds. A string with seconds is parsed and then rounded by the same rules as any
misaligned value, with a misaligned warning.
What comes back
Section titled “What comes back”| Where | Form |
|---|---|
TaskRecord (start, end, endExclusive, progressDate) |
Local Dates with the wall-clock values of the task |
onTaskDatesChange, onRangeChange, getVisibleRange, onScroll |
Dates |
ChangeSet, SessionChanges, state.derived.model |
Floating numbers, so that they go back into the core and patchTasks |
toDate(value) from @ganttlane/core turns a floating number into a local Date;
parseDateInput(input) goes the other way and returns the number with the precision, or an issue.
One daylight-saving detail: a wall-clock time that does not exist in the viewer’s zone is moved
forward by the gap. In New York, '2026-03-08T02:30' comes back as a Date for 03:30 EDT. The
chart’s own arithmetic is not affected: a floating day stays 24 hours.
Dates shifted by a time zone
Section titled “Dates shifted by a time zone”How to pass dates without a shift:
| Do | Don’t |
|---|---|
start: '2026-03-02' |
start: new Date('2026-03-02') |
start: new Date(2026, 2, 2) (local, month from 0) |
start: date.toISOString() (ends with Z: rejected) |
start: '2026-03-02T09:30' |
start: '2026-03-02T09:30:00Z' |
If your source gives instants, such as UTC timestamps from a server, decide which wall clock the
plan lives on and convert once, before the chart: format the instant in that zone (for example with
Intl.DateTimeFormat or your date library) and pass the resulting string without an offset. If you
pass new Date(instant) instead, each viewer sees the instant on their own clock, which is what
you want for “when is this meeting for me” and not for a shared plan.
The same applies to the dates you read back: record.start is a local Date with the wall-clock
fields of the task. Format it with its local getters or with a formatter that uses the local zone;
toISOString() converts to UTC and shifts it again.
The DateAdapter and its engine
Section titled “The DateAdapter and its engine”Calendar-aware arithmetic goes through a DateAdapter: adding months, the start of a week, a month
or a year, the day of the week, ISO weeks, and locale formatting. The default engine is
@internationalized/date (Gregorian calendar, no zone), which also validates ISO strings and knows
the length of every month. createFloatingAdapter({ weekStartsOn }) creates one: weeks start on
Monday unless you say otherwise, and formatting runs Intl over the wall-clock fields, ignoring
the zone, so header labels and tooltips show the same numbers everywhere.
The chart builds its own adapter. There is no option to replace it, and the first day of the week
is Monday; the adapter is for the building blocks of @ganttlane/core (createTimeScale,
headerRows, snapTime) that take one. Fixed-length work, such as days and hours, is plain
arithmetic on the numbers.
How time becomes pixels
Section titled “How time becomes pixels”The time scale is linear: x = (t - origin) * pxPerDay / 24 hours. Only pxPerDay changes between
view modes. Because a day is a fixed width, months and years get their
real length on screen: with the month view mode at 4 px per day, February is 112 px wide and
March 124 px. The hour mode at 960 px per day is 40 px per hour. Pixel values are not rounded;
snapping a drag to a step is a separate rule (see
Granularity and alignment). The
view modes example and the
custom view mode example show the scale at work.
“Today” is read from the runtime’s local clock (or from your now option, which returns a
DateInput) and truncated to the minute, so the today line follows the viewer’s local time.
Related
Section titled “Related”- Granularity and alignment: the data step, and what happens to a minute that is off it.
- The work calendar: how working days count, in days and in hours.
- Tasks and records: the fields that take dates.
- Options: Dates and Scheduling.
- Examples: working days at hourly granularity, data formats, scrolling and today.
- API reference:
DateInput,DurationInput,DatePrecision,DateAdapter.