ROC (Minguo) Date Picker (React)
A React date picker that toggles between the ROC (Minguo) calendar and the Gregorian one — a grid calendar with a "民國 115 年" (Minguo year) dropdown, typed input like "115/08/18" or "2026-08-18" parsed with an IME guard, and ISO output that also carries the ROC and Gregorian forms.
Taiwanese forms (birthdays, ID expiry dates, insurance policies) are written in ROC (Minguo) years, but back ends want Gregorian ISO strings. This component bridges the two: the text input accepts "115/08/18", "2026-08-18", "民國115年8月18日" (Minguo 115, Aug 18) or even "1150818", and parses on blur or Enter (an Enter pressed while a Chinese IME is composing is ignored); the button on the right opens a calendar popover whose "民國|西元" (ROC / Gregorian) segmented control re-labels every year instantly (ROC = Gregorian − 1911, and years before the ROC epoch are shown as Gregorian, unconverted), with a year dropdown, month arrows, a 7-column day grid, a today marker and min/max disabled ranges. value is always an ISO string (YYYY-MM-DD), and onChange also hands you the ROC and Gregorian triples plus the display string for the current calendar.
npx shadcn@latest add https://webberui.com/r/roc-date-picker.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<RocDatePicker />
Installation
npx shadcn@latest add https://webberui.com/r/roc-date-picker.jsonOr, once registries are configured in components.json, install it as @webberui/roc-date-picker.
Usage
import {
RocDatePicker,
formatRocDate,
parseRocDateInput,
} from "@/components/ui/roc-date-picker";
const [iso, setIso] = React.useState("2026-08-18");
<RocDatePicker
value={iso}
onChange={(next, meta) => {
setIso(next); // "2026-08-18"; "" when cleared
if (meta) {
console.log(meta.roc); // { year: 115, month: 8, day: 18 }
console.log(meta.gregorian); // { year: 2026, month: 8, day: 18 }
console.log(meta.display); // "115/08/18" in ROC mode, "2026/08/18" in Gregorian mode
}
}}
label="保單生效日"
min="2026-01-01"
max="2027-12-31"
/>;
// The parser and formatter can be used on their own (importing legacy data, printing ROC years)
parseRocDateInput("115/08/18"); // "2026-08-18"
parseRocDateInput("民國115年8月18日"); // "2026-08-18"
parseRocDateInput("2026-08-18"); // "2026-08-18"
formatRocDate("2026-08-18", "roc"); // "115/08/18"
formatRocDate("2026-08-18", "gregorian", "-"); // "2026-08-18"Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | null | — | Controlled value: an ISO date string (YYYY-MM-DD); an empty string or null means nothing is selected. When omitted the component is uncontrolled |
defaultValue | string | "" | Initial value in uncontrolled mode (ISO string) |
onChange | (iso: string, meta: RocDateChangeMeta | null) => void | — | Callback when the value changes: the first argument is the ISO string (empty when cleared), the second carries the ROC, Gregorian and display forms (null when cleared) |
calendar | "roc" | "gregorian" | — | Controlled calendar: ROC (roc) or Gregorian (gregorian); when omitted the built-in toggle manages it |
defaultCalendar | "roc" | "gregorian" | "roc" | Initial calendar in uncontrolled mode |
onCalendarChange | (calendar: RocCalendar) => void | — | Callback when the user flips the "民國|西元" (ROC / Gregorian) toggle |
weekStartsOn | 0 | 1 | 0 | Whether the week starts on Sunday (0) or Monday (1) |
min | string | — | Earliest selectable date (ISO string, inclusive); earlier days are disabled and typed input outside the range is rejected |
max | string | — | Latest selectable date (ISO string, inclusive); later days are disabled and typed input outside the range is rejected |
label | string | "日期" (Date) | Field label text |
placeholder | string | — | Input placeholder; when omitted it follows the calendar — "例:115/08/18" (e.g. 115/08/18) or "例:2026/08/18" |
disabled | boolean | false | Disable the field |
name | string | — | Form field name: the ISO string is submitted through a hidden input (the visible text input carries no name) |
className | string | — | Forwarded to the outermost container |
RocDateChangeMeta
| Field | Type | Default | Description |
|---|---|---|---|
roc | RocDate | — | ROC year/month/day (year is 0 or negative for dates in 1911 or earlier) |
gregorian | RocDate | — | Gregorian year/month/day |
display | string | — | Display string formatted for the current calendar ("115/08/18" or "2026/08/18") |
RocDate
| Field | Type | Default | Description |
|---|---|---|---|
year | number | — | Year (ROC or Gregorian depending on context) |
month | number | — | Month (1–12) |
day | number | — | Day (1–31) |
Named helper exports
parseRocDateInput(raw: string): string | null— parses user-typed text into an ISO string, returningnullon failure (rules under "How it works")formatRocDate(iso: string, calendar?: RocCalendar, separator?: string): string— formats an ISO date for the given calendar; defaults to ROC mode with a "/" separator, and returns""for an invalid ISO stringtoRocDate(iso: string): RocDate | null— ISO string to ROC year/month/dayfromRocDate(roc: RocDate): string | null— ROC year/month/day to ISO string; returnsnullfor dates that do not exist (such as Feb 30)
The RocCalendar type ("roc" | "gregorian") is exported as well, so the same conversion logic can be shared at the form layer.
How it works
- Parsing rules: "115/08/18", "115-8-18", "115.08.18", "民國115年8月18日", "1150818" as well as "2026-08-18", "2026/08/18", "2026年8月18日" and "20260818" are all accepted; full-width digits and separators are normalized first. A year of 1000 or more is treated as Gregorian, anything smaller as ROC (+1911); a "民國" (Minguo) prefix always forces ROC; 8 plain digits are read as Gregorian
YYYYMMDD, 6–7 digits as ROC(Y)YYMMDD - When parsing fails or the date is outside
min/max, the input turns red, explains why and setsaria-invalid; the previous value is left untouched. Clearing the input and blurring counts as clearing the value - ROC year 1 is Gregorian 1912. In ROC mode, dates in 1911 or earlier keep Gregorian years in the header, the dropdown and the display string — you never see "民國 −3 年";
meta.roc.yearstill reports 0 or a negative number so you can handle it yourself - The year dropdown covers ROC year 1 through 2100 by default; with
min/maxit is narrowed to that span, and the month arrows are disabled at the boundary months - The today marker and the "今天" (Today) button use the device clock and are computed after mount, so SSR and the first client paint match; the button is disabled when today is outside the selectable range
- The popover opens with a spring, month changes slide the old and new grids past each other horizontally (
AnimatePresence mode="popLayout"), and the selected cell pops in with a scale bounce; the grid is fixed at 6 rows / 42 cells so its height never jumps between months - When
nameis given, a hidden input submits the ISO string; the visible input carries noname, so native forms always receiveYYYY-MM-DD
Accessibility
- The input is associated with its
label; the hint / error message lives in arole="status"(aria-live="polite") region referenced byaria-describedby, so parse results are read out by screen readers, andaria-invalidis set on failure - The trigger is a
type="button"witharia-haspopup="dialog",aria-expandedandaria-controls; the popover is arole="dialog"that closes on Esc (returning focus to the trigger), on outside clicks and when focus leaves the component - The calendar uses
role="grid"/row/columnheader/gridcell, witharia-selectedon the chosen day,aria-current="date"on today andaria-disabledon out-of-range days; every cell has a fullaria-label(date + weekday + today / unavailable) - The grid uses a roving tabindex, so only one cell is in the Tab order; arrow keys move by a day or a week, Home / End jump to the start / end of the week, PageUp / PageDown change the month (with Shift, the year), and Enter or Space selects; the visible month follows the focused cell across month boundaries
- The calendar toggle is a
role="group"of twoaria-pressedbuttons and the year dropdown is a nativeselect, so keyboard and screen-reader behavior matches ordinary form controls - Enter in the input checks
isComposingandkeyCode 229before parsing, so nothing is submitted mid-composition in a Chinese IME - When the user has "reduce motion" enabled at the system level, the popover open/close, month slide, selection bounce and hint transitions all switch instantly, leaving only the state and color changes
CJK Vertical Nav
A side navigation bar in vertical Chinese writing — writing-mode vertical typesetting, with an ink line marking the current item.
Chinese Amount Input (React)
A React money input that mirrors Arabic digits into formal Chinese uppercase (新臺幣壹萬貳仟元整) as you type: thousands grouping, jiao/fen handling, one-click copy, and a standalone toChineseUpper() converter for checks, receipts, contracts and quotes.