为 tldraw 画布应用填充 UI 预留区:TopPanel 与 SharePanel 组件插槽定制指南
默认的 tldraw 界面在顶部预留了两块空的 UI 插槽(slot):屏幕顶部居中的 TopPanel,以及顶部右侧(样式面板上方)的 SharePanel。本指南讲解如何通过 <Tldraw> 的 components 属性注入自定义 React 组件来填充这两个插槽,适用于在编辑器上方放置文档标题、分享按钮、协作成员头像等自定义功能;读完你将掌握插槽的运行机制、完整实现代码与源码级的布局细节。
两个预留插槽:TopPanel 与 SharePanel
本主题的配套示例文档位于 README.md,其核心说明只有一句话:
默认 UI 为你留下了两个空插槽:屏幕顶部居中的
TopPanel,以及顶部右侧、样式面板上方的SharePanel。通过components属性设置其中任意一个,即可在那里渲染你自己的 React 组件。tldraw.com 正是用它们来承载文档标题与分享按钮。
这两处插槽的渲染位置可以从布局源码中得到印证。在 TldrawUi.tsx 中,顶部区域被划分为左、中、右三个区域:
tlui-layout__top__left:渲染MenuPanel(主菜单)与HelperButtons(辅助按钮);tlui-layout__top__center:居中插槽TopPanel;tlui-layout__top__right:右侧插槽SharePanel,其后才是样式面板StylePanel(在非只读、且断点达到平板尺寸以上时显示)。
也就是说,SharePanel 之所以"位于样式面板上方",是因为它在 DOM 中先于 StylePanel 渲染,且两者共用右侧顶部栏。
默认值需要澄清的一个细节
示例文档说两个插槽默认都是空的,但从源码看两者默认行为略有差异。在 components.tsx 中,组件上下文的默认值如下:
SharePanel: showCollaborationUi ? DefaultSharePanel : null,
CursorChatBubble: showCollaborationUi ? CursorChatBubble : null,
TopPanel: null,
即:
TopPanel默认就是null,永远是空的,完全留给你填充;SharePanel默认并不总是空的:当协作 UI 处于开启状态(showCollaborationUi为真,例如接入了 tldraw sync 等协作环境)时,它渲染的是 DefaultSharePanel,内部再挂载PeopleMenu(协作成员列表/头像);在没有协作环境时它才是null。
示例文档将其描述为"空插槽",是站在"最终用户需要自行注入组件、注入后即覆盖默认实现"的使用视角而言的。如果你想完全接管右侧区域,把 SharePanel 换成自己的组件即可——它和 TopPanel 一样,都是 components 属性机制下普通的可替换插槽。
用 components 属性填充插槽
第一步:编写两个自定义组件
参考示例代码 ZonesExample.tsx,组件就是普通的 React 组件,没有特殊 props 约束:
// [1] 渲染在顶部居中区域
function CustomTopPanel() {
return (
<div
style={{
backgroundColor: 'thistle',
width: '100%',
textAlign: 'center',
padding: '2px',
minWidth: '80px',
}}
>
<p>Top panel</p>
</div>
)
}
// [2] 渲染在顶部右侧、样式面板上方
function CustomSharePanel() {
return (
<div
style={{
backgroundColor: 'thistle',
width: '100%',
textAlign: 'center',
minWidth: '80px',
}}
>
<p>Share panel</p>
</div>
)
}
第二步:组装 TLComponents 并传入
TLComponents 是所有可替换组件的联合接口,它同时继承自编辑器的 TLEditorComponents 与 UI 层的 TLUiComponents(见 Tldraw.tsx)。把两个自定义组件挂到对应的插槽键上:
import { TLComponents, Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
const components: TLComponents = {
SharePanel: CustomSharePanel,
TopPanel: CustomTopPanel,
}
export default function ZonesExample() {
return (
<div className="tldraw__editor">
<Tldraw components={components} />
</div>
)
}
示例源码的文件末尾还保留了一段注释,明确说明两个插槽的业务用途定位:
[1]渲染在顶部居中。tldraw.com 用这个插槽显示文档标题。[2]渲染在顶部右侧。tldraw.com 用这个插槽放分享按钮和协作者头像。
这正是这两个插槽最典型的落地方案:居中放文档名/文件名,右侧放与分享、协作相关的操作入口。
第三步:注意 components 对象的引用稳定性
在 Tldraw.tsx 中,components 属性被显式标注了两条约束:
重要:必须用
useMemo记忆化,或定义在任何 React 组件之外。
原因在于 UI 层的 TldrawUiComponentsProvider 通过 useShallowObjectIdentity 对 overrides 做浅层引用比较(见 components.tsx),每次渲染若传入新对象,就会引发上下文值重建与重渲染。因此,要么把 components 定义在模块顶层(如上例),要么在组件内部用 useMemo 包裹:
const components = useMemo<TLComponents>(
() => ({ TopPanel: CustomTopPanel, SharePanel: CustomSharePanel }),
[]
)
return <Tldraw components={components} />
直接禁用插槽与子组件级定制
设为 null 即可彻底隐藏
TLUiComponents 中每个字段的类型都是 ComponentType<...> | null(见 components.tsx)。Tldraw 组件在渲染时会先判断"组件是否存在"再挂载,例如 {TopPanel && <TopPanel />},因此把某个插槽设为 null 即可完全禁用该区域:
const components: TLComponents = {
TopPanel: null, // 即使默认不渲染,显式置空也可行
SharePanel: null, // 隐藏默认的协作成员面板
}
TldrawBaseProps 的文档注释也印证了这一点:"用 null 覆盖组件即可彻底禁用它们。"
SharePanel 背后还有更细粒度的协作插槽
SharePanel 是一个"容器级"插槽,而容器内部还有可单独替换的子插槽。从 components.tsx 可以看到与协作/分享相关的其余键:
PeopleMenu、PeopleMenuAvatar、PeopleMenuItem、PeopleMenuFacePile、UserPresenceEditor等。
默认的 DefaultSharePanel 内部只是读取 PeopleMenu 并把它包在 .tlui-share-zone 容器中(见 DefaultSharePanel.tsx):
export function DefaultSharePanel() {
const { PeopleMenu } = useTldrawUiComponents()
if (!PeopleMenu) return null
return (
<div className="tlui-share-zone" draggable={false}>
<PeopleMenu />
</div>
)
}
因此有两种定制粒度:
- 整体替换
SharePanel:完全不使用默认实现,适合放"分享到社交平台""邀请链接复制"等自定义按钮; - 保留
DefaultSharePanel,只替换其子组件(如自定义PeopleMenuAvatar改变在线头像样式),默认容器与面板逻辑仍然生效。
进阶:让内容在顶部区域智能居中
TopPanel 插槽自身并不会做自动居中——它在布局中位于 .tlui-layout__top__center 容器内。如果你的自定义内容希望在左右 UI(如左侧主菜单、右侧样式面板)之间动态居中、且避免遮挡,仓库还提供了现成的辅助容器 CenteredTopPanelContainer(见 CenteredTopPanelContainer.tsx)。
该组件通过 ResizeObserver 监听顶栏左、右区域与自身宽度,动态计算 translate 偏移并限制 max-width,从而实现"在可用宽度内居中、空间不足时左对齐避让"的效果。它暴露的可调参数包括:
| 参数 | 默认值 | 作用 |
|---|---|---|
maxWidth |
420 |
顶栏内容允许的最大宽度 |
ignoreRightWidth |
0 |
计算自宽时忽略的宽度(如按钮溢出部分) |
stylePanelWidth |
148 |
判定右侧样式面板宽度的阈值 |
marginBetweenZones |
12 |
与左右区域之间保持的最小间距 |
squeezeAmount |
52 |
小屏(断点 ≤ 6)且右侧过宽时额外收缩的像素 |
示例中直接在 TopPanel 里用 textAlign: 'center' + width: '100%' 即可满足简单场景;而当你的顶部内容是文档标题这类需要"见缝插针"的 UI 时,CenteredTopPanelContainer 是保持体验稳定的推荐包装。
断点与响应式行为提示
TopPanel 与 SharePanel 都位于顶部栏,顶部栏在小屏/竖屏下仍然渲染,但需要注意:
- 右侧区域中,
StylePanel只有在断点达到PORTRAIT_BREAKPOINT.TABLET_SM及以上且非只读时才渲染(见 TldrawUi.tsx)。因此窄屏下你的SharePanel内容会有更多可用空间,但布局代码应做好宽度自适应; - 顶栏容器带有
data-breakpoint等响应式数据属性,若需要精确响应断点,可在组件内使用useBreakpoint,也可以观察CenteredTopPanelContainer的写法作为响应式布局参考。
小结
TopPanel 与 SharePanel 是 tldraw 为自定义宿主应用预留的"官方空白区":
TopPanel位于屏幕顶部居中,默认恒为空,适合放文档标题等居中内容;SharePanel位于顶部右侧、样式面板上方,默认仅在协作 UI 下渲染DefaultSharePanel,适合放分享按钮与协作者头像,也可整体替换或置null禁用;- 两者均通过
<Tldraw components={...}>注入,组件对象需用模块顶层常量或useMemo保证引用稳定; - 需要精细居中时可借助 CenteredTopPanelContainer.tsx,需要拆解协作面板时可单独覆盖
PeopleMenu系列子插槽。
完整的可运行示例见 ZonesExample.tsx,插槽的类型定义见 components.tsx,渲染布局见 TldrawUi.tsx,三处配合阅读即可彻底掌握该机制。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00