悬浮卡片体系Floating card
所有“浮在页面之上的容器”都是同一张卡片:圆角 --ok-r-5、表面 --ok-surface、1px 描边加顶部高光,四周和边缘留一圈缝,从停靠的那一侧滑入。Dialog、Drawer、Sheet、Panel 以及 AppShell 的侧栏,只是它在不同位置、不同尺寸、是否模态下的用法。
一张卡片,五个位置
下面直接用基座组件 FloatingCard。切换 placement 和 modal,再打开卡片:外观、间隙、进退场的方式都是同一套。演示用了 contained,把卡片限制在演示框里。
- center:居中,从触发点(没有触发点时从中心)0.6 放大到位。
- left / right / top / bottom:贴着对应的边滑入;手机宽度下卡片撑满整宽,只滑进三分之一的距离。
size在左右是宽度,在上下是高度。 - 五个位置共用一条进场、一条退场:进场 303ms 用 soft 弹簧落位、不过冲(
--ok-float-in-dur/--ok-float-in-ease),退场 160ms 加速走开(--ok-float-out-dur/--ok-float-out-ease),遮罩跟着同一档时长。见 动效 · 悬浮卡片。 - 没有阴影。模态时靠遮罩压暗并模糊背景,非模态时靠
--ok-line-2的描边和页面分开。
模态与非模态
- 模态(默认,Dialog / Drawer / Sheet)。带遮罩;锁住页面滚动;焦点圈在卡片里,打开时自动移进去,关闭后还给触发元素;Esc 和点遮罩关闭;
role="dialog"加aria-modal。 - 非模态(
modal={false},Panel)。没有遮罩,不锁滚动,不圈焦点,页面其余部分照常可用;焦点在卡片里时 Esc 才关闭,关闭时把焦点还回原处;默认role="complementary",需要用标题或aria-label起名字。
选哪种,看用户是不是必须先处理它:必须回应的(确认、表单)用模态;辅助性的、要和页面对照着看的(检查器、筛选面板)用非模态。dismissible={false} 可以关掉 Esc 和点遮罩,这时一定要在卡片里留一个明确的关闭或确认按钮。
四个封装
日常直接用封装好的组件:它们各有一张 sm / md / lg 尺寸表,并补上各自的结构和默认值。这里的四个都渲染在整个视口上。
- Dialog(center,模态):宽度 sm 320 / md 400 / lg 560。自带图标位、标题、说明、操作区。各种宽度下形态不变,都从触发点放大进场。
- Drawer(四边,默认模态):左右宽度 sm 280 / md 360 / lg 480,上下高度 sm 240 / md 360 / lg 520。
- Sheet(bottom,默认模态):高度跟随内容,或 sm 40% / md 60% / lg 85% / full。带抓手,可拖拽关闭;宽屏上最宽 560 并居中。
- Panel(四边,非模态):左右宽度 sm 280 / md 320 / lg 400,上下高度 sm 200 / md 280 / lg 400。支持
container加contained。 - AppShell.Sidebar / Aside(left / right,模态):宽屏时是停靠在两侧、占据布局空间的卡片;窄屏时同一份内容改由 FloatingCard 从对应一侧滑入。见 AppShell。
contained:限制在一个区域里
默认卡片渲染到 body 下、盖住整个视口。传 container(一个 position: relative 的元素)加 contained 之后,卡片渲染进这个元素,定位、遮罩都只在它里面,也不再锁页面滚动。
- 上面“五个位置”的演示就是这么做的;
AppShell的contained模式也用它把侧栏抽屉限制在骨架自己的范围里。 - 容器需要
position: relative,通常再加overflow: hidden,否则滑入滑出的过程会露到容器外面。 container是元素本身而不是 ref:用useState存回调 ref 拿到的节点,节点就绪后会触发一次重渲染。Dialog、Drawer、Sheet没有开放contained;需要限制范围时用Panel,或者直接用FloatingCard。
结构件
卡片里面用四个结构件搭:它们负责内边距、滚动和无障碍接线。
FloatingHeader / FloatingBody / FloatingFooter / FloatingGrip
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
FloatingHeader | title, description, icon, level, closable | — | 标题、说明和关闭按钮。标题和说明会自动成为卡片的 aria-labelledby / aria-describedby;在可拖拽的卡片里同时是拖拽把手。 |
FloatingBody | padded | — | 内容区:占满头尾之间的空间,超出时原生滚动。内容自己通栏(列表、表格)时把 padded 关掉。 |
FloatingFooter | align: 'start' | 'end' | 'between' | 'stretch' | — | 操作区:一排按钮,默认靠右。 |
FloatingGrip | — | — | 顶上的小横杠,也是拖拽关闭的把手。纯装饰,对读屏隐藏。Sheet 默认自带。 |
在结构件或自己的子组件里,可以用 useFloatingCard() 拿到所在卡片的 close()、placement、modal 等。
FloatingCard 属性
属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
open* | boolean | — | 是否打开。基座只有受控写法;封装组件另外支持 defaultOpen。 |
onOpenChange | (open: boolean) => void | — | 请求开合时触发(Esc、点遮罩、关闭按钮、拖拽关闭)。 |
placement | 'center' | 'left' | 'right' | 'top' | 'bottom' | 'center' | 停靠位置。 |
modal | boolean | true | 模态:遮罩、锁滚动、圈焦点。 |
dismissible | boolean | true | 允许 Esc 和点遮罩关闭。 |
size | number | string | — | 主轴尺寸:left / right / center 是宽度,top / bottom 是高度。数字按 px。永远不会超过可用空间。 |
gap | number | 12(窄屏 8) | 卡片到边缘的间隙(px)。 |
stretch | boolean | true | 贴边时是否在另一个方向上撑满;false 则按内容大小并居中。 |
role | 'dialog' | 'alertdialog' | 'complementary' | 'region' | 'dialog' | 语义角色。 |
initialFocus | RefObject<HTMLElement> | — | 模态打开时先聚焦哪个元素;不传则找带 data-autofocus 的元素,再没有就聚焦卡片本身。 |
restoreFocus | boolean | true | 关闭后把焦点还给之前的元素。 |
container | HTMLElement | null | — | 渲染到哪个容器,默认 body。 |
contained | boolean | false | 配合 container:把浮层限制在容器范围内。 |
dragToDismiss | boolean | false | 按住带 data-ok-drag-handle 的区域朝停靠的一侧拖拽关闭。只对贴边的卡片有效。 |
onExited | () => void | — | 退场动画结束、节点卸载之后。 |