Skip to content

DateRangeText

SM · 950B gzip budget

A 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 fixture
Preview locale
Jan 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)}`

DateRangeText

2026/01/01~2026/01/05

date-range-text

Install

npx shadcn@latest add https://gear5-ui.vercel.app/r/date-range-text.json

Copies 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