DateRangeText
SM · 950B gzip budgetA date range formatted as one range, collapsing shared parts in the locale's own calendar.
- Category
- Internationalisation
- Budget
- sm, single-purpose control
- External runtime
- React / React DOM only
axe fixtureSSR rendersource scanbundle budget
Preview
Live fixtureJan 1 – 5, 2026
Try the component here. Locale-aware formatting and direction follow the selected language; example content and labels are not automatically translated. Overlay demos use interactive launchers.
Why this exists
DateRangeText
日本語Joining two formatted dates with a dash repeats everything they share, and puts the separator where a right-to-left locale does not want it.
Formatting shortcut
2026/01/01 - 2026/01/05
`${fmt(a)} - ${fmt(b)}`Install
npx shadcn@latest add https://gear5-ui.vercel.app/r/date-range-text.jsonCopies the source into your project. Pulls in 2 shared registry items: locale, use-locale.
New here? Set up Tailwind and import aliases first →Source & props
Edit on GitHub ↗Prop types and inline documentation are included below. This is repository source; the installer rewrites shared imports for your project.
"use client";
import { useMemo } from "react";
import { getCalendar } from "../lib/locale";
import { useLocale } from "../lib/use-locale";
export interface DateRangeTextProps {
start: Date | number;
end: Date | number;
options?: Intl.DateTimeFormatOptions;
}
/*
* Rendered inside a `suppressHydrationWarning` span.
*
* `Intl` output is not byte-identical across ICU versions, and Node's ICU is
* not the browser's. This exact call produces "Jan 1 <U+2009>–<U+2009> 5, 2026"
* on Node and "Jan 1 <U+0020>–<U+0020> 5, 2026" in Chrome — visually
* identical, different bytes — so every server-rendered use would throw a
* hydration error in a consumer's app through no fault of theirs.
*
* This is the case React documents the escape hatch for. The suppression is
* scoped to this one text node, so a genuine structural mismatch anywhere else
* still reports normally.
*/
/**
* A date range formatted as one range, not two dates with a dash between them.
*
* `Intl.DateTimeFormat#formatRange` collapses the parts the two dates share:
* English gives "1–5 January 2026" rather than "1 January 2026 – 5 January
* 2026", and every locale collapses differently, using its own range separator
* — which is not always a hyphen, and in RTL locales does not sit where a
* hardcoded `${a} - ${b}` would put it.
*
* The range also goes through the locale's own calendar, so a Thai reader sees
* Buddhist-era years and a Saudi reader sees Hijri ones.
*/
export function DateRangeText({ start, end, options }: DateRangeTextProps) {
const { locale } = useLocale();
const text = useMemo(() => {
const formatter = new Intl.DateTimeFormat(locale, {
dateStyle: "medium",
calendar: getCalendar(locale),
...options,
});
return formatter.formatRange(new Date(start), new Date(end));
}, [locale, start, end, options]);
return <span suppressHydrationWarning>{text}</span>;
}
What CI checks
Quality contract- Bundled, minified and gzipped against its tier budget
- Audited by axe in the state previewed above
- Rendered through react-dom/server with no browser globals
- Scanned for network calls, dangerous sinks, and unguarded animation