Layer Dialog
@cloudflare/kumo
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.Action
            disabled={!hostname || !name}
            onClick={() => undefined}
          >
            Save hostname
          </LayerDialog.Action>
        </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.Action onClick={save}>
            Save hostname
          </LayerDialog.Action>
        </LayerDialog.Actions>
      </LayerDialog.Content>
    </LayerDialog.Root>
  );
}

组合规则

LayerDialog.Content 恰好接受一个 Title、一个 Body、一个可选的 Description 和一个可选的 Actions。它会自动选择关闭 UI:

  • 无 Actions 时:在标题栏中显示 X,且底部无操作栏。
  • 有 Actions 时:移除 X,并显示含关闭或取消及一个主操作的底部操作栏。在移动端,它们保持在滚动正文下方、以发丝分隔线隔开;在桌面端,它们保留在固定底部操作栏中。

使用方不能混用这些布局,也不能添加无关的底部控件。Actions 接受一个 Action,它可以可选地暴露相关菜单项。只读内容用 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.Action
            disabled={!hostname || !name}
            onClick={() => undefined}
          >
            Save hostname
          </LayerDialog.Action>
        </LayerDialog.Actions>
      </LayerDialog.Content>
    </LayerDialog.Root>
  );
}

原生表单

Body 和 Actions 是独立的插槽,因此原生 <form> 属于 Body 内部,而其提交按钮属于 Actions。给表单一个 ID,然后用其 HTML form 属性关联主操作。这保留了内置验证,并在没有 React 状态的情况下支持非受控表单字段。

import { useId } from "react";
import { Button, Input, LayerDialog } from "@cloudflare/kumo";

export function LayerDialogFormDemo() {
  const formId = useId();

  return (
    <LayerDialog.Root>
      <LayerDialog.Trigger
        render={(props) => <Button {...props}>Create deployment</Button>}
      />
      <LayerDialog.Content>
        <LayerDialog.Title>Create deployment</LayerDialog.Title>
        <LayerDialog.Description>
          The browser owns the form’s native validation and submission state.
        </LayerDialog.Description>
        <LayerDialog.Body>
          <form
            id={formId}
            className="flex flex-col gap-5"
            onSubmit={(event) => event.preventDefault()}
          >
            <Input
              label="Service name"
              name="serviceName"
              placeholder="production-api"
              required
            />
            <Input
              label="Compatibility date"
              name="compatibilityDate"
              required
              type="date"
            />
          </form>
        </LayerDialog.Body>
        <LayerDialog.Actions dismissLabel="Cancel">
          <LayerDialog.Action form={formId} type="submit">
            Create deployment
          </LayerDialog.Action>
        </LayerDialog.Actions>
      </LayerDialog.Content>
    </LayerDialog.Root>
  );
}

搜索与选择器

控件可以作为 Body 的第一个元素。此处一个无标签的搜索输入框直接位于标题栏下方,因此请为它提供 aria-label。正文在其内容上方留有空间,因此输入框的边框和焦点环不会被滚动区域裁剪。

import { useState } from "react";
import { Button, Input, LayerDialog, Text } from "@cloudflare/kumo";

/** Unlabeled search input as the first control in the body. */
export function LayerDialogSearchDemo() {
  const [query, setQuery] = useState("");
  const matches = WORKERS.filter((worker) =>
    worker.toLowerCase().includes(query.trim().toLowerCase()),
  );

  return (
    <LayerDialog.Root>
      <LayerDialog.Trigger
        render={(props) => <Button {...props}>Choose a Worker</Button>}
      />
      <LayerDialog.Content>
        <LayerDialog.Title>Choose a Worker</LayerDialog.Title>
        <LayerDialog.Description>
          Select the Worker that handles requests for this route.
        </LayerDialog.Description>
        <LayerDialog.Body>
          <div className="flex flex-col gap-3">
            <Input
              aria-label="Search Workers"
              onChange={(event) => setQuery(event.target.value)}
              placeholder="Search Workers"
              type="search"
              value={query}
            />
            {matches.length > 0 ? (
              <ul className="flex flex-col divide-y divide-kumo-hairline rounded-lg border border-kumo-hairline">
                {matches.map((worker) => (
                  <li key={worker} className="px-3 py-2 text-kumo-default">
                    {worker}
                  </li>
                ))}
              </ul>
            ) : (
              <Text variant="secondary">No Workers match “{query}”.</Text>
            )}
          </div>
        </LayerDialog.Body>
      </LayerDialog.Content>
    </LayerDialog.Root>
  );
}

分隔正文内容

在 LayerDialog.Body 的各个分区之间使用 LayerDialog.Separator。分隔线贯穿正文的水平内边距,横跨整个内容表面。

import { Button, LayerDialog, Text } from "@cloudflare/kumo";

export function LayerDialogSeparatorDemo() {
  return (
    <LayerDialog.Root>
      <LayerDialog.Trigger
        render={(props) => <Button {...props}>Review deployment</Button>}
      />
      <LayerDialog.Content>
        <LayerDialog.Title>Deployment summary</LayerDialog.Title>
        <LayerDialog.Description>
          Review the configuration before deploying.
        </LayerDialog.Description>
        <LayerDialog.Body>
          <Text variant="secondary">
            production-api will deploy to the global network.
          </Text>
          <LayerDialog.Separator />
          <Text variant="secondary">
            The deployment uses compatibility date 2026-10-26.
          </Text>
        </LayerDialog.Body>
        <LayerDialog.Actions>
          <LayerDialog.Action>Deploy</LayerDialog.Action>
        </LayerDialog.Actions>
      </LayerDialog.Content>
    </LayerDialog.Root>
  );
}

多个主操作

通过 LayerDialog.Action 的 menu 属性传入相关操作。它会在操作旁渲染一个标准化的拆分按钮箭头。让自动关闭按钮保持独立。例如,保存并部署 可以暴露 另存为草稿,而不会把底部操作栏变成三个同级操作。

import { Button, DropdownMenu, LayerDialog, Text } from "@cloudflare/kumo";

export function LayerDialogSplitActionDemo() {
  return (
    <div className="flex flex-wrap justify-center gap-3">
      <LayerDialog.Root>
        <LayerDialog.Trigger
          render={(props) => <Button {...props}>Edit deployment</Button>}
        />
        <LayerDialog.Content>
          <LayerDialog.Title>Save deployment</LayerDialog.Title>
          <LayerDialog.Description>
            Deploy these changes now, or keep them as a draft.
          </LayerDialog.Description>
          <LayerDialog.Body>
            <Text variant="secondary">
              The alternate save outcome is related to the primary action, so it
              lives in its split-button menu rather than as an unrelated footer
              button.
            </Text>
          </LayerDialog.Body>
          <LayerDialog.Actions>
            <LayerDialog.Action
              menu={[
                <DropdownMenu.Item key="draft">
                  Save as draft
                </DropdownMenu.Item>,
              ]}
              menuLabel="Save options"
            >
              Save and deploy
            </LayerDialog.Action>
          </LayerDialog.Actions>
        </LayerDialog.Content>
      </LayerDialog.Root>

      <LayerDialog.Alert>
        <LayerDialog.Trigger
          render={(props) => (
            <Button {...props} variant="secondary">
              Delete deployment
            </Button>
          )}
        />
        <LayerDialog.Content>
          <LayerDialog.Title>Delete deployment</LayerDialog.Title>
          <LayerDialog.Description>
            This permanently removes the deployment and cannot be undone.
          </LayerDialog.Description>
          <LayerDialog.Body>
            <Text variant="secondary">
              The destructive split button keeps every part of the action,
              including the menu trigger, visually consistent.
            </Text>
          </LayerDialog.Body>
          <LayerDialog.Actions>
            <LayerDialog.Action
              menu={[
                <DropdownMenu.Item key="delete-with-tokens">
                  Delete and revoke tokens
                </DropdownMenu.Item>,
              ]}
              menuLabel="Delete options"
              variant="destructive"
            >
              Delete deployment
            </LayerDialog.Action>
          </LayerDialog.Actions>
        </LayerDialog.Content>
      </LayerDialog.Alert>
    </div>
  );
}

取消文案

当存在 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.Action
            disabled={!email || !name}
            onClick={() => undefined}
          >
            Save changes
          </LayerDialog.Action>
        </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.Action
            disabled={confirmation !== workerName}
            onClick={() => undefined}
            variant="destructive"
          >
            Delete Worker
          </LayerDialog.Action>
        </LayerDialog.Actions>
      </LayerDialog.Content>
    </LayerDialog.Alert>
  );
}

嵌套对话框

当用户必须确认某个相关操作时,可以在父对话框的正文中嵌套一个 LayerDialog.Root 或 LayerDialog.Alert。每个对话框独立进行 portal 渲染;Base UI 会让嵌套抽屉在其父级之上保持激活,并在其关闭时将焦点返回给父级触发器。嵌套抽屉会在父级之上渲染自己的背景遮罩,因此父级保持完整但明显处于非激活状态。无需任何 z-index 覆盖。

import { Button, LayerDialog, Text } from "@cloudflare/kumo";

export function LayerDialogNestedDemo() {
  return (
    <LayerDialog.Root>
      <LayerDialog.Trigger
        render={(props) => <Button {...props}>Edit deployment</Button>}
      />
      <LayerDialog.Content>
        <LayerDialog.Title>Edit deployment</LayerDialog.Title>
        <LayerDialog.Description>
          Review the deployment settings before saving.
        </LayerDialog.Description>
        <LayerDialog.Body>
          <div className="flex flex-col gap-4">
            <Text variant="secondary">
              Opening a second dialog from this body should keep the first
              dialog beneath it and restore focus when it closes.
            </Text>
            <LayerDialog.Alert>
              <LayerDialog.Trigger
                render={(props) => (
                  <Button variant="secondary-destructive" {...props}>
                    Discard changes
                  </Button>
                )}
              />
              <LayerDialog.Content size="sm">
                <LayerDialog.Title>Discard unsaved changes?</LayerDialog.Title>
                <LayerDialog.Description>
                  Your deployment edits will be permanently lost.
                </LayerDialog.Description>
                <LayerDialog.Body>
                  <Text variant="secondary">
                    This nested alert is independently portaled and should
                    dismiss back to the edit dialog.
                  </Text>
                </LayerDialog.Body>
                <LayerDialog.Actions>
                  <LayerDialog.Action variant="destructive">
                    Discard changes
                  </LayerDialog.Action>
                </LayerDialog.Actions>
              </LayerDialog.Content>
            </LayerDialog.Alert>
          </div>
        </LayerDialog.Body>
        <LayerDialog.Actions>
          <LayerDialog.Action>Save changes</LayerDialog.Action>
        </LayerDialog.Actions>
      </LayerDialog.Content>
    </LayerDialog.Root>
  );
}

进行中的操作

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.Action
            loading={pending}
            onClick={() => {
              setPending(true);
              window.setTimeout(() => setPending(false), 1500);
            }}
          >
            Save changes
          </LayerDialog.Action>
        </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.Action>Create deployment</LayerDialog.Action>
          </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.Action>Export log</LayerDialog.Action>
          </LayerDialog.Actions>
        </LayerDialog.Content>
      </LayerDialog.Root>
    </>
  );
}

API 参考

LayerDialog.Root

控制打开状态。接受 Base UI Drawer 的所有 root 属性,包括 open、 defaultOpen、onOpenChange、modal 和 actionsRef。不渲染自身的 HTML 元素。

属性类型默认值描述
dismissDisabledbooleanfalse

在操作待处理期间,阻止通过 X、关闭、Escape、背景和滑动进行关闭。 程序化关闭永远不会被阻止。

LayerDialog.Alert

与 LayerDialog.Root 属性相同。强制 modal、阻止指针关闭、渲染 role="alertdialog",并要求使用 LayerDialog.Actions。

LayerDialog.Trigger

点击后打开对话框的按钮。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

LayerDialog.Content

将背景和弹出层放入 portal,并校验组合的合法性。

PropTypeDefaultDescription
children*ReactNode--
containerPortalContainer-

用于弹出层(Portal)的容器元素。覆盖 KumoPortalProvider 上下文。

sizeKumoLayerDialogSize-

仅桌面端宽度。移动端对话框始终为全宽。

verticalAlignKumoLayerDialogVerticalAlign-

仅桌面端定位。移动端对话框始终作为底部面板展示。

closeLabelstring-

自动 X 按钮的无障碍名称。覆盖 KumoLocaleProvider 中的 close 翻译。

LayerDialog.Title

为对话框提供无障碍标签的标题。

PropTypeDefault
children*ReactNode-

LayerDialog.Description

标题下方的可选说明文字,会成为对话框的可访问描述。

PropTypeDefault
children*ReactNode-

LayerDialog.Body

可滚动的内容区域。

PropTypeDefault
children*ReactNode-

LayerDialog.Separator

分隔正文内容各分区的全宽分隔线。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

LayerDialog.Actions

包含一个自动关闭按钮和恰好一个 LayerDialog.Action 的底部操作栏。

PropTypeDefaultDescription
children*ReactNode-

恰好一个操作。如需相关操作(例如“另存为草稿”),请将 menu 传给 LayerDialog.Action。

dismissLabelstring-

自动关闭按钮的文本。仅当工作流具有真实的取消结果时才使用“取消”。对于非英语产品,请将其翻译。

LayerDialog.Action

唯一的主操作。渲染一个 Kumo Button,并接受 onClick、disabled 和 type 等标准按钮属性。通过 menu 传入菜单项,可渲染匹配的拆分按钮菜单。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.