Data Export
Request a copy of your data: pick what to include and the format, see the size, then track the export to download.
$
View as Markdownnpx shadcn@latest add https://sundaeui.dev/r/data-export.json'use client'
import { useEffect, useState } from 'react'
import './data-export.css'
export interface ExportCategory { id: string; label: string; mb: number }
export interface DataExportProps {
categories: ExportCategory[]
format?: 'json' | 'csv'
onRequest?: (ids: string[], format: string) => void
fill?: 'dark' | 'light'
className?: string
}
/** Personal data export request and tracker. */
export function DataExport({ categories, format: f0 = 'json', onRequest, fill = 'dark', className }: DataExportProps) {
const [sel, setSel] = useState(categories.slice(0, 3).map((c) => c.id))
const [fmt, setFmt] = useState(f0)
const [pct, setPct] = useState<number | null>(null)
const mb = categories.filter((c) => sel.includes(c.id)).reduce((a, c) => a + c.mb, 0) * (fmt === 'csv' ? 0.7 : 1)
useEffect(() => { if (pct === null || pct >= 100) return; const id = setTimeout(() => setPct((p) => Math.min(100, (p ?? 0) + 9 + ((p ?? 0) % 7))), 260); return () => clearTimeout(id) }, [pct])
const size = mb >= 1000 ? `${(mb / 1000).toFixed(1)} GB` : `${Math.max(1, Math.round(mb))} MB`
return (
<div className={['sd-dex', className].filter(Boolean).join(' ')} data-fill={fill}>
<div className="sd-dex__h"><b>Export your data</b><small>We will prepare a zip you can download</small></div>
{pct === null ? <>
<ul>{categories.map((c) => <li key={c.id}><label><input type="checkbox" checked={sel.includes(c.id)} onChange={() => setSel(sel.includes(c.id) ? sel.filter((x) => x !== c.id) : [...sel, c.id])} /><span>{c.label}</span><small>{c.mb >= 1000 ? `${(c.mb / 1000).toFixed(1)} GB` : `${c.mb} MB`}</small></label></li>)}</ul>
<div className="sd-dex__fm" role="radiogroup" aria-label="Format"><span>Format</span>{(['json', 'csv'] as const).map((f) => <button key={f} type="button" role="radio" aria-checked={fmt === f} onClick={() => setFmt(f)}>{f.toUpperCase()}</button>)}</div>
<button type="button" className="sd-dex__go" disabled={!sel.length} onClick={() => { setPct(0); onRequest?.(sel, fmt) }}>Request export · about {size}</button>
</> : (
<div className="sd-dex__st" aria-live="polite">
<b>{pct < 100 ? 'Preparing your export…' : 'Your export is ready'}</b>
<div className="sd-dex__pb" role="progressbar" aria-valuemin={0} aria-valuemax={100} aria-valuenow={pct} aria-label="Export progress"><i style={{ width: `${pct}%` }} /></div>
<small>{pct < 100 ? `${sel.length} categories · ${fmt.toUpperCase()} · ${pct}%` : `${size} zip · link expires in 7 days`}</small>
{pct >= 100 && <a href="#export" onClick={(e) => e.preventDefault()}>Download</a>}
<button type="button" style={{ fontSize: 12, color: 'var(--mute)' }} onClick={() => setPct(null)}>{pct < 100 ? 'Cancel' : 'Start a new export'}</button>
</div>
)}
</div>
)
}.sd-dex { --bg: #1c1c1c; --fg: #f6f6f6; --mute: #8e8e8e; --line: rgb(255 255 255 / 0.1); --soft: rgb(255 255 255 / 0.07); --strong: #fafafa; --on: #111;
color: var(--fg); font-size: 14px; line-height: 1.4; -webkit-tap-highlight-color: transparent; }
.sd-dex[data-fill='light'] { --bg: #fff; --fg: #121212; --mute: #6f6f6f; --line: rgb(0 0 0 / 0.09); --soft: rgb(0 0 0 / 0.05); --strong: #111; --on: #fff; }
.sd-dex :where(button) { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.sd-dex :focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.sd-dex .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.sd-dex { width: min(100%, 330px); padding: 14px; grid-template-columns: minmax(0, 1fr); border-radius: 18px; background: var(--bg); box-shadow: inset 0 0 0 1px var(--line); display: grid; gap: 10px; }
.sd-dex__h b { display: block; font-size: 15px; }
.sd-dex__h small { color: var(--mute); font-size: 12px; }
.sd-dex ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
.sd-dex li label { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 10px; font-size: 13px; cursor: pointer; box-shadow: inset 0 0 0 1px var(--line); }
.sd-dex li label:has(input:checked) { box-shadow: inset 0 0 0 1.5px var(--fg); }
.sd-dex li input { appearance: none; width: 17px; height: 17px; margin: 0; border-radius: 5px; box-shadow: inset 0 0 0 1.5px var(--mute); display: grid; place-items: center; flex: none; }
.sd-dex li input:checked { background: var(--fg); box-shadow: none; }
.sd-dex li input:checked::after { content: ''; width: 7px; height: 4px; border: solid var(--bg); border-width: 0 0 2px 2px; transform: translateY(-1px) rotate(-45deg); }
.sd-dex li label span { flex: 1; }
.sd-dex li label small { color: var(--mute); font-size: 11.5px; font-variant-numeric: tabular-nums; }
.sd-dex__fm { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--mute); }
.sd-dex__fm span { flex: 1; }
.sd-dex__fm button { padding: 5px 12px; border-radius: 99px; font-size: 12px; font-weight: 600; box-shadow: inset 0 0 0 1px var(--line); color: var(--fg) !important; }
.sd-dex__fm button[aria-checked='true'] { background: var(--strong) !important; color: var(--on) !important; box-shadow: none; }
.sd-dex__go { height: 40px; border-radius: 11px; font-size: 13px; font-weight: 600;background: var(--strong) !important; color: var(--on) !important; }
.sd-dex__go:disabled { opacity: 0.4; }
.sd-dex__st { display: grid; gap: 8px; padding: 12px; border-radius: 12px; background: var(--soft); }
.sd-dex__st b { font-size: 14px; }
.sd-dex__st small { color: var(--mute); font-size: 12px; }
.sd-dex__pb { height: 6px; border-radius: 3px; background: var(--line); overflow: hidden; }
.sd-dex__pb i { display: block; height: 100%; background: var(--fg); transition: width 300ms; }
.sd-dex__st a { display: grid; place-items: center; height: 40px; border-radius: 11px; font-size: 13px; font-weight: 600;background: var(--strong) !important; color: var(--on) !important; text-decoration: none; }
@media (prefers-reduced-motion: reduce) { .sd-dex *, .sd-dex *::before, .sd-dex *::after { animation: none !important; transition: none !important; } }Overview
Checkboxes choose categories, each with its rough size; the estimate updates as you tick. Format is JSON or CSV.
Requesting runs through preparing and ready states with progress; the download link expires after a set time, which is stated.
When to use
- Account settings, compliance flows.
When not to use
- Real-time sync: use an integration.
Installation
$
pnpm dlx shadcn@latest add https://sundaeui.dev/r/data-export.json$
npx shadcn@latest add https://sundaeui.dev/r/data-export.json$
yarn dlx shadcn@latest add https://sundaeui.dev/r/data-export.json$
bunx --bun shadcn@latest add https://sundaeui.dev/r/data-export.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view.
- No dependencies beyond React 18 or later.
- Copy data-export.tsx and data-export.css into components/sundae/.
- The CSS file is imported by the component, so no extra configuration is needed.
- Import the component and use it as shown below.
Usage
import { DataExport } from '@/components/sundae/data-export'
<DataExport />Props
| Prop | Type | Default |
|---|---|---|
categories*What can be exported. | { id: string; label: string; mb: number }[] | — |
onRequestCalled on request. | (ids: string[], format: string) => void | — |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- Categories are checkboxes; format is a radiogroup.
- Progress is a progressbar with a text status.
- Reduced motion removes the movement and keeps every state change.
Customization
- Colours are CSS custom properties on .sd-dex; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
FAQ
- Is Data Export free?
- Yes. Data Export is free for personal and commercial projects, with full source on this page.
- How do I install Data Export?
- Run npx shadcn@latest add https://sundaeui.dev/r/data-export.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Data Export support keyboard and screen readers?
- Categories are checkboxes; format is a radiogroup. Progress is a progressbar with a text status.
- Does Data Export respect reduced motion?
- Reduced motion removes the movement and keeps every state change.
More like this
All forms →Profile Visibility
Forms · RefinedNotification Matrix
Forms · Refined- Pro
Shortcut Recorder
Forms · Refined Settings List
Forms · RefinedAsync Switch
Forms · Refined- Pro
Feature Flag
Forms · Refined