CSV Export Guard

Previews what each column turns into when the CSV opens in a spreadsheet, so leading zeros, long IDs and dates aren't silently corrupted.

StopsExcel ate the leading zeros

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/csv-export-guard.json

The files land in your project. They're yours to edit.

Usage

csv-export-guard-usage.tsx
import { CsvExportGuard } from "@/components/ui/csv-export-guard"

<CsvExportGuard rows={ROWS} columns={COLUMNS} filename="orders.csv" onExport={(csv) => setLast(csv.split("\r\n")[2])} />

What it catches

  • Leading zeros and + signs dropped from phone numbers and codes
  • Long numeric IDs turned into scientific notation
  • Values like "3-4" turned into dates
  • Cells starting with = + - @ that run as formulas (CSV injection)

Props

CsvExportGuard props
PropTypeDefault
rows* (required)

Rows to export.

Record<string, unknown>[]—
columns

Columns in order. Defaults to the keys of the first row.

{ key: string; label: string }[]—
filename

File name for the default download.

string"export.csv"
onExport

Receives the finished CSV. Defaults to downloading it as `filename`.

(csv: string, filename: string) => void—
defaultFixed

Columns fixed from the start.

string[][]
formulaPrefix

Character put in front of formula-looking cells. OWASP suggests a tab for Excel; `'` is the common choice.

string"'"
bom

Prepend a UTF-8 byte-order mark so Excel reads accents and ₹ correctly.

booleantrue
messages

Short phrase per issue kind: "{count} values {phrase}".

Partial<Record<CsvIssueKind, string>>—
title

Heading above the column list.

string"Before you export"
fixLabel

Text for a column's fix toggle.

string"Keep as typed"
fixedLabel

Text for a column's fix toggle when on.

string"Kept as typed"
fixAllLabel

Text for the fix-all button.

string"Fix all"
exportLabel

Export button text.

string"Export CSV"
exportAnywayLabel

`{count}` is filled in.

string"Export with {count} unfixed"
sizeText

Size line under the title.

(columns: number, rows: string) => string(columns, rows) => `${columns} column${columns === 1 ? ""…
valuesText

"{count} values {phrase}" for each issue.

(count: number, phrase: string) => string(count, phrase) => `${count} ${count === 1 ? "value" : "v…
unfixedText

Screen-reader summary while columns would still be changed.

(count: number) => string(count) => `${count} column${count === 1 ? "" : "s"} woul…
allSafeText

Screen-reader summary once every column is safe.

string"Every column opens as typed."
asTextLabel

Shown after a neutralised formula sample.

string"(as text)"
columnsLabel

Accessible name of the column list.

string"Columns"
noProblemsLabel

Accessible name of the check on a clean column.

string"No problems"
opensAsLabel

Read before the "after" sample, e.g. "007 opens as 7".

string"opens as"

* required. Also accepts the native props of <div> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Columns are a labelled list; fix toggles use aria-pressed
  • The "no problems" icon has an aria-label; arrows are aria-hidden
  • Summary is aria-live="polite"

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

This is a UX guard rail. Enforce on the server: Neutralise formula-leading cells when generating CSV on the server as well, since exports can be produced without this component.

  • Predicts typical Excel/Sheets behaviour; locale settings and other apps may differ
  • Forcing text or prefixing formulas changes the raw CSV, which other importers may not expect
  • Checks the rows you pass, not a whole export streamed from the server

What it stops

Excel ate the leading zeros

Also known as

csv export · excel · leading zeros · csv injection · pin code · +91 · google sheets · download csv

Dependencies

Pairs well with