Mocking Date
Storybook addon that mocks the JavaScript Date — and opt-in Temporal and Intl.DateTimeFormat — for individual stories.
Storybook Addon Mocking Date
A Storybook addon that mocks the JavaScript Date — and, opt-in, Temporal and Intl.DateTimeFormat — for individual stories using @sinonjs/fake-timers. Useful for components that read the current date or time and need deterministic snapshots, visual regression tests, or coverage of time-sensitive UI such as relative timestamps and seasonal greetings.
Requirements
- Storybook
^10.0.0 - Renderer-agnostic — the addon ships a preview decorator that works with any Storybook framework (React, Vue, Svelte, etc.)
Installation
First, install the package.
npm install --save-dev storybook-addon-mock-date
Then, register it as an addon in .storybook/main.ts.
// .storybook/main.ts
// Replace your-framework with the framework you are using (e.g., react-vite, vue3-vite)
import type { StorybookConfig } from '@storybook/your-framework';
const config: StorybookConfig = {
// ...rest of config
addons: [
'storybook-addon-mock-date', // 👈 register the addon here
],
};
export default config;
With CSF factories (CSF Next)
If your project uses CSF factories, register the addon in .storybook/preview.ts as well (keep the main.ts entry — it provides the toolbar):
// .storybook/preview.ts
import { definePreview } from '@storybook/your-framework';
import mockDate from 'storybook-addon-mock-date';
export default definePreview({
addons: [mockDate()],
});
This also types the mockingDate parameter and global in preview.meta() and meta.story(). The advanceMockedTime / runAllMockedTimers / getMockedClock helpers can then be imported from the package root — it shares the clock with the /preview entry.
Usage
Pass a Date, a millisecond timestamp, an ISO 8601 string, or a Temporal.Instant / Temporal.ZonedDateTime via the mockingDate parameter at the story, meta, or preview level. Storybook merges parameters with the most specific value winning, so the precedence is story > meta > preview.
// Button.stories.ts
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
parameters: {
mockingDate: new Date(2024, 3, 1),
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const AddParametersAtStory: Story = {
parameters: {
mockingDate: new Date(2023, 6, 1),
},
};
export const AddParametersAtMeta: Story = {};
// .storybook/preview.ts
// Replace your-renderer with the name of your renderer
import type { Preview } from '@storybook/your-renderer';
const preview: Preview = {
parameters: {
mockingDate: new Date(2024, 0, 1),
},
};
export default preview;
A story whose merged mockingDate is undefined reverts the system clock to the moment the preview iframe loaded, so subsequent stories continue to see a deterministic value rather than continuing to drift forward.
Faking other timers
By default only Date is mocked. To freeze other time sources too — Temporal, Intl, or scheduling APIs like setTimeout / setInterval / requestAnimationFrame / performance — pass the object form of mockingDate with a fake array — its values map directly to @sinonjs/fake-timers' toFake:
export const Toast: Story = {
parameters: {
mockingDate: {
now: '2024-01-01T00:00:00',
// intercept the auto-dismiss timer so the toast never races the screenshot
fake: ['Date', 'setTimeout', 'clearTimeout'],
},
},
};
An explicit fake array replaces the default entirely — fake: ['setTimeout'] fakes only setTimeout and leaves Date real. When fake is omitted (including the scalar form) it defaults to ['Date'], so existing stories keep working as-is.
Components that read the clock through Temporal.Now or a zero-argument Intl.DateTimeFormat#format need those APIs faked too — faking Date alone leaves them on the real clock:
export const ChristmasBanner: Story = {
parameters: {
mockingDate: {
now: '2024-12-25T12:00:00Z',
// freeze Temporal.Now and zero-arg Intl.DateTimeFormat#format as well
fake: ['Date', 'Temporal', 'Intl'],
},
},
};
rAF needs
performance. Animation libraries (framer-motion, react-spring, GSAP, Lottie, three.js) compute their delta fromperformance.now(), so fakerequestAnimationFrameandperformancetogether — faking rAF alone leaves the solver with a zero/NaN delta.
Advancing time in play
Faking a timer freezes it. To reach a settled "after" state (a dismissed toast, a finished count-up, an elapsed countdown), advance the clock from a story's play function with advanceMockedTime — after the component has mounted and registered its timers:
import { advanceMockedTime } from 'storybook-addon-mock-date/preview';
export const AfterDismiss: Story = {
parameters: { mockingDate: { fake: ['setTimeout'] } },
play: async ({ canvas }) => {
advanceMockedTime(4000); // run the auto-dismiss timeout
// assert the dismissed state…
},
};
runAllMockedTimers() (drain every scheduled timer) and getMockedClock() (the raw @sinonjs/fake-timers clock) are also exported from storybook-addon-mock-date/preview for finer control.
Always import these helpers from
storybook-addon-mock-date/preview— the same entry the decorator ships from. They share a single module-level clock, so importing from any other path gives you a disconnected instance and a "called without an installed clock" error.
Advancing has to happen in
play, not in a decorator: a component registers its timers in an effect that runs after mount, so a decorator-level tick would fire before any timer exists.
Toolbar override
The addon registers a clock icon in the Storybook toolbar. Clicking it opens a popover with a datetime-local input and a Reset to real time button.

Picking a date stores it in globals.mockingDate and applies the mock immediately to every story you visit, regardless of what parameters.mockingDate is set to. Press Reset to real time (or clear the input) to drop the override and fall back to the parameter-based mocking.
The full precedence with the toolbar in play is toolbar (globals) > story > meta > preview. So a story with its own parameters.mockingDate only shows that date until the toolbar override is engaged.
This is intended for ad-hoc exploration — checking how a "happy birthday" banner looks on the actual day, walking through the same story across a year, etc. — without editing source files. Permanent mocking should still go through parameters.mockingDate so the value lives in version control.
Disabling the toolbar (decorator-only mode)
If you want the date mocking but don't want the clock icon in the toolbar, register the preview entry directly in .storybook/preview.ts instead of listing the addon in .storybook/main.ts:
// .storybook/preview.ts
import type { Preview } from '@storybook/your-renderer';
import mockDate from 'storybook-addon-mock-date/preview';
const preview: Preview = {
...mockDate,
parameters: {
mockingDate: new Date(2024, 0, 1),
},
};
export default preview;
// .storybook/main.ts
const config: StorybookConfig = {
// 'storybook-addon-mock-date' is intentionally not listed here
addons: [/* ... */],
};
The decorator runs the same way; only the toolbar manager bundle is skipped.
What gets mocked
By default only the Date constructor and its static methods (Date.now, Date.parse, etc.) are replaced; everything else keeps using the host clock. Opt into more per story with the fake option (see Faking other timers):
'Temporal'— replaces theTemporal.Nownamespace (instant(),zonedDateTimeISO(),plainDateISO(), …), in browsers that ship nativeTemporal(Chrome/Edge 144+, Firefox 139+, Node.js 26+). WhereTemporaldoesn't exist yet the entry is skipped harmlessly. Imported polyfills (temporal-polyfill,@js-temporal/polyfill) bypass the global and are not replaced, but they derive the current time fromDate.now()internally, so theDatefake pins them at millisecond precision.'Intl'— routes zero-argumentIntl.DateTimeFormatformat()/formatToParts()calls, which read the system clock directly rather than going throughDate.now, through the mocked clock. Calls with an explicit date argument behave as usual (except a falsy0, which upstream@sinonjs/fake-timerstreats as absent and formats as the mocked instant).- Scheduling APIs (
'setTimeout','setInterval','requestAnimationFrame','performance', and the rest) — freezes timers so timer-driven UI can be advanced deterministically fromplay.
What stays real
- The timezone. The addon freezes the instant, not the environment:
Date.prototype.getTimezoneOffset(),Intl.DateTimeFormat().resolvedOptions().timeZone, andTemporal.Now.timeZoneId()all keep reporting the host's timezone. To render a story as if in another timezone, launch the browser with one — e.g. Playwright'stimezoneIdoption or theTZenvironment variable. - Values captured before the decorator runs. A module-scope
const now = new Date()is evaluated at import time, before any story's mock is installed. - Other realms. Web Workers, Service Workers, and other iframes have their own globals; the mock is installed only in the preview iframe.
- Everything outside the JS clock. CSS animations/transitions, the Web Animations API,
IntersectionObserver/ResizeObserver,AbortSignal.timeout(), and network requests are unaffected — disable or mock those separately for stable visual snapshots.
Multiple stories in a docs page
Stories rendered together on the same docs page (e.g. autodocs pages, MDX pages with several <Canvas> blocks) share one set of mocked globals (Date, Temporal, Intl), because the underlying @sinonjs/fake-timers installs the mock globally. Each story's mockingDate is applied correctly during its initial render — so static snapshots show the expected date for every story — but any code that reads the clock after that render sees whichever story's mock was installed last. Live-updating UI such as countdowns, "time ago" labels that refresh, or running clocks will therefore all converge on a single value across the page.
If you need each story on a docs page to keep its own mockingDate for ongoing new Date() reads, render the stories in separate iframes by setting parameters.docs.story.inline to false:
// .storybook/preview.ts
const preview: Preview = {
parameters: {
docs: {
story: { inline: false },
},
},
};
Each iframe gets its own globals, so the mocks no longer leak between stories. The tradeoff is the extra iframe startup cost per story on the page.