Chinese Localization

Solar Terms Timeline

A horizontal timeline of the 24 solar terms for React: locates the current term for any date, counts down to the next one, with seasonal colour bands and phenology / seasonal-food cards; term dates come from the standard 21st-century approximation, getSolarTerms and currentSolarTerm are exported, and almanac values can override the formula.

A content component for tea, produce, wellness and calendar-style sites. The 24 solar terms sit in order on a horizontally scrollable track, split into four seasonal colour bands (spring green, summer vermilion, autumn gold, winter blue); the component locates the current term from date (defaults to today, resolved after mount) — that node scales up, breathes with a soft glow and is scrolled into view automatically. A summary card above shows today's term, how many days remain until the next one, and a one-line phenology / seasonal-food note (24 built-in Traditional Chinese lines, each overridable). Term dates are computed with the common 21st-century approximation (the "Shouxing" formula), which is occasionally off by ±1 day, so almanac values can be passed in as overrides; getSolarTerms() and currentSolarTerm() are exported separately for use elsewhere.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add solar-terms-timeline.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

Active edited usageDefault props
<SolarTermsTimeline/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/solar-terms-timeline.en.json

Or, once registries are configured in components.json, install it as @webberui/solar-terms-timeline.

Usage

import {
  SolarTermsTimeline,
  getSolarTerms,
  currentSolarTerm,
} from "@/components/ui/solar-terms-timeline";

// Simplest form: with no date it locates "today" after mount
<SolarTermsTimeline onSelect={(term) => console.log(term.name, term.date)} />

// Fixed date, horizontal term names, almanac override for the formula's ±1-day error, custom note
<SolarTermsTimeline
  date="2026-02-18"
  orientation="horizontal"
  terms={[{ name: "Rain Water", date: "2026-02-18" }]}
  descriptions={{ Rain Water: "Spring teaPre-orderstarts now, Enjoy warm tea on cold, damp days. " }}
/>

// The helpers can be used on their own
getSolarTerms(2026);
// [{ index: 0, name: "Minor Cold", en: "Minor Cold", season: "winter", date: 2026-01-05 }, … 24 entries]

const { current, next, daysUntilNext } = currentSolarTerm(new Date(2026, 7, 18));
// current.name === "Beginning of Autumn" (Start of Autumn), next.name === "End of Heat" (End of Heat), daysUntilNext === 5

Props

PropTypeDefaultDescription
dateDate | stringtoday (resolved after mount)Anchor date; strings use YYYY-MM-DD. When omitted or unparseable, "today" is taken after mount, and SSR / the first render show a placeholder state
yearnumberthe year of dateWhich year's 24 terms the timeline shows; when it differs from the year of date, no node is highlighted
termsSolarTermOverride[]—Almanac overrides for specific term dates, correcting the formula's ±1-day error
descriptionsRecord<string, string>24 built-in Traditional Chinese notesOverride the phenology / food notes, keyed by term name; terms not overridden keep the built-in text
orientation"vertical" | "horizontal""vertical"Layout of the term name on each node: vertical (top-to-bottom) or horizontal
showSummarybooleantrueShow the summary card above the track (today's / selected term, countdown, note, back-to-today)
locale"zh-TW" | "en""zh-TW"Language of the built-in copy; with en the node title becomes the English term name and the Chinese name is shown small underneath
labelsPartial<SolarTermsTimelineLabels>—Override individual strings; takes precedence over the locale defaults
valuestring | null—Controlled: name of the selected term; null means "follow today's term". Uncontrolled when omitted
defaultValuestring—Initially selected term name in uncontrolled mode
onSelect(term: SolarTerm) => void—Fires when a node is clicked, the keyboard moves the selection, or "Back to today" is pressed, with that term's data
classNamestring—Forwarded to the outermost container

SolarTerm

FieldTypeDefaultDescription
indexnumber—Position within the year, 0 is Minor Cold (Minor Cold) and 23 is Winter Solstice (Winter Solstice)
namestring—Term name in Traditional Chinese
enstring—English name, e.g. "Start of Spring"
season"spring" | "summer" | "autumn" | "winter"—Season: Beginning of Spring–Grain Rain spring, Beginning of Summer–Great Heat summer, Beginning of Autumn–Frost's Descent autumn, The Beginning of Winter–Great Cold winter
dateDate—Date the term begins (local midnight)

SolarTermOverride

FieldTypeDefaultDescription
namestring—Name of the term to override; must be one of the 24
datestring—Almanac date as YYYY-MM-DD; an unparseable entry is ignored and the formula value is kept

SolarTermStatus

Return value of currentSolarTerm(date, terms?):

FieldTypeDefaultDescription
currentSolarTerm—The term the date falls in; before Minor Cold (early January) it is last year's Winter Solstice
nextSolarTerm—The following term; after Winter Solstice it is next year's Minor Cold
daysUntilNextnumber—Days until the next term, 0 on the day the term begins
daysSinceStartnumber—Days since the current term began, 0 on the day it begins

SolarTermsTimelineLabels

Strings overridable through labels; fields containing {name}, {days} or {n} placeholders are filled in at render time:

FieldTypeDefaultDescription
timelinestring"Twenty-Four Solar Terms Timeline" (24 solar terms timeline)Accessible name of the role="region" wrapper
termsstring"Twenty-Four Solar Terms" (The 24 solar terms)Accessible name of the role="radiogroup" track
currentstring"Today's Solar Term" (Today's term)Summary-card heading when showing today's term
selectedstring"Solar Term" (Solar term)Summary-card heading when showing a term the user picked
nextUpstring"Next Solar Term" (Next term)Countdown heading
dayUnitstring"days" (day)Day unit, singular
daysUnitstring"days" (days)Day unit, plural
daysUntilstring"{days} days until "{name}"" ({days} days until {name})Distance text when a future term is selected
daysAgostring"{days} days have passed since "{name}"" ({name} was {days} days ago)Distance text when a past term is selected
isTodaystring"Today is the day "{name}" begins its solar term" (Today is {name})Text shown when the anchor date is exactly the day a term begins
ordinalstring"The {n}th Solar Term" (Term {n} of 24)Ordinal of the term
backToTodaystring"Back to Today" (Back to today)Label of the back-to-today button
pendingstring"Retrieving today's date..." (Getting today's date…)Placeholder before the date is known (pre-mount)
currentMarkstring"Current Solar Term" (current term)"Current term" note inside each node's aria-label
spring / summer / autumn / winterstring"Spring" / "Summer" / "Autumn" / "Winter" (Spring / Summer / Autumn / Winter)Season names shown at the start of each band and on the summary-card chip

getSolarTerms(year, overrides?)

getSolarTerms(year: number, overrides?: SolarTermOverride[]): SolarTerm[] — returns the 24 terms of that year (from Minor Cold to Winter Solstice), computed with the Shouxing formula, most accurate for 2001–2100. Pass overrides to replace specific dates with almanac values.

currentSolarTerm(date, terms?)

currentSolarTerm(date: Date, terms?: SolarTerm[]): SolarTermStatus — finds the term a date falls in, the next term and the distances in days. terms may be the (already overridden) term list for that year; when omitted the formula is used. The cross-year values — last year's Winter Solstice and next year's Minor Cold — are always computed by the formula.

The constants SOLAR_TERM_NAMES (the 24 names in year order) and SOLAR_TERM_DESCRIPTIONS (the 24 built-in notes keyed by term name), plus the SolarTermSeason type, are also named exports.

How it works

  • Date formula: day = int(Y × 0.2422 + C) − L. Y is the last two digits of the year (2001–2100 → 1–100), C is each term's 21st-century constant (Minor Cold 5.4055, Great Cold 20.12, Beginning of Spring 3.87, Rain Water 18.73, Awakening of Insects 5.63, Spring Equinox 20.646, Qingming Festival 4.81, Grain Rain 20.1, Beginning of Summer 5.52, Lesser Fullness 21.04, Grain in Ear 5.678, Summer Solstice 21.37, Lesser Heat 7.108, Great Heat 22.83, Beginning of Autumn 7.5, End of Heat 23.13, White Dew 7.646, Autumn Equinox 23.042, Cold Dew 8.318, Frost's Descent 23.438, The Beginning of Winter 7.438, Lesser Snow 22.36, Heavy Snow 7.18, Winter Solstice 21.94). L is the leap-year correction: the January / February terms (Minor Cold, Great Cold, Beginning of Spring, Rain Water) use int((Y − 1) / 4), the rest use int(Y / 4) — in January and February that year's 29 February has not happened yet, so one fewer accumulated leap day counts; using a single L for all terms would shift a whole batch of them by a day in every leap year
  • ±1-day error: the approximation occasionally differs from the almanac by one day (known cases include Rain Water 2026, Lesser Heat 2016 and Winter Solstice 2021); for production, override with almanac values via terms. The demo and playground show formula values
  • Year boundary: the timeline is cyclic — in early January the "current term" is really last year's Winter Solstice, and the component still highlights this year's Winter Solstice node; after Winter Solstice the "next term" is next year's Minor Cold and the countdown crosses the year correctly
  • Selection and summary card: with nothing selected the card shows today's term and "N days until the next term"; after picking another node it shows that term, how many days before / after today it falls, and a "Back to today" button. A value of null means "follow today's term" — in controlled mode, sync it inside onSelect
  • Auto-scroll: only the track's own horizontal scrollbar moves (it computes offsetLeft and calls scrollTo on the track) rather than scrollIntoView — the latter also scrolls the whole page, so the page would jump whenever the component sits off-screen
  • SSR: without a date, the server and the first render list the 24 term names only, with no dates and a placeholder in the summary card; today is resolved after mount and the dates fill in without layout shift because the node height is reserved

Accessibility

  • The outer wrapper is role="region" named by a visually hidden heading (labels.timeline); the track is role="radiogroup" and the 24 nodes are native type="button" elements with role="radio" — aria-checked marks the user's selection, aria-current="date" marks today's term, and each aria-label contains the term name, date and a "current term" note
  • Keyboard: roving tabindex — only the current node is in the Tab order; the arrow keys (left / right, up / down) move through the 24 nodes cyclically and select as they go, Home / End jump to Minor Cold / Winter Solstice, and the track scrolls along with focus
  • The summary-card note lives in an aria-live="polite" region, so switching terms is announced by screen readers; the track points at the card via aria-describedby
  • Seasonal bands and the glow are aria-hidden decoration; seasons are not conveyed by colour alone — each band starts with the season name and the summary-card chip carries text and an icon
  • When the user has "reduce motion" enabled at the system level, the breathing glow on the current node is replaced by a static faint halo, node scaling and note fades switch instantly, and the auto-scroll uses behavior: "auto" to jump straight to the position