import { useState } from "react";
import { Button, Input, LayerDialog } from "@cloudflare/kumo";
export function LayerDialogActionDemo() {
const [name, setName] = useState("Production API");
const [hostname, setHostname] = useState("api.example.com");
return (
<LayerDialog.Root>
<LayerDialog.Trigger
render={(props) => <Button {...props}>Open settings</Button>}
/>
<LayerDialog.Content>
<LayerDialog.Title>Configure custom hostname</LayerDialog.Title>
<LayerDialog.Description>
Route requests for this hostname to your Worker.
</LayerDialog.Description>
<LayerDialog.Body>
<div className="flex flex-col gap-5">
<Input
label="Hostname"
onChange={(event) => setHostname(event.target.value)}
value={hostname}
/>
<Input
label="Display name"
onChange={(event) => setName(event.target.value)}
value={name}
/>
</div>
</LayerDialog.Body>
<LayerDialog.Actions>
<LayerDialog.Actions.Primary
disabled={!hostname || !name}
onClick={() => undefined}
>
Save hostname
</LayerDialog.Actions.Primary>
</LayerDialog.Actions>
</LayerDialog.Content>
</LayerDialog.Root>
);
}安装
桶式导入
import { LayerDialog } from "@cloudflare/kumo";子路径导入
import { LayerDialog } from "@cloudflare/kumo/components/layer-dialog";用法
import { Button, LayerDialog } from "@cloudflare/kumo";
export default function Example() {
return (
<LayerDialog.Root>
<LayerDialog.Trigger
render={(props) => <Button {...props}>Open settings</Button>}
/>
<LayerDialog.Content>
<LayerDialog.Title>Configure custom hostname</LayerDialog.Title>
<LayerDialog.Description>
Route requests for this hostname to your Worker.
</LayerDialog.Description>
<LayerDialog.Body>{/* form fields */}</LayerDialog.Body>
<LayerDialog.Actions>
<LayerDialog.Actions.Primary onClick={save}>
Save hostname
</LayerDialog.Actions.Primary>
</LayerDialog.Actions>
</LayerDialog.Content>
</LayerDialog.Root>
);
}组合规则
LayerDialog.Content 恰好接受一个 Title、一个 Body、一个可选的 Description 和一个可选的 Actions。它会自动选择关闭 UI:
- 无
Actions时:在标题栏中显示 X,且底部无操作栏。 - 有
Actions时:移除 X,并显示含关闭或取消及一个主操作的底部操作栏。
使用方不能混用这些布局,也不能添加更多主操作。只读内容用 X,用户必须提交变更时用 Actions,破坏性操作或重要确认用 LayerDialog.Alert。
Description 直接渲染在粘性标题栏内的标题下方,并成为对话框的可访问描述。没有它时,则由正文文本来描述对话框。
信息型对话框
Title 和 Description 始终位于带边框的正文表面内。正文滚动时标题栏保持可见,并提供唯一的自动 X 关闭操作。当正文滚动过顶部后,描述会折叠到标题下方,为内容腾出更多空间,滚回顶部时再展开。内容边缘遮罩用于提示溢出。
import { Button, LayerDialog, Text } from "@cloudflare/kumo";
function LongContent() {
return (
<div className="flex flex-col gap-5">
<div className="rounded-lg border border-kumo-line p-4 text-kumo-subtle">
Navigation and command shortcuts
</div>
<Text variant="secondary">
The title frame stays visible, receives a divider once content scrolls,
and the scroll mask indicates more content below.
</Text>
<div className="h-96" />
</div>
);
}
export function LayerDialogInformationalDemo() {
return (
<LayerDialog.Root>
<LayerDialog.Trigger
render={(props) => <Button {...props}>Open keyboard shortcuts</Button>}
/>
<LayerDialog.Content>
<LayerDialog.Title>Keyboard shortcuts</LayerDialog.Title>
<LayerDialog.Description>
Browse available shortcuts without changing a setting.
</LayerDialog.Description>
<LayerDialog.Body>
<LongContent />
</LayerDialog.Body>
</LayerDialog.Content>
</LayerDialog.Root>
);
}标准操作
添加 Actions 会启用固定底部操作栏,其中恰好包含一个 Kumo 主操作和一个自动生成的关闭按钮。使用方不能添加自定义底部控件、修改按钮尺寸或增加额外 CTA。主操作接受 variant="primary"(默认)或 variant="destructive"。
import { useState } from "react";
import { Button, Input, LayerDialog } from "@cloudflare/kumo";
export function LayerDialogActionDemo() {
const [name, setName] = useState("Production API");
const [hostname, setHostname] = useState("api.example.com");
return (
<LayerDialog.Root>
<LayerDialog.Trigger
render={(props) => <Button {...props}>Open settings</Button>}
/>
<LayerDialog.Content>
<LayerDialog.Title>Configure custom hostname</LayerDialog.Title>
<LayerDialog.Description>
Route requests for this hostname to your Worker.
</LayerDialog.Description>
<LayerDialog.Body>
<div className="flex flex-col gap-5">
<Input
label="Hostname"
onChange={(event) => setHostname(event.target.value)}
value={hostname}
/>
<Input
label="Display name"
onChange={(event) => setName(event.target.value)}
value={name}
/>
</div>
</LayerDialog.Body>
<LayerDialog.Actions>
<LayerDialog.Actions.Primary
disabled={!hostname || !name}
onClick={() => undefined}
>
Save hostname
</LayerDialog.Actions.Primary>
</LayerDialog.Actions>
</LayerDialog.Content>
</LayerDialog.Root>
);
}取消文案
当存在 LayerDialog.Actions 时,LayerDialog 会渲染一个用于关闭对话框的次要按钮。默认文案为“关闭”,对于 LayerDialog.Alert 则为“取消”。
仅当对话框需要更具体的文案时才使用 dismissLabel,例如“继续编辑”或“放弃更改”。该标签不改变按钮行为:它始终关闭对话框。
import { useState } from "react";
import { Button, Input, LayerDialog } from "@cloudflare/kumo";
export function LayerDialogCancelDemo() {
const [email, setEmail] = useState("alex@example.com");
const [name, setName] = useState("Alex Morgan");
return (
<LayerDialog.Root>
<LayerDialog.Trigger
render={(props) => <Button {...props}>Edit profile</Button>}
/>
<LayerDialog.Content>
<LayerDialog.Title>Edit profile</LayerDialog.Title>
<LayerDialog.Description>
Update the profile information shown to your teammates. Changes are
not saved until you confirm.
</LayerDialog.Description>
<LayerDialog.Body>
<div className="flex flex-col gap-5">
<Input
label="Display name"
onChange={(event) => setName(event.target.value)}
value={name}
/>
<Input
label="Email address"
onChange={(event) => setEmail(event.target.value)}
type="email"
value={email}
/>
</div>
</LayerDialog.Body>
<LayerDialog.Actions dismissLabel="Cancel">
<LayerDialog.Actions.Primary
disabled={!email || !name}
onClick={() => undefined}
>
Save changes
</LayerDialog.Actions.Primary>
</LayerDialog.Actions>
</LayerDialog.Content>
</LayerDialog.Root>
);
}确认与破坏性操作
LayerDialog.Alert 遵循 Base UI 的 alert dialog:提供 role="alertdialog"、自动取消按钮、无 X、始终模态,并阻止背景与滑动关闭。Escape 仍然可以取消,符合 ARIA alert dialog 模式。
当确认操作不可逆时,为主操作传入 variant="destructive"。对于确认非破坏性但关键步骤的 Alert,保留默认的主操作样式。
import { useState } from "react";
import { Button, Input, LayerDialog, Text } from "@cloudflare/kumo";
export function LayerDialogAlertDemo() {
const workerName = "example-worker";
const [confirmation, setConfirmation] = useState("");
return (
<LayerDialog.Alert>
<LayerDialog.Trigger
render={(props) => (
<Button variant="secondary-destructive" {...props}>
Delete Worker
</Button>
)}
/>
<LayerDialog.Content>
<LayerDialog.Title>Delete Worker</LayerDialog.Title>
<LayerDialog.Description>
Deleting{" "}
<strong className="font-medium text-kumo-default">
{workerName}
</strong>{" "}
is permanent.
</LayerDialog.Description>
<LayerDialog.Body>
<div className="flex flex-col gap-5">
<Text variant="secondary">
This deletes the Worker, deployments, and configuration. If this
Worker consumes Queues, those connections are removed first.
Queues, D1 databases, and messages stay in your account.
</Text>
<Input
label={
<>
Type{" "}
<strong className="font-medium text-kumo-default">
{workerName}
</strong>{" "}
to confirm
</>
}
onChange={(event) => setConfirmation(event.target.value)}
placeholder={workerName}
value={confirmation}
/>
</div>
</LayerDialog.Body>
<LayerDialog.Actions>
<LayerDialog.Actions.Primary
disabled={confirmation !== workerName}
onClick={() => undefined}
variant="destructive"
>
Delete Worker
</LayerDialog.Actions.Primary>
</LayerDialog.Actions>
</LayerDialog.Content>
</LayerDialog.Alert>
);
}进行中的操作
dismissDisabled 会在异步操作进行期间统一阻止所有用户触发的关闭操作。通过 actionsRef.current.close() 或受控 open 属性进行的程序化关闭仍然有效,因此成功的操作可以关闭对话框。唯一的主操作拥有独立的 loading 或 disabled 状态。
import { useState } from "react";
import { Button, LayerDialog, Text } from "@cloudflare/kumo";
export function LayerDialogPendingDemo() {
const [pending, setPending] = useState(false);
return (
<LayerDialog.Root dismissDisabled={pending}>
<LayerDialog.Trigger
render={(props) => <Button {...props}>Save a setting</Button>}
/>
<LayerDialog.Content>
<LayerDialog.Title>Save a setting</LayerDialog.Title>
<LayerDialog.Description>
While saving, Close, Escape, backdrop, and mobile swipe dismissals are
blocked together.
</LayerDialog.Description>
<LayerDialog.Body>
<Text variant="secondary">
Programmatic closes still work, so a successful save can dismiss the
dialog through `actionsRef` or a controlled `open` prop.
</Text>
</LayerDialog.Body>
<LayerDialog.Actions>
<LayerDialog.Actions.Primary
loading={pending}
onClick={() => {
setPending(true);
window.setTimeout(() => setPending(false), 1500);
}}
>
Save changes
</LayerDialog.Actions.Primary>
</LayerDialog.Actions>
</LayerDialog.Content>
</LayerDialog.Root>
);
}清理
将清理逻辑放在 onOpenChange 中,而不是放在关闭点击处理函数里,这样 X、关闭、Escape、背景和滑动关闭都能触发清理。
import { useState } from "react";
import { Button, LayerDialog, Text } from "@cloudflare/kumo";
export function LayerDialogCleanupDemo() {
const [open, setOpen] = useState(false);
const [cleanupCount, setCleanupCount] = useState(0);
return (
<LayerDialog.Root
open={open}
onOpenChange={(nextOpen) => {
if (!nextOpen) setCleanupCount((count) => count + 1);
setOpen(nextOpen);
}}
>
<LayerDialog.Trigger
render={(props) => <Button {...props}>Open draft</Button>}
/>
<LayerDialog.Content>
<LayerDialog.Title>Draft settings</LayerDialog.Title>
<LayerDialog.Body>
<Text variant="secondary">
Cleanup has run {cleanupCount} time{cleanupCount === 1 ? "" : "s"}.
</Text>
</LayerDialog.Body>
</LayerDialog.Content>
</LayerDialog.Root>
);
}本地化
该对话框自带两处文案。可通过 KumoLocaleProvider 配置它们的全局默认值:
layerDialog.close用于自动 X 按钮和默认底部关闭按钮。默认值:“关闭”。layerDialog.cancel用于Alert内默认底部关闭按钮。
<KumoLocaleProvider
translations={{
layerDialog: { close: "Schließen", cancel: "Abbrechen" },
}}
>
<App />
</KumoLocaleProvider>当某个对话框需要自定义文案时,可在 Content 上使用 closeLabel,或在 Actions 上使用 dismissLabel;显式属性会覆盖提供者的默认值。
桌面端宽度
桌面端对话框默认使用 size="base"(576px)。当内容需要不同宽度时,可使用 sm(448px)、lg(672px)或 xl(768px)。移动端对话框在所有尺寸下都保持全宽。
import { useState } from "react";
import { Button, Input, LayerDialog, KumoLayerDialogSize } from "@cloudflare/kumo";
export function LayerDialogSizeDemo() {
const [open, setOpen] = useState(false);
const [size, setSize] = useState<KumoLayerDialogSize>("base");
const openAtSize = (nextSize: KumoLayerDialogSize) => {
setSize(nextSize);
setOpen(true);
};
return (
<>
<div className="flex flex-wrap gap-2">
<Button onClick={() => openAtSize("sm")}>Small</Button>
<Button onClick={() => openAtSize("base")}>Default</Button>
<Button onClick={() => openAtSize("lg")}>Large</Button>
<Button onClick={() => openAtSize("xl")}>Extra large</Button>
</div>
<LayerDialog.Root open={open} onOpenChange={setOpen}>
<LayerDialog.Content size={size}>
<LayerDialog.Title>Review deployment configuration</LayerDialog.Title>
<LayerDialog.Description>
Confirm the service details and routing configuration before this
deployment is created.
</LayerDialog.Description>
<LayerDialog.Body>
<div className="grid gap-5 sm:grid-cols-2">
<Input label="Service name" defaultValue="production-api" />
<Input label="Environment" defaultValue="Production" />
<Input label="Hostname" defaultValue="api.example.com" />
<Input label="Compatibility date" defaultValue="2026-09-09" />
</div>
</LayerDialog.Body>
<LayerDialog.Actions>
<LayerDialog.Actions.Primary>
Create deployment
</LayerDialog.Actions.Primary>
</LayerDialog.Actions>
</LayerDialog.Content>
</LayerDialog.Root>
</>
);
}桌面端位置
桌面端对话框默认居中。仅当内容适合顶部对齐时,才使用 verticalAlign="top" 这一窄化覆盖项;移动端仍是底部抽屉(bottom sheet)。
import { Button, LayerDialog, Text } from "@cloudflare/kumo";
export function LayerDialogTopAlignDemo() {
return (
<LayerDialog.Root>
<LayerDialog.Trigger
render={(props) => <Button {...props}>Open top-aligned dialog</Button>}
/>
<LayerDialog.Content verticalAlign="top">
<LayerDialog.Title>Top-aligned dialog</LayerDialog.Title>
<LayerDialog.Body>
<Text variant="secondary">Mobile dialogs remain bottom sheets.</Text>
</LayerDialog.Body>
</LayerDialog.Content>
</LayerDialog.Root>
);
}最大高度
高度由内容决定,并以视口为上限。遵循 Base UI 的 inside-scroll 模式,视口为每种位置预留垂直空间,弹出层填满该空间,因此顶部对齐的对话框永远不会超出底部边缘。超过上限后,只有正文滚动,标题栏和操作栏保持固定。移动端抽屉则以视口的 85% 为上限。不存在高度属性:使用方只能通过所渲染的内容来控制高度。
import { useState } from "react";
import { Button, LayerDialog, KumoLayerDialogVerticalAlign } from "@cloudflare/kumo";
export function LayerDialogMaxHeightDemo() {
const [verticalAlign, setVerticalAlign] =
useState<KumoLayerDialogVerticalAlign>("center");
const [open, setOpen] = useState(false);
const openAt = (align: KumoLayerDialogVerticalAlign) => {
setVerticalAlign(align);
setOpen(true);
};
return (
<>
<div className="flex flex-wrap gap-2">
<Button onClick={() => openAt("center")}>Centered, tall content</Button>
<Button onClick={() => openAt("top")}>Top-aligned, tall content</Button>
</div>
<LayerDialog.Root open={open} onOpenChange={setOpen}>
<LayerDialog.Content verticalAlign={verticalAlign}>
<LayerDialog.Title>Audit log</LayerDialog.Title>
<LayerDialog.Description>
The dialog grows with its content until it reaches the viewport cap,
then only the body scrolls.
</LayerDialog.Description>
<LayerDialog.Body>
<ol className="flex flex-col gap-2">
{Array.from({ length: 40 }, (_, index) => (
<li
key={index}
className="rounded-lg border border-kumo-line px-3 py-2 text-kumo-subtle"
>
Entry {index + 1}
</li>
))}
</ol>
</LayerDialog.Body>
<LayerDialog.Actions>
<LayerDialog.Actions.Primary>
Export log
</LayerDialog.Actions.Primary>
</LayerDialog.Actions>
</LayerDialog.Content>
</LayerDialog.Root>
</>
);
}API 参考
LayerDialog.Root
控制打开状态。接受 Base UI Drawer 的所有 root 属性,包括 open、
defaultOpen、onOpenChange、modal 和 actionsRef。不渲染自身的
HTML 元素。
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| dismissDisabled | boolean | false | 在操作待处理期间,阻止通过 X、关闭、Escape、背景和滑动进行关闭。 程序化关闭永远不会被阻止。 |
LayerDialog.Alert
与 LayerDialog.Root 属性相同。强制 modal、阻止指针关闭、渲染
role="alertdialog",并要求使用 LayerDialog.Actions。
LayerDialog.Trigger
点击后打开对话框的按钮。
| Prop | Type | Default |
|---|
No component-specific props. Accepts standard HTML attributes.
LayerDialog.Content
将背景和弹出层放入 portal,并校验组合的合法性。
| Prop | Type | Default | Description |
|---|---|---|---|
| children* | ReactNode | - | - |
| container | PortalContainer | - | Container element for the portal. Overrides `KumoPortalProvider` context. |
| size | KumoLayerDialogSize | - | Desktop-only width. Mobile dialogs always remain full-width. |
| verticalAlign | KumoLayerDialogVerticalAlign | - | Desktop-only positioning. Mobile dialogs always remain bottom sheets. |
| closeLabel | string | - | Accessible name of the automatic X button. Overrides the `close` translation from KumoLocaleProvider. |
LayerDialog.Title
为对话框提供无障碍标签的标题。
| Prop | Type | Default |
|---|---|---|
| children* | ReactNode | - |
LayerDialog.Description
标题下方的可选说明文字,会成为对话框的可访问描述。
| Prop | Type | Default |
|---|---|---|
| children* | ReactNode | - |
LayerDialog.Body
可滚动的内容区域。
| Prop | Type | Default |
|---|---|---|
| children* | ReactNode | - |
LayerDialog.Actions
包含一个自动关闭按钮和恰好一个 LayerDialog.Actions.Primary 的底部操作栏。
| Prop | Type | Default | Description |
|---|---|---|---|
| children* | ReactElement<LayerDialogPrimaryProps> | - | - |
| dismissLabel | string | - | Text of the automatic dismiss button. Say "Cancel" only when the workflow has a real cancel outcome. Translate it for non-English products. |
LayerDialog.Actions.Primary
唯一的主操作。渲染一个 Kumo Button,接受 onClick、disabled、type
等标准按钮属性。
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| variant | ”primary” | “destructive" | "primary” | 操作的视觉强调程度。在确认删除等不可逆操作时使用 |
| loading | boolean | false | 操作待处理期间显示加载指示器并禁用操作。 |