DateRangeField 日期范围输入框

分段输入开始与结束日期。

2026
9
1
2026
9
30

值:2026-09-01 至 2026-09-30

'use client'

import { useState } from 'react'
import { CalendarRange } from 'lucide-react'
import { DateRangeField, Stack, Text, type DateRangeValue } from '@hina-ui/react'

export default function Demo() {
  const [range, setRange] = useState<DateRangeValue | null>({
    start: '2026-09-01',
    end: '2026-09-30',
  })
  return (
    <Stack gap="sm" align="start">
      <DateRangeField
        value={range}
        onValueChange={setRange}
        clearable
        aria-label="活动期间"
        className="w-96"
        leading={<CalendarRange />}
      />
      <Text tone="muted" size="sm">
        值:{range ? `${range.start ?? '空'} 至 ${range.end ?? '空'}` : '空'}
      </Text>
    </Stack>
  )
}
tsx

用法

import { DateRangeField } from '@hina-ui/react'
ts

日期范围输入框在一个输入面里放开始与结束两组日期段,中间以「至」分隔。每段只接受数字,输入完一段后自动进入下一段,填完开始日期后直接进入结束日期。value / onValueChange 绑定 { start, end } 对象,两个字段都是 ISO 8601 字符串,格式与 DateField 相同;只填了一侧时另一侧为 null,两侧都为空时整个值为 null。未声明的属性都会传给包裹各段的组元素,请用 aria-label 或者 aria-labelledby 命名。

年
月
日
年
月
日

值:空

'use client'

import { useState } from 'react'
import { DateRangeField, Stack, Text, type DateRangeValue } from '@hina-ui/react'

export default function Demo() {
  const [range, setRange] = useState<DateRangeValue | null>(null)
  return (
    <Stack gap="sm" align="start">
      <DateRangeField
        value={range}
        onValueChange={setRange}
        aria-label="发售期间"
        className="w-96"
      />
      <Text tone="muted" size="sm">
        值:{range ? `${range.start ?? '空'} 至 ${range.end ?? '空'}` : '空'}
      </Text>
    </Stack>
  )
}
tsx

示例

日期与时间

granularity 决定精度,两侧同时生效:默认 day 只有日期,hour 增加小时,minute 增加小时与分钟,second 再增加秒。hourCycle 指定 12 小时制或者 24 小时制,默认随语言决定。

2026
9
4
19
00
2026
9
4
21
30

值:2026-09-04T19:00 至 2026-09-04T21:30

'use client'

import { useState } from 'react'
import { DateRangeField, Stack, Text, type DateRangeValue } from '@hina-ui/react'

export default function Demo() {
  const [range, setRange] = useState<DateRangeValue | null>({
    start: '2026-09-04T19:00',
    end: '2026-09-04T21:30',
  })
  return (
    <Stack gap="sm" align="start">
      <DateRangeField
        value={range}
        onValueChange={setRange}
        granularity="minute"
        aria-label="直播时段"
        className="w-[30rem]"
      />
      <Text tone="muted" size="sm">
        值:{range ? `${range.start ?? '空'} 至 ${range.end ?? '空'}` : '空'}
      </Text>
    </Stack>
  )
}
tsx

范围

min 与 max 限定两侧可选的范围。任一侧超出范围,或者结束日期早于开始日期时,输入框显示为校验未通过,但值仍然会更新。placeholder 指定没有值时各段从哪个日期开始增减,默认是今天。

2026
9
20
2026
9
10
'use client'

import { useState } from 'react'
import { DateRangeField, type DateRangeValue } from '@hina-ui/react'

export default function Demo() {
  const [range, setRange] = useState<DateRangeValue | null>({
    start: '2026-09-20',
    end: '2026-09-10',
  })
  return (
    <DateRangeField
      value={range}
      onValueChange={setRange}
      min="2026-09-01"
      max="2026-09-30"
      placeholder="2026-09-01"
      aria-label="活动期间"
      className="w-96"
    />
  )
}
tsx

可清除

clearable 在末尾加一个清除按钮,一次清空两侧,清空后值为 null,焦点回到第一段。

2026
9
1
2026
9
30
'use client'

import { useState } from 'react'
import { DateRangeField, type DateRangeValue } from '@hina-ui/react'

export default function Demo() {
  const [range, setRange] = useState<DateRangeValue | null>({
    start: '2026-09-01',
    end: '2026-09-30',
  })
  return (
    <DateRangeField
      value={range}
      onValueChange={setRange}
      clearable
      aria-label="活动期间"
      className="w-96"
    />
  )
}
tsx

尺寸

size 有 sm、md、lg 三档,与同档输入框等高。

2026
9
1
2026
9
30
2026
9
1
2026
9
30
2026
9
1
2026
9
30
import { DateRangeField, Stack } from '@hina-ui/react'

export default function Demo() {
  const range = { start: '2026-09-01', end: '2026-09-30' }
  return (
    <Stack className="w-96">
      <DateRangeField size="sm" value={range} aria-label="小号" />
      <DateRangeField size="md" value={range} aria-label="中号" />
      <DateRangeField size="lg" value={range} aria-label="大号" />
    </Stack>
  )
}
tsx

状态

invalid 给输入面加上警示色,disabled 禁用整组,readOnly 只读,仍然可以聚焦与选中。variant="secondary" 是放在 surface 之内的扁平形态。

2026
9
1
2026
9
30
2026
9
1
2026
9
30
2026
9
1
2026
9
30
2026
9
1
2026
9
30
import { DateRangeField, Stack } from '@hina-ui/react'

export default function Demo() {
  const range = { start: '2026-09-01', end: '2026-09-30' }
  return (
    <Stack className="w-96">
      <DateRangeField value={range} invalid aria-label="校验未通过" />
      <DateRangeField value={range} disabled aria-label="已禁用" />
      <DateRangeField value={range} readonly aria-label="只读" />
      <DateRangeField value={range} variant="secondary" aria-label="扁平形态" />
    </Stack>
  )
}
tsx

在表单中

放进 FormField 后,标签通过 aria-labelledby 关联到整个区间输入,错误信息由字段渲染;校验规则与提交交给 Form。区间的值是一个对象,只填了一端时另一端是 null,校验完整性写在对象层,错误才会落到这个字段上。

年
月
日
年
月
日
'use client'

import { useState } from 'react'
import * as v from 'valibot'
import { Button, DateRangeField, Form, FormField, Text, type DateRangeValue } from '@hina-ui/react'

const schema = v.object({
  period: v.pipe(
    v.nullable(v.object({ start: v.nullable(v.string()), end: v.nullable(v.string()) })),
    v.check(value => !!value?.start && !!value?.end, '请填写完整的活动期间'),
  ),
})

export default function Demo() {
  const [values, setValues] = useState<{ period: DateRangeValue | null }>({ period: null })
  const [saved, setSaved] = useState('')

  async function save(data: unknown) {
    await new Promise(resolve => setTimeout(resolve, 600))
    setSaved(JSON.stringify(data))
  }

  return (
    <Form values={values} rules={schema} className="w-80" onSubmit={save}>
      {({ submitting }) => (
        <>
          <FormField name="period" label="活动期间" required>
            <DateRangeField value={values.period} onValueChange={period => setValues({ period })} />
          </FormField>
          <Button type="submit" loading={submitting} className="self-start">
            保存
          </Button>
          {saved && (
            <Text tone="muted" size="sm">
              已保存:{saved}
            </Text>
          )}
        </>
      )}
    </Form>
  )
}
tsx

行为

  • 点击任意一段即可输入;点击输入面的空白处时,焦点落到第一个空段,开始日期填满时落到结束日期的第一段。
  • 每段只接受数字,输入完一段后自动进入下一段,开始日期的最后一段填完后进入结束日期;左右方向键在段之间移动,上下方向键增减,退格清空当前段。
  • 一侧的所有段都填满时才会更新该侧的值;清空任意一段后,该侧变为 null。
  • 结束早于开始,或者任一侧超出 min 与 max 时,值仍然会更新,同时标为校验未通过。

无障碍

  • 各段是 role="spinbutton",名称带「开始日期」或者「结束日期」前缀,分隔文字与分隔符对辅助技术隐藏。
  • 通过 aria-label 或者 aria-labelledby 为整组命名。
  • invalid、结束早于开始与超出范围都会在组元素上设置 aria-invalid。

API

Props

属性
类型
默认值
说明
value
DateRangeValue | null
null
{ start, end },两侧都是 ISO 8601 字符串
placeholder
string
今天
空值时各段的起点,ISO 8601 字符串
min
string
—
可选范围的下限
max
string
—
可选范围的上限
granularity
'day' | 'hour' | 'minute' | 'second'
'day'
精度
hourCycle
12 | 24
随语言
小时制
clearable
boolean
false
是否显示清除按钮
readOnly
boolean
false
是否只读
name
string
—
表单字段名
variant
'primary' | 'secondary'
'primary'
形态
size
'sm' | 'md' | 'lg'
'md'
尺寸
disabled
boolean
false
是否禁用
invalid
boolean
false
是否处于校验未通过状态
className
string
—
追加至根元素的类名

DateRangeValue 是 { start: string | null; end: string | null },可以从包入口导入。

内容属性

属性
说明
leading
前置附属格的内容
trailing
后置附属格的内容

回调

回调
参数
说明
onValueChange
value: DateRangeValue | null
值变化
onClear
—
点击清除