Export Dialog
Choose a date range, a format and the columns, see what the export holds, then run it and watch it finish or arrive by email.
import { ExportDialog, ExportDialogValue, ExportOption, ExportRequest, ExportResult, ExportSummary } from '@youj/react/date'
Installation & usage
npm i @youj/react date-fns react-day-pickerFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use an Export Dialog when a person leaves a list or report with a file: it asks the three questions an export needs (which days, which format, which columns) in one place and shows the result of the choice before anything runs. For a dialog of your own content use a Dialog; for the range field alone, a Date Range Picker.
'use client'
import { Button } from '@youj/react'
import { ExportDialog } from '@youj/react/date'
const COLUMNS = [
{ id: 'id', label: 'Payment ID' },
{ id: 'amount', label: 'Amount' },
{ id: 'status', label: 'Status' },
]
export function ExportPayments() {
return (
<ExportDialog
title="Export payments"
trigger={<Button variant="outline">Export</Button>}
columns={COLUMNS}
summary={async ({ from, to }) => {
const { count, total } = await countPayments(from, to)
return { count, total: formatEuros(total) }
}}
countLabel={(count) => `${count} payments`}
onExport={async (request) => {
const job = await startExport(request)
return job.large ? { emailed: true, email: job.email } : undefined
}}
/>
)
}onExport receives what was chosen: { period, from, to, format, timeZone, columns }, with from and to as the first and last day (start of day, local time, both included). Resolve for done, resolve { emailed: true } for an export that will arrive by email, and reject or throw for failed; the error's message is shown. timeZone is the value of the option chosen (local or utc by default): your export applies it.
summary gets the same request without waiting for the person to press anything, once when the dialog opens and again when the range or the time zone changes. A late answer to a range that was since left is dropped. Without summary the panel shows the days alone.
It lives in the date entry, @youj/react/date, with the Date Range Picker it is built on.
Date ranges
presets is a list of ids (today, yesterday, last7Days, last30Days, thisMonth, lastMonth, yearToDate), shown in that order with the provider’s names for them. custom={false} leaves out Custom. The range behind each id is a pure function you can use yourself:
import { presetRange } from '@youj/react/date'
presetRange('last7Days', new Date(2026, 8, 29), 'en-US')
// { from: Sep 23, to: Sep 29, text: 'Sep 23 – 29' }text is how the locale writes the range, with the year only when the range is not in the current one. The clock is the second argument, so a range never depends on when it runs: pass now to the dialog to fix it in tests and stories.
Format, time zone and columns
formats is a list of { value, label } (CSV, XLSX and JSON by default), shown as a select, and the first is chosen. timeZones is a pair, shown as a two-option switch: your local zone and UTC by default. columns is every column the file can hold, { id, label }; the button next to Columns says "All 12", "8 of 12" or "None", and its popover has a checkbox per column with All and None above them. With no column chosen, Export is off.
Description
A payments export: a summary that follows the range, the ranges with their days, and the file's options.
The summary at the top says what the choice holds ("412 payments", "€18,240") and which days. It is a polite status region: when you pick another range it keeps the old figures, dims them while your summary function answers, and announces the new ones. Each range in the list shows the days it stands for, computed from today, so "Last 7 days" reads "Sep 23 – 29" and can be checked at a glance.
Export is an Async Button: it works, then says Done, or Failed with the reason under it. While it works the choices are locked, and changing any of them afterwards clears the outcome.
A year of payments is too much to build while the dialog waits.
When onExport resolves { emailed: true } the button says Queued and the dialog says "We’ll email it to you when it’s ready", naming the address when you pass email. The note is a polite status region, so it is read out. Nothing else changes: the person can close the dialog, and the file arrives where you said it would.
Custom opens the Date Range Picker inline, and the dialog grows to fit it.
Choosing Custom reveals the picker under its row, with a smooth height change and no scroll box: a form that scrolls hides its last field. Until both ends are chosen the summary asks for them and Export stays off. This example keeps the choice in the app (value and onValueChange), so the dialog opens on the range it was left on.
Code
export-dialog-demo.tsx, the Payments export story. The component itself comes from the package.
Reference
Propstrigger, columns, presets, formats, timeZones, summary, onExport, value…
Props
| Prop | Type | |
|---|---|---|
open | booleanWhether the dialog is open (controlled). | – |
defaultOpen | boolean | – |
onOpenChange | (open: boolean) => void | – |
trigger | ReactElementThe element that opens it, rendered as the trigger: | – |
title | ReactNodedefault "Export data" | "Export data" |
description | ReactNode | – |
presets | ('today' | 'yesterday' | 'last7Days' | 'last30Days' | 'thisMonth' | 'lastMonth' | 'yearToDate')[]The date ranges offered, in order. default DEFAULT_PRESETS | DEFAULT_PRESETS |
custom | booleanOffer the Custom range. default true | true |
formats | ExportOption[]The file formats. default DEFAULT_FORMATS | DEFAULT_FORMATS |
timeZones | readonly [ExportOption, ExportOption]The two time zones the switch offers. Default your local one and UTC. | – |
columnsRequired | ExportColumn[]Every column the file can hold. | – |
value | ExportDialogValueThe choice (controlled). | – |
defaultValue | Partial<ExportDialogValue>The choice at first (uncontrolled): the first preset, CSV, the first time zone and every column. | – |
onValueChange | (value: ExportDialogValue) => void | – |
summary | (request: ExportRequest) => ExportSummary | Promise<ExportSummary>What the chosen range holds, asked again when the range or the time zone changes. Without it the summary shows the period alone. | – |
countLabel | (count: number) => stringThe count line: | – |
onExportRequired | (request: ExportRequest) => ExportResult | void | Promise<ExportResult | void>Runs the export. Resolve for done (or | – |
now | DateToday, for the presets. Default the moment it mounted in the browser (never read while rendering on the server). | – |
locale | stringDefault the provider's locale. | – |
className | string | – |
Data attributes
The dialog carries data-slot="export-dialog". The summary is export-dialog-summary and carries data-yj-pending while your summary function is answering. The columns button is export-dialog-columns. The rest are the Dialog’s, the Radio Group’s, the Select’s and the Async Button’s own.
MotionDialog entrance, height change, figures dimming · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Openthe dialog rises and fades in over its scrim, as every Dialog does | no token | ||
| Custom rangethe picker’s row grows open and the dialog grows with it; choosing a preset folds it away | duration.baseease.standard220ms · cubic-bezier(0.2, 0, 0, 1) | 220mscubic-bezier(0.2, 0, 0, 1) | |
| Summary updatingthe old figures dim while the new ones are asked for, then take their place | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) | |
| Outcome notegrows open under the options when an export is queued or fails | duration.baseease.standard220ms · cubic-bezier(0.2, 0, 0, 1) | 220mscubic-bezier(0.2, 0, 0, 1) | |
| Export buttonthe Async Button’s pending, success and error states | no token |
Reduced motion
The dialog and the picker’s row appear at once, the figures change without dimming and the note shows without growing.
AccessibilityFocus, radio group, live regions, keyboard
Accessibility
- Focus starts on the chosen date range. The ranges are a radio group named "Date range": the arrow keys move and choose, Tab goes on to the options. Each radio is named by its range and described by its days ("Last 7 days", "Sep 23 – 29").
- Choosing Custom does not move focus; Tab reaches the start and end fields, which are named "Start date" and "End date", and the calendar button opens the range picker.
- The summary is a
statusregion, so a changed count, total or period is announced politely, andaria-busywhile it is being asked for. - The columns button is named by its row ("Columns") and its text ("All 12"); its popover is a labelled group of checkboxes, with the count repeated as a status line above them.
- The outcome is announced twice over: a queued export politely ("We’ll email it to you when it’s ready"), a failure assertively with its message. The Async Button announces working and done as it always does.
- While an export runs, the choices and Cancel are disabled, and Escape closes the dialog only when it is not running. On a touch screen the dialog is a bottom sheet, like every Dialog, and focus starts on the sheet so the keyboard stays down.