Regional Format
Language and region settings with a live preview of how dates, times, numbers and money will look before you save.
$
View as Markdownnpx shadcn@latest add https://sundaeui.dev/r/format-preview.json'use client'
import { useState } from 'react'
import './format-preview.css'
export interface LocaleOption { id: string; label: string; currency: string }
export interface FormatPreviewProps {
locales: LocaleOption[]
value?: string
sample?: Date
onSave?: (v: { locale: string; h24: boolean; weekStart: number }) => void
fill?: 'dark' | 'light'
className?: string
}
/** Locale settings with live Intl preview. */
export function FormatPreview({ locales, value = 'en-US', sample = new Date(Date.UTC(2026, 10, 4, 21, 5)), onSave, fill = 'dark', className }: FormatPreviewProps) {
const [loc, setLoc] = useState(value)
const [h24, setH24] = useState(value !== 'en-US')
const [ws, setWs] = useState(value === 'en-US' ? 0 : 1)
const [saved, setSaved] = useState({ loc: value, h24: value !== 'en-US', ws: value === 'en-US' ? 0 : 1 })
const cur = locales.find((l) => l.id === loc) ?? locales[0]
const tz = 'UTC'
const date = new Intl.DateTimeFormat(loc, { dateStyle: 'full', timeZone: tz }).format(sample)
const short = new Intl.DateTimeFormat(loc, { dateStyle: 'short', timeZone: tz }).format(sample)
const time = new Intl.DateTimeFormat(loc, { hour: 'numeric', minute: '2-digit', hour12: !h24, timeZone: tz }).format(sample)
const num = new Intl.NumberFormat(loc).format(1234567.891)
const money = new Intl.NumberFormat(loc, { style: 'currency', currency: cur.currency }).format(1299.5)
const days = Array.from({ length: 7 }, (_, i) => new Intl.DateTimeFormat(loc, { weekday: 'narrow', timeZone: tz }).format(new Date(Date.UTC(2026, 10, 1 + ((i + ws) % 7)))))
const dirty = saved.loc !== loc || saved.h24 !== h24 || saved.ws !== ws
return (
<div className={['sd-fmt', className].filter(Boolean).join(' ')} data-fill={fill}>
<div className="sd-fmt__h"><b>Region and format</b><small>Applies to dates, numbers and money</small></div>
<label>Region<select value={loc} onChange={(e) => setLoc(e.target.value)}>{locales.map((l) => <option key={l.id} value={l.id}>{l.label}</option>)}</select></label>
<div className="sd-fmt__row">
<label>Clock<div className="sd-fmt__seg" role="radiogroup" aria-label="Clock">{[false, true].map((v) => <button key={String(v)} type="button" role="radio" aria-checked={h24 === v} onClick={() => setH24(v)}>{v ? '24-hour' : '12-hour'}</button>)}</div></label>
<label>Week starts<div className="sd-fmt__seg" role="radiogroup" aria-label="First day of week">{[0, 1].map((v) => <button key={v} type="button" role="radio" aria-checked={ws === v} onClick={() => setWs(v)}>{v ? 'Monday' : 'Sunday'}</button>)}</div></label>
</div>
<dl aria-live="polite">
<dt>Date</dt><dd>{date}</dd>
<dt>Short</dt><dd>{short}</dd>
<dt>Time</dt><dd>{time}</dd>
<dt>Number</dt><dd>{num}</dd>
<dt>Money</dt><dd>{money}</dd>
</dl>
<div className="sd-fmt__wk" aria-label="Week preview">{days.map((d, i) => <span key={i}>{d}</span>)}</div>
<button type="button" className="sd-fmt__go" disabled={!dirty} onClick={() => { setSaved({ loc, h24, ws }); onSave?.({ locale: loc, h24, weekStart: ws }) }}>{dirty ? 'Save format' : 'Saved'}</button>
</div>
)
}.sd-fmt { --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-fmt[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-fmt :where(button) { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.sd-fmt :focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.sd-fmt .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.sd-fmt { 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-fmt__h b { display: block; font-size: 15px; }
.sd-fmt__h small { color: var(--mute); font-size: 12px; }
.sd-fmt label { display: grid; gap: 4px; font-size: 11.5px; color: var(--mute); }
.sd-fmt select { height: 38px; padding: 0 10px; border: 0; border-radius: 10px; background: var(--soft); color: var(--fg); font: inherit; font-size: 13px; outline: 0; }
.sd-fmt select:focus { box-shadow: inset 0 0 0 1.5px var(--fg); }
.sd-fmt__row { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.sd-fmt__seg { display: flex; padding: 2px; border-radius: 10px; background: var(--soft); }
.sd-fmt__seg button { flex: 1; height: 32px; border-radius: 8px; font-size: 12px; font-weight: 600; color: var(--mute) !important; }
.sd-fmt__seg button[aria-checked='true'] { background: var(--bg) !important; color: var(--fg) !important; box-shadow: inset 0 0 0 1px var(--line); }
.sd-fmt dl { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 6px 12px; padding: 12px; border-radius: 12px; box-shadow: inset 0 0 0 1px var(--line); font-size: 12.5px; }
.sd-fmt dt { color: var(--mute); }
.sd-fmt dd { margin: 0; text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; }
.sd-fmt__wk { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; text-align: center; font-size: 11px; color: var(--mute); }
.sd-fmt__wk span:first-child { color: var(--fg); font-weight: 700; }
.sd-fmt__go { height: 40px; border-radius: 11px; font-size: 13px; font-weight: 600;background: var(--strong) !important; color: var(--on) !important; }
.sd-fmt__go:disabled { opacity: 0.4; }
@media (prefers-reduced-motion: reduce) { .sd-fmt *, .sd-fmt *::before, .sd-fmt *::after { animation: none !important; transition: none !important; } }Overview
Pick a region, a 12 or 24 hour clock and the first day of the week. The preview uses Intl formatting, so it shows exactly what the browser will render.
A mini week row reflects the first-day choice; Save confirms the new locale.
When to use
- Account settings, onboarding for international products.
When not to use
- Translating UI copy: that is a separate language setting.
Installation
$
pnpm dlx shadcn@latest add https://sundaeui.dev/r/format-preview.json$
npx shadcn@latest add https://sundaeui.dev/r/format-preview.json$
yarn dlx shadcn@latest add https://sundaeui.dev/r/format-preview.json$
bunx --bun shadcn@latest add https://sundaeui.dev/r/format-preview.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 format-preview.tsx and format-preview.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 { FormatPreview } from '@/components/sundae/format-preview'
<FormatPreview />Props
| Prop | Type | Default |
|---|---|---|
locales*Options. | { id: string; label: string; currency: string }[] | — |
onSaveCalled on save. | (v) => void | — |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- Controls are native selects and radiogroups with labels.
- The preview is a description list announced on change.
- Reduced motion removes the movement and keeps every state change.
Customization
- Colours are CSS custom properties on .sd-fmt; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
FAQ
- Is Regional Format free?
- Yes. Regional Format is free for personal and commercial projects, with full source on this page.
- How do I install Regional Format?
- Run npx shadcn@latest add https://sundaeui.dev/r/format-preview.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Regional Format support keyboard and screen readers?
- Controls are native selects and radiogroups with labels. The preview is a description list announced on change.
- Does Regional Format respect reduced motion?
- Reduced motion removes the movement and keeps every state change.
More like this
All forms →Language Picker
Forms · RefinedWeek Strip
Forms · RefinedPhone Input
Forms · RefinedNotification Matrix
Forms · Refined- Pro
Shortcut Recorder
Forms · Refined Settings List
Forms · Refined