Skip to content
Lenso UI

RangeCalendar 范围日历

基于 React Aria RangeCalendar 的可组合日期范围选择器,包含月份网格、导航与年份选择支持。

用法

import { RangeCalendar } from '@lenso/ui';

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";
/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. */import { RangeCalendar } from "@lenso/ui";
export function Basic() {  return (    <RangeCalendar aria-label="Trip dates" firstDayOfWeek="mon">      <RangeCalendar.Header>        <RangeCalendar.Heading />        <RangeCalendar.NavButton slot="previous" />        <RangeCalendar.NavButton slot="next" />      </RangeCalendar.Header>      <RangeCalendar.Grid>        <RangeCalendar.GridHeader>          {(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>}        </RangeCalendar.GridHeader>        <RangeCalendar.GridBody>          {(date) => <RangeCalendar.Cell date={date} />}        </RangeCalendar.GridBody>      </RangeCalendar.Grid>    </RangeCalendar>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

组件结构

import {RangeCalendar} from '@lenso/ui';
export default () => (  <RangeCalendar aria-label="Trip dates">    <RangeCalendar.Header>      <RangeCalendar.Heading />      <RangeCalendar.NavButton slot="previous" />      <RangeCalendar.NavButton slot="next" />    </RangeCalendar.Header>    <RangeCalendar.Grid>      <RangeCalendar.GridHeader>        {(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>}      </RangeCalendar.GridHeader>      <RangeCalendar.GridBody>        {(date) => <RangeCalendar.Cell date={date} />}      </RangeCalendar.GridBody>    </RangeCalendar.Grid>  </RangeCalendar>)

示例

禁用

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { CalendarNote, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function Disabled() {  return (    <div {...stylex.props(layout.stack)}>      <RangeCalendar isDisabled aria-label="Trip dates">        <RangeHeader />        <RangeGrid />      </RangeCalendar>      <CalendarNote>Range calendar is disabled</CalendarNote>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

年份选择

RangeCalendar.YearPickerTrigger, RangeCalendar.YearPickerGrid, and their body/cell subcomponents provide an integrated year navigation pattern.

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. */import { RangeCalendar } from "@lenso/ui";import { RangeYearHeader, RangeGrid, RangeYearGrid } from "./demo-parts";export function YearPicker() {  return (    <RangeCalendar aria-label="Trip dates">      <RangeYearHeader />      <RangeGrid />      <RangeYearGrid />    </RangeCalendar>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

默认值

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. */import { RangeCalendar } from "@lenso/ui";import { parseDate } from "@internationalized/date";import { RangeHeader, RangeGrid } from "./demo-parts";export function DefaultValue() {  return (    <RangeCalendar      aria-label="Trip dates"      defaultValue={{ end: parseDate("2025-02-12"), start: parseDate("2025-02-03") }}      firstDayOfWeek="mon"    >      <RangeHeader />      <RangeGrid />    </RangeCalendar>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

受控组件

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX and native Base UI Button. */import { Button, ButtonGroup, RangeCalendar } from "@lenso/ui";import {  getLocalTimeZone,  parseDate,  startOfMonth,  startOfWeek,  today,  type DateValue,} from "@internationalized/date";import { useState } from "react";import { useLocale } from "react-aria-components/I18nProvider";import * as stylex from "@stylexjs/stylex";import { CalendarNote, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function Controlled() {  const [value, setValue] = useState<{ start: DateValue; end: DateValue } | null>(null);  const [focusedDate, setFocusedDate] = useState<DateValue>(() => parseDate("2025-12-25"));  const { locale } = useLocale();  return (    <div {...stylex.props(layout.stack)}>      <ButtonGroup variant="tertiary">        <Button onClick={() => setFocusedDate(today(getLocalTimeZone()))}>This week</Button>        <Button          onClick={() =>            setFocusedDate(startOfWeek(today(getLocalTimeZone()).add({ weeks: 1 }), locale))          }        >          Next week        </Button>        <Button          onClick={() => setFocusedDate(startOfMonth(today(getLocalTimeZone()).add({ months: 1 })))}        >          Next month        </Button>      </ButtonGroup>      <RangeCalendar        aria-label="Trip dates"        firstDayOfWeek="mon"        focusedValue={focusedDate}        value={value}        onChange={setValue}        onFocusChange={setFocusedDate}      >        <RangeHeader />        <RangeGrid />      </RangeCalendar>      <CalendarNote>        Selected range: {value ? `${value.start.toString()} -> ${value.end.toString()}` : "(none)"}      </CalendarNote>      <div {...stylex.props(layout.controls)}>        <Button          size="sm"          variant="secondary"          onClick={() => {            const start = today(getLocalTimeZone());            setValue({ end: start.add({ days: 6 }), start });            setFocusedDate(start);          }}        >          Set 1 week        </Button>        <Button          size="sm"          variant="secondary"          onClick={() => {            const start = parseDate("2025-12-20");            setValue({ end: parseDate("2025-12-31"), start });            setFocusedDate(start);          }}        >          Set Holidays        </Button>        <Button size="sm" variant="tertiary" onClick={() => setValue(null)}>          Clear        </Button>      </div>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

日期范围限制

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import { getLocalTimeZone, today } from "@internationalized/date";import * as stylex from "@stylexjs/stylex";import { CalendarNote, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function MinMaxDates() {  const now = today(getLocalTimeZone());  const maxDate = now.add({ months: 3 });  return (    <div {...stylex.props(layout.stack)}>      <RangeCalendar aria-label="Trip dates" maxValue={maxDate} minValue={now}>        <RangeHeader />        <RangeGrid />      </RangeCalendar>      <CalendarNote>Select dates between today and {maxDate.toString()}</CalendarNote>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

不可用日期

使用 isDateUnavailable 屏蔽周末、节假日或已预订等不可用日期。

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import { getLocalTimeZone, today } from "@internationalized/date";import * as stylex from "@stylexjs/stylex";import { CalendarNote, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function UnavailableDates() {  const now = today(getLocalTimeZone());  const blockedRanges = [    [now.add({ days: 2 }), now.add({ days: 5 })],    [now.add({ days: 12 }), now.add({ days: 13 })],  ] as const;  return (    <div {...stylex.props(layout.stack)}>      <RangeCalendar        aria-label="Trip dates"        defaultValue={{ end: now.add({ days: 9 }), start: now.add({ days: 6 }) }}        firstDayOfWeek="mon"        isDateUnavailable={(date) =>          blockedRanges.some(([start, end]) => date.compare(start) >= 0 && date.compare(end) <= 0)        }      >        <RangeHeader />        <RangeGrid />      </RangeCalendar>      <CalendarNote>Some days are unavailable</CalendarNote>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

基于锚点的不可用日期

选择范围时,isDateUnavailable 会收到第二个参数 anchorDate(首个选中日期)。可用它限制哪些结束日期有效(例如起始日期后 7 天内)。

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import { getLocalTimeZone, today } from "@internationalized/date";import * as stylex from "@stylexjs/stylex";import { CalendarNote, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function AnchorUnavailableDates() {  return (    <div {...stylex.props(layout.stack)}>      <RangeCalendar        aria-label="Trip dates"        isDateUnavailable={(date, anchorDate) =>          anchorDate != null && Math.abs(date.compare(anchorDate)) > 7        }        minValue={today(getLocalTimeZone())}      >        <RangeHeader />        <RangeGrid />      </RangeCalendar>      <CalendarNote>        After selecting a start date, only dates within 7 days are available      </CalendarNote>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

月份周数

将 weeksInMonth 设为固定值(例如 6),在月份切换时保持网格高度稳定。

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { CalendarNote, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function WeeksInMonth() {  return (    <div {...stylex.props(layout.stack)}>      <RangeCalendar aria-label="Trip dates" weeksInMonth={6}>        <RangeHeader />        <RangeGrid />      </RangeCalendar>      <CalendarNote>        Always shows 6 weeks per month to avoid layout shift when navigating      </CalendarNote>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

周视图

设置 visibleDuration={{ weeks: n }} 一次显示一个或多个周。导航按可见周范围前进。显示多周时,使用 pageBehavior="single" 每次移动一周。

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX and native Base UI Select. */import { RangeCalendar } from "@lenso/ui";import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { DurationSelect, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function WeekView() {  const [weeks, setWeeks] = useState(1);  return (    <div {...stylex.props(layout.controlsStack)}>      <DurationSelect unit="weeks" value={weeks} onChange={setWeeks} />      <RangeCalendar key={weeks} aria-label="Trip dates" visibleDuration={{ weeks }}>        <RangeHeader />        <RangeGrid />      </RangeCalendar>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

日视图

设置 visibleDuration={{ days: n }} 显示连续日期的滚动窗口。导航按可见日范围前进。显示多天时,使用 pageBehavior="single" 每次移动一天。

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX and native Base UI Select. */import { RangeCalendar } from "@lenso/ui";import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { DurationSelect, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function DayView() {  const [days, setDays] = useState(5);  return (    <div {...stylex.props(layout.controlsStack)}>      <DurationSelect unit="days" value={days} onChange={setDays} />      <RangeCalendar key={days} aria-label="Trip dates" visibleDuration={{ days }}>        <RangeHeader />        <RangeGrid />      </RangeCalendar>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

非连续范围

启用 allowsNonContiguousRanges 以允许跨越不可用日期进行选择。

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import { getLocalTimeZone, today } from "@internationalized/date";import * as stylex from "@stylexjs/stylex";import { CalendarNote, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function AllowsNonContiguousRanges() {  const now = today(getLocalTimeZone());  const blockedRanges = [    [now.add({ days: 2 }), now.add({ days: 5 })],    [now.add({ days: 12 }), now.add({ days: 13 })],  ] as const;  return (    <div {...stylex.props(layout.stack)}>      <RangeCalendar        allowsNonContiguousRanges        aria-label="Trip dates"        defaultValue={{ end: now.add({ days: 9 }), start: now.add({ days: 1 }) }}        firstDayOfWeek="mon"        isDateUnavailable={(date) =>          blockedRanges.some(([start, end]) => date.compare(start) >= 0 && date.compare(end) <= 0)        }      >        <RangeHeader />        <RangeGrid />      </RangeCalendar>      <CalendarNote>Non-contiguous ranges are allowed across unavailable dates</CalendarNote>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

只读

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import { getLocalTimeZone, today } from "@internationalized/date";import * as stylex from "@stylexjs/stylex";import { CalendarNote, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function ReadOnly() {  const now = today(getLocalTimeZone());  return (    <div {...stylex.props(layout.stack)}>      <RangeCalendar        isReadOnly        aria-label="Trip dates"        defaultValue={{ start: now, end: now.add({ days: 4 }) }}      >        <RangeHeader />        <RangeGrid />      </RangeCalendar>      <CalendarNote>Range calendar is read-only</CalendarNote>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

无效状态

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import { getLocalTimeZone, today, type DateValue } from "@internationalized/date";import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { CalendarNote, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";const styles = stylex.create({ error: { fontSize: 14, color: "var(--danger)" } });export function Invalid() {  const now = today(getLocalTimeZone());  const [value, setValue] = useState<{ start: DateValue; end: DateValue }>({    end: now.add({ days: 14 }),    start: now.add({ days: 6 }),  });  const isInvalid = value.end.compare(value.start) > 7;  return (    <div {...stylex.props(layout.stack)}>      <RangeCalendar        aria-label="Trip dates"        firstDayOfWeek="mon"        isInvalid={isInvalid}        value={value}        onChange={setValue}      >        <RangeHeader />        <RangeGrid />      </RangeCalendar>      {isInvalid ? (        <p {...stylex.props(styles.error)}>Maximum stay duration is 1 week</p>      ) : (        <CalendarNote>Select a stay of up to 7 days</CalendarNote>      )}    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

聚焦值

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX and native Base UI Button. */import { Button, RangeCalendar } from "@lenso/ui";import { parseDate, type DateValue } from "@internationalized/date";import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { CalendarNote, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function FocusedValue() {  const [focusedDate, setFocusedDate] = useState<DateValue>(() => parseDate("2025-06-15"));  return (    <div {...stylex.props(layout.stack)}>      <RangeCalendar        aria-label="Trip dates"        firstDayOfWeek="mon"        focusedValue={focusedDate}        onFocusChange={setFocusedDate}      >        <RangeHeader />        <RangeGrid />      </RangeCalendar>      <CalendarNote>Focused: {focusedDate.toString()}</CalendarNote>      <div {...stylex.props(layout.controls)}>        <Button          size="sm"          variant="secondary"          onClick={() => setFocusedDate(parseDate("2025-01-01"))}        >          Go to Jan        </Button>        <Button          size="sm"          variant="secondary"          onClick={() => setFocusedDate(parseDate("2025-06-15"))}        >          Go to Jun        </Button>        <Button          size="sm"          variant="secondary"          onClick={() => setFocusedDate(parseDate("2025-12-25"))}        >          Go to Christmas        </Button>      </div>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

单元格标记

可自定义 RangeCalendar.Cell 的子内容,并使用 RangeCalendar.CellIndicator 展示事件等元数据。

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. */import { RangeCalendar } from "@lenso/ui";import { getLocalTimeZone, isToday } from "@internationalized/date";import { RangeHeader, RangeGrid } from "./demo-parts";const datesWithEvents = [3, 7, 12, 15, 21, 28];export function WithIndicators() {  return (    <RangeCalendar aria-label="Trip dates">      <RangeHeader />      <RangeGrid        cell={(date) => (          <RangeCalendar.Cell date={date}>            {({ formattedDate }) => (              <>                {formattedDate}                {(isToday(date, getLocalTimeZone()) || datesWithEvents.includes(date.day)) && (                  <RangeCalendar.CellIndicator />                )}              </>            )}          </RangeCalendar.Cell>        )}      />    </RangeCalendar>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

典型场景

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX and native Base UI Button. */import { Button, RangeCalendar } from "@lenso/ui";import { getLocalTimeZone, isWeekend, today, type DateValue } from "@internationalized/date";import { useState } from "react";import { useLocale } from "react-aria-components/I18nProvider";import * as stylex from "@stylexjs/stylex";import { layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";const blockedDates = [5, 6, 12, 13, 14, 20];export function BookingCalendar() {  const [selectedRange, setSelectedRange] = useState<{ start: DateValue; end: DateValue } | null>(    null,  );  const { locale } = useLocale();  return (    <div {...stylex.props(layout.stack)}>      <RangeCalendar        aria-label="Booking range"        isDateUnavailable={(date) => isWeekend(date, locale) || blockedDates.includes(date.day)}        minValue={today(getLocalTimeZone())}        value={selectedRange}        onChange={setSelectedRange}      >        <RangeHeader />        <RangeGrid          cell={(date) => (            <RangeCalendar.Cell date={date}>              {({ formattedDate, isUnavailable }) => (                <>                  {formattedDate}                  {!isUnavailable &&                    !isWeekend(date, locale) &&                    blockedDates.includes(date.day) && <RangeCalendar.CellIndicator />}                </>              )}            </RangeCalendar.Cell>          )}        />      </RangeCalendar>      <div {...stylex.props(layout.bookingDetails)}>        <div {...stylex.props(layout.legend)}>          <span {...stylex.props(layout.legendItem)}>            <span {...stylex.props(layout.mutedDot)} /> Blocked dates          </span>          <span {...stylex.props(layout.legendItem)}>            <span {...stylex.props(layout.defaultDot)} /> Weekend/Unavailable          </span>        </div>        {selectedRange ? (          <Button size="sm" variant="primary">            Book {selectedRange.start.toString()} {"->"} {selectedRange.end.toString()}          </Button>        ) : null}      </div>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

多月份展示

结合 visibleDuration 与 offset 渲染多个网格,适用于预订与规划场景。

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  root: { containerType: "normal", width: "100%", maxWidth: "none", overflowX: "auto" },  months: { marginInline: "auto", display: "flex", width: "max-content", gap: 32 },  month: { width: 256 },  heading: { flex: "none" },  spacer: { width: 24, height: 24 },});export function MultipleMonths() {  return (    <RangeCalendar aria-label="Trip dates" xstyle={styles.root} visibleDuration={{ months: 2 }}>      <div {...stylex.props(styles.months)}>        <div {...stylex.props(styles.month)}>          <RangeCalendar.Header>            <RangeCalendar.NavButton slot="previous" />            <RangeCalendar.Heading xstyle={styles.heading} />            <div {...stylex.props(styles.spacer)} />          </RangeCalendar.Header>          <RangeCalendar.Grid>            <RangeCalendar.GridHeader>              {(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>}            </RangeCalendar.GridHeader>            <RangeCalendar.GridBody>              {(date) => <RangeCalendar.Cell date={date} />}            </RangeCalendar.GridBody>          </RangeCalendar.Grid>        </div>        <div {...stylex.props(styles.month)}>          <RangeCalendar.Header>            <div {...stylex.props(styles.spacer)} />            <RangeCalendar.Heading xstyle={styles.heading} offset={{ months: 1 }} />            <RangeCalendar.NavButton slot="next" />          </RangeCalendar.Header>          <RangeCalendar.Grid offset={{ months: 1 }}>            <RangeCalendar.GridHeader>              {(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>}            </RangeCalendar.GridHeader>            <RangeCalendar.GridBody>              {(date) => <RangeCalendar.Cell date={date} />}            </RangeCalendar.GridBody>          </RangeCalendar.Grid>        </div>      </div>    </RangeCalendar>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

国际化日历

默认情况下,RangeCalendar 使用用户 locale 的历法系统显示日期。可用 I18nProvider 包裹组件,并设置 Unicode 历法 locale 扩展 覆盖。

以下示例展示印度历法系统:

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. */import { RangeCalendar } from "@lenso/ui";import { I18nProvider } from "react-aria-components/I18nProvider";import { RangeYearHeader, RangeGrid, RangeYearGrid } from "./demo-parts";export function InternationalCalendar() {  return (    <I18nProvider locale="hi-IN-u-ca-indian">      <RangeCalendar aria-label="Trip dates">        <RangeYearHeader />        <RangeGrid />        <RangeYearGrid />      </RangeCalendar>    </I18nProvider>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

注意: 无论显示 locale 如何,onChange 事件始终返回与 value 或 defaultValue 相同历法系统的日期(未提供 value 时为公历)。

自定义样式

Tailwind CSS

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import { parseDate } from "@internationalized/date";import * as stylex from "@stylexjs/stylex";
const styles = stylex.create({  root: {    width: 252,    borderRadius: "var(--radius-xl)",    borderWidth: 1,    borderStyle: "solid",    borderColor: "color-mix(in oklab,var(--border) 80%,transparent)",    backgroundColor: "var(--surface)",    padding: 12,    boxShadow:      "0 1px 2px 0 rgb(0 0 0 / .05),0 0 0 1px color-mix(in oklab,var(--success) 10%,transparent)",  },  heading: { color: "var(--foreground)" },  nav: {    borderRadius: "var(--radius-md)",    color: "var(--success)",    backgroundColor: {      default: "transparent",      ":hover": "var(--success-soft)",      ":is([data-hovered])": "var(--success-soft)",    },  },  cell: {    borderRadius: { default: "var(--radius-md)", ":is([data-selected])": 0 },    borderTopLeftRadius: {      default: "var(--radius-md)",      ":is([data-selected])": 0,      ":is([data-selection-start])": "var(--radius-md)",    },    borderBottomLeftRadius: {      default: "var(--radius-md)",      ":is([data-selected])": 0,      ":is([data-selection-start])": "var(--radius-md)",    },    borderTopRightRadius: {      default: "var(--radius-md)",      ":is([data-selected])": 0,      ":is([data-selection-end])": "var(--radius-md)",    },    borderBottomRightRadius: {      default: "var(--radius-md)",      ":is([data-selected])": 0,      ":is([data-selection-end])": "var(--radius-md)",    },    backgroundColor: {      default: null,      ":is([data-selected])": "var(--success-soft)",      ":is([data-selected][data-outside-month])":        "color-mix(in oklab,var(--default) 20%,transparent)",    },  },  button: {    borderRadius: "var(--radius-md)",    backgroundColor: {      default: null,      ":is([data-hovered]:not([data-selected]) > *)": "var(--default)",      ":is([data-today] > *)": "var(--success-soft)",      ":is([data-today][data-hovered]:not([data-selected]) > *)": "var(--success-soft-hover)",      ":is([data-selection-start] > *,[data-selection-end] > *)": "var(--success)",      ":is([data-selection-start][data-pressed] > *,[data-selection-end][data-pressed] > *)":        "var(--success-hover)",    },    color: {      default: null,      ":is([data-today] > *)": "var(--success-soft-foreground)",      ":is([data-selection-start] > *,[data-selection-end] > *)": "var(--success-foreground)",    },  },});export function CustomStyles() {  return (    <RangeCalendar      aria-label="Hotel stay"      xstyle={styles.root}      defaultValue={{ end: parseDate("2025-02-14"), start: parseDate("2025-02-08") }}      firstDayOfWeek="mon"    >      <RangeCalendar.Header>        <RangeCalendar.Heading xstyle={styles.heading} />        <RangeCalendar.NavButton xstyle={styles.nav} slot="previous" />        <RangeCalendar.NavButton xstyle={styles.nav} slot="next" />      </RangeCalendar.Header>      <RangeCalendar.Grid>        <RangeCalendar.GridHeader>          {(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>}        </RangeCalendar.GridHeader>        <RangeCalendar.GridBody>          {(date) => (            <RangeCalendar.Cell date={date} xstyle={styles.cell} buttonXstyle={styles.button} />          )}        </RangeCalendar.GridBody>      </RangeCalendar.Grid>    </RangeCalendar>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

全局 CSS

若要自定义组件类,可使用 @layer components 指令。了解更多。

@layer components {  .range-calendar {    @apply w-80 rounded-2xl border border-border bg-surface p-3 shadow-sm;  }
  .range-calendar__heading {    @apply text-sm font-semibold text-default;  }
  .range-calendar__cell[data-selected="true"] .range-calendar__cell-button {    @apply bg-accent text-accent-foreground;  }}

样式参考

HeroUI 遵循 BEM 方法论,确保组件变体与状态可复用且易于自定义。

CSS 类

RangeCalendar 在 packages/styles/components/range-calendar.css 与 packages/styles/components/calendar-year-picker.css 中使用以下类:

  • .range-calendar - 根容器。
  • .range-calendar__header - 含导航按钮与标题的头部行。
  • .range-calendar__heading - 当前月份标签。
  • .range-calendar__nav-button - 上一月/下一月导航控件。
  • .range-calendar__grid - 主体日期网格。
  • .range-calendar__grid-header - 星期标题行外层。
  • .range-calendar__grid-body - 日期行外层。
  • .range-calendar__header-cell - 星期标题单元格。
  • .range-calendar__cell - 可交互日期单元格外层。
  • .range-calendar__cell-button - 单元格内的可交互日期按钮。
  • .range-calendar__cell-indicator - 日期单元格内的圆点指示器。
  • .calendar-year-picker__trigger - 年份选择器切换按钮。
  • .calendar-year-picker__trigger-heading - 年份选择触发器内的标题文案。
  • .calendar-year-picker__trigger-indicator - 年份选择触发器内的指示图标。
  • .calendar-year-picker__year-grid - 可选年份的覆盖网格。
  • .calendar-year-picker__year-cell - 单个年份选项。

交互状态

RangeCalendar 同时支持伪类与 React Aria 的 data 属性:

  • 已选中:[data-selected="true"]
  • 范围起点:[data-selection-start="true"]
  • 范围终点:[data-selection-end="true"]
  • 范围内:[data-selection-in-range="true"]
  • 今天:[data-today="true"]
  • 不可用:[data-unavailable="true"]
  • 跨月:[data-outside-month="true"]
  • 悬停::hover 或 [data-hovered="true"]
  • 按下::active 或 [data-pressed="true"]
  • 焦点可见::focus-visible 或 [data-focus-visible="true"]
  • 禁用::disabled 或 [data-disabled="true"]

API 参考

RangeCalendar

RangeCalendar 继承 React Aria RangeCalendar 的全部 props。

Prop类型默认值描述
valueRangeValue<DateValue> | null-受控的选中范围。
defaultValueRangeValue<DateValue> | null-初始选中范围(非受控)。
onChange(value: RangeValue<DateValue>) => void-选中变化时调用。
focusedValueDateValue-受控的焦点日期。
onFocusChange(value: DateValue) => void-焦点移动到其它日期时调用。
minValueDateValue历法感知的 1900-01-01可选的最早日期。
maxValueDateValue历法感知的 2099-12-31可选的最晚日期。
weeksInMonthnumber-一个月的周数。该值会覆盖区域设置的默认值。
isDateUnavailable(date: DateValue, anchorDate: CalendarDate | null) => boolean-将日期标记为不可用。anchorDate 为当前范围选择中的首个日期。
firstDayOfWeek'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'-覆盖区域设置的一周起始日。
pageBehavior'visible' | 'single''visible'翻页按可见范围或单步前进。
selectionAlignment'start' | 'center' | 'end''center'初始渲染时按选中项对齐可见范围。
allowsNonContiguousRangesbooleanfalse允许范围跨越不可用日期。
isDisabledbooleanfalse禁用交互与选择。
isReadOnlybooleanfalse内容只读,不可更改选中。
isInvalidbooleanfalse标记为无效以配合校验样式。
visibleDuration{months?: number; weeks?: number; days?: number}{months: 1}可见时间范围。使用 { months: n } 为月视图,{ weeks: n } 为周视图,{ days: n } 为日视图。
defaultYearPickerOpenbooleanfalse内置年份选择器的初始展开状态。
isYearPickerOpenboolean-受控的年份选择器展开状态。
onYearPickerOpenChange(isOpen: boolean) => void-年份选择器展开状态变化时调用。

组合部件

组件描述
RangeCalendar.Header导航与标题的头部容器。
RangeCalendar.Heading可见范围的格式化标题。支持 offset(多月份布局)与 format(月/年/日格式选项)。
RangeCalendar.NavButton上一页/下一页导航(slot="previous" 或 slot="next")。
RangeCalendar.Grid单个月的日期网格(多月份布局支持 offset)。
RangeCalendar.GridHeader星期标题容器。
RangeCalendar.GridBody日期单元格主体容器。
RangeCalendar.HeaderCell星期标签单元格。
RangeCalendar.Cell单个日期单元格。
RangeCalendar.CellIndicator用于自定义元数据的可选指示元素。
RangeCalendar.YearPickerTrigger切换年份选择模式的触发器。
RangeCalendar.YearPickerTriggerHeading年份选择触发器内的本地化标题内容。
RangeCalendar.YearPickerTriggerIndicator年份选择触发器内的切换图标。
RangeCalendar.YearPickerGrid年份选择覆盖网格容器。
RangeCalendar.YearPickerGridBody年份网格单元格的 body 渲染器。
RangeCalendar.YearPickerCell单个年份选项单元格。

年份选择器子组件

年份选择器子组件继承 React Aria useCalendarHeading 与 useCalendarYearPicker 的格式化属性。

组件属性类型默认值描述
RangeCalendar.YearPickerTriggerHeadingformatDateFormatterOptions-自定义月/年标题(如 {month: 'short'})。
RangeCalendar.YearPickerTriggerHeadingoffset{months?: number}-相对聚焦日期偏移标题(多月布局)。
RangeCalendar.YearPickerGridformatDateFormatterOptions{year: 'numeric'}自定义年份单元格标签(纪元、历法系统等)。
RangeCalendar.YearPickerGridvisibleYearsnumbermin–max 跨度或 20滑动窗口中显示的年份数量。当同时设置 minValue 与 maxValue 时,默认为二者之间的完整范围。

RangeCalendar.Cell Render

当 RangeCalendar.Cell 的 children 为函数时,可使用 React Aria 的渲染参数:

Prop类型描述
formattedDatestring单元格日期的本地化文案。
isSelectedboolean该日期是否已选中。
isSelectionStartboolean是否为选中范围的起点。
isSelectionEndboolean是否为选中范围的终点。
isUnavailableboolean该日期是否不可用。
isDisabledboolean单元格是否禁用。
isOutsideMonthboolean是否属于相邻月份。

支持的历法系统及其标识符完整列表见:

  • @internationalized/date — 各日期组件共用的日期类型(CalendarDate、CalendarDateTime、ZonedDateTime)与工具函数
  • I18nProvider — 为子树覆盖语言环境
  • useLocale — 读取当前语言环境与书写方向

相关组件