Remotion Studio 无指针交互规范:hoverable 类、CSS 悬停与键盘焦点的完整实践
【免费下载链接】remotion🎥 Make videos programmatically with React项目地址: https://gitcode.com/GitHub_Trending/re/remotion
本文基于 Remotion 仓库中的.agents/skills/no-pointer/SKILL.md技能文档,系统讲解 Remotion Studio 内部统一的"无指针(pointerless)"交互规范:可点击控件一律使用cursor: default,悬停效果全部由 CSS:hover驱动而非 React 状态,且仅对键盘导航显示焦点装饰。读完本文,你将掌握如何在 Studio 代码中正确使用HOVERABLE_CLASS_NAME、hoverableStyle()、FOCUS_VISIBLE_ONLY_CLASS_NAME这套工具,理解其背后的 CSS 变量机制、选择器优先级技巧与 iframe 环境下的 hover 可靠性问题,并能按照文档给出的标准完成逐项验证。
规范速览:四条核心约定
该技能文档面向"新增或修改 Studio 中按钮、链接等可点击控件"的场景,给出四条硬性约定:
- 光标:设置
cursor: 'default',不要使用cursor: 'pointer'。 - 悬停:纯视觉的悬停样式必须通过 hoverable.ts 中的
HOVERABLE_CLASS_NAME和hoverableStyle()驱动,禁止为颜色或背景变化引入 React hover 状态。 - 背景:除非周围组件已经在使用共享的"背景悬停"约定,否则保持背景不变;纯文字操作(text-only action)的
idleBackground与hoverBackground都应使用TRANSPARENT。 - 文字颜色:空闲态使用
LIGHT_TEXT(#A6A7A9),悬停态使用WHITE(white);图标与嵌套文本必须跟随同一状态。如果 Studio 的 CSS reset 阻断了颜色继承,需要检查真正绘制像素的那个元素。
为什么禁用cursor: pointer与 React hover 状态
规范的第一条看似反直觉——Web 上可点击元素通常都显示手型光标。但 Studio 选择保持default光标,是为了让界面视觉更克制、避免"处处像链接"的噪声。这一点在源码中得到印证:通用 Button 组件 的基础样式中直接写死了cursor: 'default'(见 Button.tsx#L14-L24),侧边栏条目 CompositionSelectorItem 的itemStyle同样是cursor: 'default'(见 CompositionSelectorItem.tsx#L63-L82)。
禁用 React hover 状态的原因则更技术性:hoverable.ts 文件顶部的注释明确指出——当 Studio 运行在<iframe>中(如 Browser Studio 场景)时,浏览器在指针离开 iframe 边界时可能丢失pointerleave事件,导致基于状态驱动的悬停背景永久卡住;而 CSS:hover由浏览器自身维护,具备自我纠正能力(见 hoverable.ts#L4-L9)。因此"视觉归 CSS,行为归 JS"是这条规范的底层逻辑。
hoverable 机制源码解析
类名常量与 CSS 变量
hoverable.ts 导出四个类名常量:
HOVERABLE_CLASS_NAME:'__remotion-hoverable',标记一个可悬停元素;HOVER_GROUP_CLASS_NAME:'__remotion-hover-group',悬停组的外层;HOVER_GROUP_REVEAL_CLASS_NAME:'__remotion-hover-group-reveal',悬停组内"悬停才显现"的内容;FOCUS_VISIBLE_ONLY_CLASS_NAME:'__remotion-focus-visible-only',焦点仅在键盘导航时可见。
hoverableStyle()接收idleBackground、hoverBackground、idleColor、hoverColor四个字符串参数,返回一个 React 内联样式对象,把它们写入四个 CSS 自定义属性(见 hoverable.ts#L27-L44):
| CSS 变量 | 来源参数 | 作用 |
|---|---|---|
--remotion-hoverable-bg | idleBackground | 空闲态背景色 |
--remotion-hoverable-hover-bg | hoverBackground | 悬停态背景色 |
--remotion-hoverable-color | idleColor | 空闲态文字色 |
--remotion-hoverable-hover-color | hoverColor | 悬停态文字色 |
文件还导出NO_HOVER_BACKGROUND_STYLE(仅设置悬停背景变量为TRANSPARENT),以及一个实用技巧:如需禁用某个控件的悬停效果(例如禁用态),把空闲值同时传给 hover 参数即可,源码注释对此有明确说明(见 hoverable.ts#L21-L26)。
注入的全局 CSS 及其优先级技巧
makeHoverableCSS()(hoverable.ts#L56-L92)生成的规则由 inject-css.ts 在全局样式表末尾统一注入(见 inject-css.ts#L168),核心规则如下:
.__remotion-hoverable.__remotion-hoverable { background-color: var(--remotion-hoverable-bg, transparent); } .__remotion-hoverable.__remotion-hoverable, .__remotion-hoverable.__remotion-hoverable * { color: var(--remotion-hoverable-color, inherit); }其中包含三个值得注意的设计:
- 双写类名提升特异性:选择器写作
.x.x是为了压过.css-reset, .css-reset *这类"对每个后代都强制设置color和background"的 reset 规则——两份样式表的注入顺序并不可控,靠特异性而非注入顺序保证生效(见 hoverable.ts#L46-L54 的注释)。 *后代选择器重建继承:color变量同时应用到元素本身及其所有后代,使嵌套文本与基于currentColor的图标自动跟随悬停状态。这正是 SKILL.md 要求"图标和嵌套文本必须跟随同一状态"的实现依据。- 悬停规则限定在
@media (hover: hover)内:只在支持真实悬停的设备(鼠标等精确指针)上生效,避免触屏设备出现"粘住"的悬停态。
此外还有一条防止"嵌套悬停串扰"的规则:外层 hoverable 处于悬停时,内部未处于悬停的嵌套 hoverable 仍保持其空闲颜色(见 hoverable.ts#L76-L82)。悬停组规则则用opacity0/1 切换实现"悬停才显现"的子元素,例如侧边栏 Composition 条目上悬停时才浮现的上下文菜单按钮(HOVER_GROUP_CLASS_NAME+CompositionContextButton的组合,见 CompositionSelectorItem.tsx#L774-L808)。
键盘焦点样式:FOCUS_VISIBLE_ONLY
SKILL.md 的 "Focus styling" 小节要求:移除默认 outline 与指针聚焦时的阴影,但必须保留键盘焦点指示器。实现方式是在控件上同时添加HOVERABLE_CLASS_NAME与FOCUS_VISIBLE_ONLY_CLASS_NAME,共享类实现如下模式:
.__remotion-focus-visible-only:focus { outline: none; box-shadow: none; } .__remotion-focus-visible-only:focus-visible { box-shadow: ${FOCUS_BOX_SHADOW}; }这条规则已在 Studio 的全局 CSS 中落地:inject-css.ts#L73-L88 对.__remotion-focus-visible-only:focus清空outline与box-shadow,对:focus-visible应用标准的FOCUS_BOX_SHADOW。
FOCUS_BOX_SHADOW 的具体值
FOCUS_BOX_SHADOW定义在 colors.ts#L107-L109,是一个由四段inset阴影拼成的"内描边框"效果,等价于 1px 的#555边框但不会撑开布局:
export const FOCUS_BOX_SHADOW = `inset 1px 1px #555, inset -1px -1px #555, ` + `inset 1px -1px #555, inset -1px 1px #555`;同一文件还定义了规范引用的两个颜色常量:LIGHT_TEXT = '#A6A7A9'(colors.ts#L18)与WHITE = 'white'(colors.ts#L2)。
文档同时强调两条禁令:永远不要移除:focus-visible指示器(键盘可访问性底线),以及未可发现的隐藏操作不要进入 Tab 顺序。仓库中 InlineAction 展示了后一条的落地方式:它通过useZIndex()状态读取统一的tabIndex(InlineAction.tsx#L93),由状态层集中决定哪些控件可被 Tab 到达,而不是每个组件自行硬编码。
真实组件中的完整用法
Button:规范的标准样例
Button.tsx 是这套约定最完整的范例。它把外部传入的style?.backgroundColor/style?.color作为空闲值,未提供时使用TRANSPARENT/LIGHT_TEXT;悬停背景在未指定时取共享约定getBackgroundFromHoverState({hovered: true, selected: false})(即WHITE_ALPHA_06),悬停文字色为WHITE;disabled时把 hover 值折叠回 idle 值(即前文"禁用悬停"技巧):
// 来自 packages/studio/src/components/Button.tsx(节选) className={`${HOVERABLE_CLASS_NAME} ${FOCUS_VISIBLE_ONLY_CLASS_NAME}`} style={{ ...button, ...hoverableStyle({ idleBackground, hoverBackground: disabled || style?.backgroundColor ? idleBackground : getBackgroundFromHoverState({hovered: true, selected: false}), idleColor, hoverColor: disabled || style?.color ? idleColor : WHITE, }), }}注意className中两个类名并排出现,这正是 SKILL.md 对焦点小节的要求;且cursor: 'default'在基础样式与按钮容器样式中各写了一次(见 Button.tsx#L77-L88)。
InlineAction:纯文字操作的透明背景
对于"纯文字/图标操作",InlineAction.tsx#L56-L78 则完全按 SKILL.md 第三条约定,idleBackground使用TRANSPARENT,idleColor/hoverColor默认为LIGHT_TEXT/WHITE,且渲染子内容时统一传入CURRENT_COLOR(InlineAction.tsx#L97),让图标颜色交给 hoverable 的*继承规则处理。
CompositionSelectorItem:背景保持共享约定
侧边栏条目属于"周围组件已使用共享背景悬停约定"的情形,因此 CompositionSelectorItem.tsx#L294-L312 中idleBackground随选中态取值(WHITE_ALPHA_06/TRANSPARENT),hoverBackground使用WHITE_ALPHA_06,idleColor在选中时为WHITE、否则LIGHT_TEXT,hoverColor恒为WHITE——与规范允许的背景例外完全对应。
验证清单与本地检查命令
SKILL.md 的 "Verify" 小节要求在运行中的 Studio内检查控件在各状态下的 computed style,本文按原文完整继承该清单:
| 检查项 | 期望值 |
|---|---|
空闲与悬停时的cursor | default |
| 空闲文字颜色 | rgb(166, 167, 169)(即#A6A7A9) |
| 悬停文字颜色 | rgb(255, 255, 255) |
| 指针聚焦(pointer focus) | 无 outline、无 box-shadow |
键盘Tab聚焦 | 可见标准FOCUS_BOX_SHADOW |
对涉及 Studio 代码的改动,文档要求运行以下两条命令(对应仓库的 Turborepo 工作流,包名为@remotion/studio):
bunx turbo run make --filter='@remotion/studio' bunx turbo run lint test --filter='@remotion/studio'第一条完成该包的编译产物生成,第二条执行 Lint 与测试,改动前后可用同一组命令确认没有引入回归。
适用边界与小结
需要说明适用前提:这套规范是Remotion Studio(packages/studio)前端交互层的内部约定,服务于深色主题(rgb(31, 36, 40)背景,见 colors.ts#L12)下的浏览器 UI,以及以 Browser Studio 形式嵌入<iframe>的运行环境;它不适用于用 Remotion 编写的视频内容组件(<Composition>内的画面元素)。从 Button.tsx、InlineAction.tsx、CompositionSelectorItem.tsx 等十余个使用HOVERABLE_CLASS_NAME的组件看,该约定已在 Studio 的按钮、行内操作、侧边栏条目、时间轴控件等处广泛落地。
小结:遵循 no-pointer 规范时,记住三件事即可——光标永远default;颜色与背景的两种状态通过hoverableStyle()写入 CSS 变量、由 hoverable.ts 生成的全局 CSS 消费;类名上同时挂HOVERABLE_CLASS_NAME与FOCUS_VISIBLE_ONLY_CLASS_NAME,把"鼠标点击无装饰、键盘 Tab 有FOCUS_BOX_SHADOW"交给:focus/:focus-visible选择器完成。最后按上表的五态清单在运行中的 Studio 里逐项核对 computed style,并以两条bunx turbo命令收尾验证。技能入口文件为 .agents/skills/no-pointer/SKILL.md,其 Agent 配置(含默认提示词)位于 .agents/skills/no-pointer/agents/openai.yaml。
【免费下载链接】remotion🎥 Make videos programmatically with React项目地址: https://gitcode.com/GitHub_Trending/re/remotion
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考