Skip to content

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-picker

For 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.

Reference

Propstrigger, columns, presets, formats, timeZones, summary, onExport, value…

Props

ExportDialog props
PropType
openboolean

Whether the dialog is open (controlled).

–
defaultOpenboolean–
onOpenChange(open: boolean) => void–
triggerReactElement

The element that opens it, rendered as the trigger: trigger={<Button>Export</Button>}.

–
titleReactNode

default "Export data"

"Export data"
descriptionReactNode–
presets('today' | 'yesterday' | 'last7Days' | 'last30Days' | 'thisMonth' | 'lastMonth' | 'yearToDate')[]

The date ranges offered, in order.

default DEFAULT_PRESETS

DEFAULT_PRESETS
customboolean

Offer the Custom range.

default true

true
formatsExportOption[]

The file formats.

default DEFAULT_FORMATS

DEFAULT_FORMATS
timeZonesreadonly [ExportOption, ExportOption]

The two time zones the switch offers. Default your local one and UTC.

–
columnsRequiredExportColumn[]

Every column the file can hold.

–
valueExportDialogValue

The choice (controlled).

–
defaultValuePartial<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) => string

The count line: (n) => ${n} payments``. Default "412 rows", in the provider's language.

–
onExportRequired(request: ExportRequest) => ExportResult | void | Promise<ExportResult | void>

Runs the export. Resolve for done (or { emailed: true } when it will arrive by email), reject or throw for failed.

–
nowDate

Today, for the presets. Default the moment it mounted in the browser (never read while rendering on the server).

–
localestring

Default the provider's locale.

–
classNamestring–

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

Motion tokens
MomentTokens
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.standard

220ms · 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.standard

140ms · 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.standard

220ms · 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 status region, so a changed count, total or period is announced politely, and aria-busy while 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.