import { useRef, useState } from "react";
import { InputGroup, Loader } from "@cloudflare/kumo";
import { CheckCircleIcon } from "@phosphor-icons/react";
export function InputGroupDemo() {
const [status, setStatus] = useState<"idle" | "loading" | "success">(
"success",
);
const [value, setValue] = useState("kumo");
const timerRef = useRef<ReturnType<typeof setTimeout>>(null);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const next = e.target.value;
setValue(next);
if (timerRef.current) clearTimeout(timerRef.current);
if (next.length > 0) {
setStatus("loading");
timerRef.current = setTimeout(() => setStatus("success"), 1500);
} else {
setStatus("idle");
}
};
return (
<div className="w-full max-w-2xs">
<InputGroup>
<InputGroup.Input
maxLength={20}
onChange={handleChange}
value={value}
/>
<InputGroup.Suffix>.workers.dev</InputGroup.Suffix>
{status !== "idle" && (
<InputGroup.Addon align="end">
{status === "loading" ? (
<Loader />
) : (
<CheckCircleIcon weight="duotone" className="text-kumo-success" />
)}
</InputGroup.Addon>
)}
</InputGroup>
</div>
);
}安装
批量导入
import { InputGroup } from "@cloudflare/kumo";细粒度导入
import { InputGroup } from "@cloudflare/kumo/components/input-group";用法
使用内置 Field(推荐)
向 InputGroup 传入 label 属性即可启用内置的 Field 包装,获得标签、说明与错误提示支持。
import { InputGroup } from "@cloudflare/kumo";
import { MagnifyingGlassIcon } from "@phosphor-icons/react";
export default function Example() {
return (
<InputGroup label="Search" description="Find pages, components, and more">
<InputGroup.Addon>
<MagnifyingGlassIcon />
</InputGroup.Addon>
<InputGroup.Input placeholder="Search..." />
</InputGroup>
);
}裸 InputGroup(自定义布局)
自定义表单布局时,可不使用 label 直接使用 InputGroup。必须在 InputGroup.Input
上提供 aria-label 以保证无障碍。
import { InputGroup } from "@cloudflare/kumo";
import { MagnifyingGlassIcon } from "@phosphor-icons/react";
export default function Example() {
return (
<InputGroup>
<InputGroup.Addon>
<MagnifyingGlassIcon />
</InputGroup.Addon>
<InputGroup.Input placeholder="Search..." aria-label="Search" />
</InputGroup>
);
}示例
图标
使用 Addon 在输入框开头放置图标,作为视觉标识。
import { InputGroup } from "@cloudflare/kumo";
import { LinkIcon } from "@phosphor-icons/react";
export function InputGroupIconsDemo() {
return (
<InputGroup className="w-full max-w-3xs">
<InputGroup.Addon>
<LinkIcon />
</InputGroup.Addon>
<InputGroup.Input placeholder="Paste a link..." aria-label="Link" />
</InputGroup>
);
}文本
使用 Addon 在输入框旁放置文本前缀或后缀。
import { InputGroup } from "@cloudflare/kumo";
export function InputGroupTextDemo() {
return (
<div className="flex flex-col gap-4">
<InputGroup className="w-full max-w-3xs">
<InputGroup.Addon>@</InputGroup.Addon>
<InputGroup.Input placeholder="username" aria-label="Username" />
</InputGroup>
<InputGroup className="w-full max-w-3xs">
<InputGroup.Input placeholder="email" aria-label="Email" />
<InputGroup.Addon align="end">@example.com</InputGroup.Addon>
</InputGroup>
<InputGroup className="w-full max-w-3xs">
<InputGroup.Addon>/api/</InputGroup.Addon>
<InputGroup.Input placeholder="endpoint" aria-label="API path" />
<InputGroup.Addon align="end">.json</InputGroup.Addon>
</InputGroup>
</div>
);
}按钮
将 InputGroup.Button 放在 Addon 内,用于直接操作输入值的动作,
如显示/隐藏、清除等。
import { useState } from "react";
import { InputGroup } from "@cloudflare/kumo";
import { MagnifyingGlassIcon, EyeIcon, EyeSlashIcon, XIcon } from "@phosphor-icons/react";
export function InputGroupButtonsDemo() {
const [show, setShow] = useState(false);
const [searchValue, setSearchValue] = useState("search");
return (
<div className="flex flex-col gap-4">
<InputGroup className="w-full max-w-3xs">
<InputGroup.Input
type={show ? "text" : "password"}
defaultValue="password"
aria-label="Password"
/>
<InputGroup.Addon align="end">
<InputGroup.Button
shape="square"
className="text-kumo-subtle"
icon={show ? EyeSlashIcon : EyeIcon}
aria-label={show ? "Hide password" : "Show password"}
onClick={() => setShow(!show)}
/>
</InputGroup.Addon>
</InputGroup>
<InputGroup className="w-full max-w-3xs">
<InputGroup.Addon>
<MagnifyingGlassIcon />
</InputGroup.Addon>
<InputGroup.Input
value={searchValue}
placeholder="Search"
aria-label="Search"
onChange={(e) => setSearchValue(e.target.value)}
/>
{searchValue && (
<InputGroup.Addon align="end" className="pr-1">
<InputGroup.Button
shape="square"
icon={XIcon}
aria-label="Clear search"
onClick={() => setSearchValue("")}
/>
</InputGroup.Addon>
)}
<InputGroup.Button variant="secondary" onClick={() => {}}>
Search
</InputGroup.Button>
</InputGroup>
</div>
);
}带提示框的按钮
向 InputGroup.Button 传入 tooltip 属性,可在悬停时显示提示框。若未显式提供
aria-label,按钮会从字符串形式的 tooltip 值中推导出它。
import { InputGroup } from "@cloudflare/kumo";
import { MagnifyingGlassIcon, QuestionIcon } from "@phosphor-icons/react";
export function InputGroupTooltipButtonDemo() {
return (
<InputGroup className="w-full max-w-2xs">
<InputGroup.Addon>
<MagnifyingGlassIcon />
</InputGroup.Addon>
<InputGroup.Input
placeholder="Search with query language..."
aria-label="Search"
/>
<InputGroup.Addon align="end">
<InputGroup.Button
shape="square"
className="text-kumo-subtle"
icon={QuestionIcon}
aria-label="Query language help"
tooltip="Query language help"
onClick={() => {}}
/>
</InputGroup.Addon>
</InputGroup>
);
}Kbd
在末尾的 Addon 中放置键盘快捷键提示。
import { InputGroup } from "@cloudflare/kumo";
import { MagnifyingGlassIcon } from "@phosphor-icons/react";
export function InputGroupKbdDemo() {
return (
<InputGroup className="w-full max-w-3xs">
<InputGroup.Addon>
<MagnifyingGlassIcon />
</InputGroup.Addon>
<InputGroup.Input placeholder="Search..." aria-label="Search" />
<InputGroup.Addon align="end">
<kbd className="border-none! bg-none!">⌘K</kbd>
</InputGroup.Addon>
</InputGroup>
);
}加载中
在末尾的 Addon 中放置 Loader,作为校验输入值时的状态指示。
import { InputGroup, Loader } from "@cloudflare/kumo";
export function InputGroupLoadingDemo() {
return (
<InputGroup className="w-full max-w-3xs">
<InputGroup.Input defaultValue="kumo" aria-label="kumo" />
<InputGroup.Addon align="end">
<Loader />
</InputGroup.Addon>
</InputGroup>
);
}行内后缀
Suffix 渲染的文本与已输入的值无缝衔接——适用于 .workers.dev
这类域名输入。可搭配显示状态图标的 Addon 来展示校验状态。
import { InputGroup } from "@cloudflare/kumo";
import { CheckCircleIcon, XCircleIcon } from "@phosphor-icons/react";
export function InputGroupSuffixDemo() {
return (
<div className="flex w-full max-w-2xs flex-col gap-4">
<InputGroup label="Subdomain">
<InputGroup.Input
aria-label="Subdomain"
defaultValue="kumo"
maxLength={20}
/>
<InputGroup.Suffix>.workers.dev</InputGroup.Suffix>
<InputGroup.Addon align="end">
<CheckCircleIcon weight="duotone" className="text-kumo-success" />
</InputGroup.Addon>
</InputGroup>
<InputGroup
label="Subdomain"
error={{ message: "This subdomain is unavailable", match: true }}
>
<InputGroup.Input
aria-label="Subdomain"
defaultValue="kumo"
maxLength={20}
/>
<InputGroup.Suffix>.workers.dev</InputGroup.Suffix>
<InputGroup.Addon align="end">
<XCircleIcon weight="duotone" className="text-kumo-danger" />
</InputGroup.Addon>
</InputGroup>
</div>
);
}尺寸
四种尺寸:xs、sm、base(默认) 和 lg。尺寸作用于整个组。
import { InputGroup } from "@cloudflare/kumo";
import { MagnifyingGlassIcon, QuestionIcon } from "@phosphor-icons/react";
export function InputGroupSizesDemo() {
return (
<div className="flex w-full max-w-3xs flex-col gap-4">
<InputGroup size="xs" label="Extra Small">
<InputGroup.Addon>
<MagnifyingGlassIcon />
</InputGroup.Addon>
<InputGroup.Input placeholder="Extra small input" />
<InputGroup.Addon align="end">
<InputGroup.Button
className="text-kumo-subtle"
icon={QuestionIcon}
shape="square"
aria-label="Help"
/>
</InputGroup.Addon>
</InputGroup>
<InputGroup size="sm" label="Small">
<InputGroup.Addon>
<MagnifyingGlassIcon />
</InputGroup.Addon>
<InputGroup.Input placeholder="Small input" />
<InputGroup.Addon align="end">
<InputGroup.Button
className="text-kumo-subtle"
icon={QuestionIcon}
shape="square"
aria-label="Help"
/>
</InputGroup.Addon>
</InputGroup>
<InputGroup label="Base (default)">
<InputGroup.Addon>
<MagnifyingGlassIcon />
</InputGroup.Addon>
<InputGroup.Input placeholder="Base input" />
<InputGroup.Addon align="end">
<InputGroup.Button
className="text-kumo-subtle"
icon={QuestionIcon}
shape="square"
aria-label="Help"
/>
</InputGroup.Addon>
</InputGroup>
<InputGroup size="lg" label="Large">
<InputGroup.Addon>
<MagnifyingGlassIcon />
</InputGroup.Addon>
<InputGroup.Input placeholder="Large input" />
<InputGroup.Addon align="end">
<InputGroup.Button
className="text-kumo-subtle"
icon={QuestionIcon}
shape="square"
aria-label="Help"
/>
</InputGroup.Addon>
</InputGroup>
</div>
);
}状态
各种输入状态,包括错误、禁用和带说明。将 label、error 和 description
属性直接传给 InputGroup 即可。
Must be at least 8 characters
import { useState } from "react";
import { InputGroup } from "@cloudflare/kumo";
import { MagnifyingGlassIcon, EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
export function InputGroupStatesDemo() {
const [show, setShow] = useState(false);
return (
<div className="flex w-full max-w-3xs flex-col gap-4">
<InputGroup
label="Error State"
error={{ message: "Please enter a valid email address", match: true }}
>
<InputGroup.Input type="email" defaultValue="invalid-email" />
<InputGroup.Addon align="end">@example.com</InputGroup.Addon>
</InputGroup>
<InputGroup label="Disabled" disabled>
<InputGroup.Addon>
<MagnifyingGlassIcon />
</InputGroup.Addon>
<InputGroup.Input placeholder="Search..." />
</InputGroup>
<InputGroup label="Optional Field" required={false}>
<InputGroup.Addon>$</InputGroup.Addon>
<InputGroup.Input placeholder="0.00" />
</InputGroup>
<InputGroup
label="With Description"
description="Must be at least 8 characters"
labelTooltip="Your password is stored securely"
>
<InputGroup.Input
type={show ? "text" : "password"}
placeholder="Password"
/>
<InputGroup.Addon align="end">
<InputGroup.Button
shape="square"
className="text-kumo-subtle"
icon={show ? EyeSlashIcon : EyeIcon}
aria-label={show ? "Hide password" : "Show password"}
onClick={() => setShow(!show)}
/>
</InputGroup.Addon>
</InputGroup>
</div>
);
}API 参考
InputGroup
根容器,为所有子组件提供上下文。接受 Field 属性 (label、description、error),
并在提供 label 时将内容包裹在 Field 中。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| label | ReactNode | - | The label content — can be a string or any React node. |
| description | ReactNode | - | Helper text displayed below the control (hidden when `error` is present). |
| error | object | - | Validation error with a message and a browser `ValidityState` match key. |
| required | boolean | - | When explicitly `false`, shows gray "(optional)" text after the label. When `true` or `undefined`, no indicator is shown. |
| labelTooltip | ReactNode | - | Tooltip content displayed next to the label via an info icon. |
| children | ReactNode | - | - |
| className | string | - | - |
| id | string | - | - |
| lang | string | - | - |
| title | string | - | - |
| size | "xs" | "sm" | "base" | "lg" | "base" | - |
| disabled | boolean | - | - |
InputGroup.Input
文本输入元素。从 InputGroup 上下文继承 size、disabled 和 error。
接受所有标准 input 属性,与 Field 相关的属性除外 (它们由父级处理)。
| 属性 | 类型 | 默认值 |
|---|
该组件没有专属属性,接受标准 HTML 属性。
InputGroup.Addon
用于放置图标、文本或紧凑按钮的容器,位于输入框的开头或末尾。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| align | "start" | "end" | "start" | Position relative to the input. |
| className | string | - | Additional CSS classes. |
InputGroup.Button
用于次级操作 (如切换、复制、帮助) 的按钮,渲染在 Addon 内部。
传入 tooltip 属性可在悬停时显示提示框。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| tooltip | ReactNode | - | When provided, wraps the button in a Tooltip. Automatically sets aria-label from a string value. |
| tooltipSide | "top" | "right" | "bottom" | "left" | "bottom" | Preferred side for the tooltip popup. |
| variant | "primary" | "secondary" | "ghost" | "destructive" | "secondary-destructive" | "outline" | "ghost" | Button visual style. Defaults to ghost. |
| size | "xs" | "sm" | "base" | "lg" | "sm" | Button size. |
InputGroup.Suffix
与已输入的值无缝衔接的行内文本 (如 .workers.dev)。用户输入时,
输入框宽度会自动调整。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| className | string | - | Additional CSS classes. |
校验错误类型
将 error 作为对象使用时,match 属性对应 HTML5 ValidityState 的取值:
| Match | 说明 |
|---|---|
| valueMissing | 必填字段为空 |
| typeMismatch | 值与类型不匹配 (如无效的邮箱地址) |
| patternMismatch | 值与 pattern 属性不匹配 |
| tooShort | 值短于 minLength |
| tooLong | 值长于 maxLength |
| rangeUnderflow | 值小于 min |
| rangeOverflow | 值大于 max |
| true | 始终显示错误 (用于服务端校验) |