WebberUI

Solar Terms Timeline (React)

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.

Loading preview…
npx shadcn@latest add https://webberui.com/r/solar-terms-timeline.json

Playground

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

<SolarTermsTimeline />

Installation

npx shadcn@latest add https://webberui.com/r/solar-terms-timeline.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: "雨水", date: "2026-02-18" }]}
  descriptions={{ 雨水: "春茶預購開跑,濕冷天請多喝溫熱的茶。" }}
/>

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

const { current, next, daysUntilNext } = currentSolarTerm(new Date(2026, 7, 18));
// current.name === "立秋" (Start of Autumn), next.name === "處暑" (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 | nullControlled: name of the selected term; null means "follow today's term". Uncontrolled when omitted
defaultValuestringInitially selected term name in uncontrolled mode
onSelect(term: SolarTerm) => voidFires when a node is clicked, the keyboard moves the selection, or "Back to today" is pressed, with that term's data
classNamestringForwarded to the outermost container

SolarTerm

FieldTypeDefaultDescription
indexnumberPosition within the year, 0 is 小寒 (Minor Cold) and 23 is 冬至 (Winter Solstice)
namestringTerm name in Traditional Chinese
enstringEnglish name, e.g. "Start of Spring"
season"spring" | "summer" | "autumn" | "winter"Season: 立春–穀雨 spring, 立夏–大暑 summer, 立秋–霜降 autumn, 立冬–大寒 winter
dateDateDate the term begins (local midnight)

SolarTermOverride

FieldTypeDefaultDescription
namestringName of the term to override; must be one of the 24
datestringAlmanac date as YYYY-MM-DD; an unparseable entry is ignored and the formula value is kept

SolarTermStatus

Return value of currentSolarTerm(date, terms?):

FieldTypeDefaultDescription
currentSolarTermThe term the date falls in; before 小寒 (early January) it is last year's 冬至
nextSolarTermThe following term; after 冬至 it is next year's 小寒
daysUntilNextnumberDays until the next term, 0 on the day the term begins
daysSinceStartnumberDays 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"二十四節氣時間軸" (24 solar terms timeline)Accessible name of the role="region" wrapper
termsstring"二十四節氣" (The 24 solar terms)Accessible name of the role="radiogroup" track
currentstring"今日節氣" (Today's term)Summary-card heading when showing today's term
selectedstring"節氣" (Solar term)Summary-card heading when showing a term the user picked
nextUpstring"下一節氣" (Next term)Countdown heading
dayUnitstring"天" (day)Day unit, singular
daysUnitstring"天" (days)Day unit, plural
daysUntilstring"距「{name}」還有 {days} 天" ({days} days until {name})Distance text when a future term is selected
daysAgostring"「{name}」已過 {days} 天" ({name} was {days} days ago)Distance text when a past term is selected
isTodaystring"今天正是「{name}」交節之日" (Today is {name})Text shown when the anchor date is exactly the day a term begins
ordinalstring"第 {n} 節氣" (Term {n} of 24)Ordinal of the term
backToTodaystring"回到今日" (Back to today)Label of the back-to-today button
pendingstring"正在取得今天的日期…" (Getting today's date…)Placeholder before the date is known (pre-mount)
currentMarkstring"目前節氣" (current term)"Current term" note inside each node's aria-label
spring / summer / autumn / winterstring"春" / "夏" / "秋" / "冬" (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 小寒 to 冬至), 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 冬至 and next year's 小寒 — 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 (小寒 5.4055, 大寒 20.12, 立春 3.87, 雨水 18.73, 驚蟄 5.63, 春分 20.646, 清明 4.81, 穀雨 20.1, 立夏 5.52, 小滿 21.04, 芒種 5.678, 夏至 21.37, 小暑 7.108, 大暑 22.83, 立秋 7.5, 處暑 23.13, 白露 7.646, 秋分 23.042, 寒露 8.318, 霜降 23.438, 立冬 7.438, 小雪 22.36, 大雪 7.18, 冬至 21.94). L is the leap-year correction: the January / February terms (小寒, 大寒, 立春, 雨水) 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 雨水 2026, 小暑 2016 and 冬至 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 冬至, and the component still highlights this year's 冬至 node; after 冬至 the "next term" is next year's 小寒 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 小寒 / 冬至, 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

On this page