SignaturePad 签名板
用指针书写的画布:按下落笔、移动成迹、抬起收笔,输出可缩放、可直接提交的 SVG。
用法
一块画布加一条笔迹路径即可:按下落笔、移动成迹、抬起收笔
组件结构
加粗的是必需部件。
data-scope="signature-pad":root · label · control · guide · path · clear-trigger · status · hidden-input
示例
标题、基准线与清空
基准线是纯画面(带 aria-hidden),清空按钮是原生 button,读屏朗读的是 translations 中的文案
参与表单
提供 name 后带上表单影子,提交的是一份独立 SVG;表单重置会把画布清空
笔迹外形
drawing 调整笔宽与压感:thinning 越大,划得越快笔画越细,simulatePressure 决定压感取设备值还是按速度计算
只读与禁用
只读时已绘制的笔迹可见但不可修改,禁用时连清空按钮都不可按下;两者都使用原生 disabled,不只是视觉置灰
取出签名
签名定稿时 draw-end 带上一份可直接入库的 SVG;提交前用 empty 拦截一次,空签名不应离开客户端
设计指引
何时使用
- 承诺书、回执、验收单上需要手写签名。
- 交付确认、上门服务签收等需要留下确认痕迹的场景。
何时不用
特性
- 笔迹是 SVG 填充路径,放大不模糊;每一笔是同一条路径上的一条子路径。
- 第一笔落下时测量一次画布并固定这套坐标,画布的
viewBox与导出的 SVG 都使用它:容器变宽变窄时,已有笔迹跟随缩放而不是停留在原像素上错位。清空后重新测量。 drawing一组选项调整笔画外形:size决定粗细,thinning让粗细随压感变化,simulatePressure决定压感取设备值还是按落笔速度计算。- 带
name即参与表单提交,提交的是一份独立的 SVG 文档;表单重置会清空画布。 - 笔迹变化时发出
draw,签名定稿时发出draw-end:抬笔、点击清空、表单重置三条路径都发出。按draw-end缓存待提交的 SVG 不会取到过期版本。 - 指针划出画布甚至划出窗口都持续跟随,抬起即收笔;落笔的指针被捕获,手掌与第二根手指的移动不会续进这一笔。
- 画布是一块字段外壳:静息不填底 +
--xh-border-control描边 + 4px 控件圆角、无影;落笔时描边加深,只读只换淡底,禁用退到--xh-border-default+--xh-bg-subtle。画布按宽高比撑高,吃不下字段家族配方钉死的控件行高,因此外壳按同一套字段规则自绘。 - 标签走字段标签档(14 / 500 /
--xh-fg-default),贴画布--xh-space-1;状态句是说明角色(13 /--xh-fg-muted)。 - 清空按钮走 Action Control text 档 sm:缺省
outline描边,白底承载阶梯悬停 100 → 按下 200,按下缩放并换底;空画布时只把静息前景压淡,按钮照常可按。
组合
节点形状是硬约束
画布这一族部件必须落在特定标签上,写错不报错,但无法绘制:
control必须是<svg>;guide必须是control内的<line>;path必须是control内的<path>;clear-trigger必须是原生<button>,hidden-input必须是原生<input>。
viewBox 由组件写入,作者不要在 control 上再写。
两个适配器的分工
- Vue:
XhSignaturePadRoot的默认插槽给出empty/paths/drawing/statusText与toSvg()/clear();也可以用useSignaturePad()自行获取。XhSignaturePadGuide与XhSignaturePadPath必须写在XhSignaturePadControl内:SVG 命名空间由该子树带下,移出后会成为 HTML 元素,无法绘制。 - Web Components:结构由作者编写(Light DOM,不投影插槽)。
<xh-signature-pad>上有clear()、toSvg()与只读的empty;提交前取签名用toSvg(),不需要缓存上一次draw-end。 - 两侧的
status部件内都不需要自行写文字:节点为空时由适配器填入内建文案;写了文字则以作者的为准。
最佳实践
- 给清空按钮一句可见文字或稳定的图标语义,不只依靠一个叉。
- 清空之后安置焦点:按钮被禁用或收起时焦点会回到
<body>,键盘用户每清空一次就丢失一次位置。要么让按钮始终可按(本组件的默认做法),要么清空后显式把焦点交给下一个落点。 - 提交前用
empty拦截:空签名与潦草签名是两回事,前者应在客户端拦截。 - 需要缓存待提交的 SVG 时按
draw-end缓存:清空与表单重置同样会发出它,缓存不会停留在旧版本。不要嗅探清空按钮的点击。 - 存储的是 SVG 文本,不是位图。需要位图时在服务端渲染,不在前端截屏。
反模式
- 把画布做得很窄:手写需要面积,过窄的画布只会产生无法辨认的字迹。
- 让签名成为唯一的确认方式却不提供替代路径,这是可达性问题。
- 把签名图当作身份凭证。它证明的是有人在画布上书写过,不是书写者的身份。
API 参考
产物
| 层 | 值 |
|---|---|
| 自定义元素 | <xh-signature-pad> |
| Vue 组件 | XhSignaturePadClearTrigger XhSignaturePadControl XhSignaturePadGuide XhSignaturePadHiddenInput XhSignaturePadLabel XhSignaturePadPath XhSignaturePadRoot XhSignaturePadStatus |
| 组合式函数 | useSignaturePad |
| 状态机 | signaturePadMachine |
| 皮肤 | @xihan-ui/styles/signature-pad.css |
Props
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
disabled | boolean | 整块不可交互:不响应落笔,清空按钮也不可按下。 | |
readOnly | boolean | 只读:已绘制的签名照常显示,但不可修改。 | |
required | boolean | ||
invalid | boolean | 校验未通过的标记,只改变外观与表单影子上的 aria-invalid。 | |
name | string | 表单字段名;提供后表单影子才带 name 并参与提交。 | |
drawing | SignaturePadDrawingOptions | 笔迹外形。默认为 4px 恒定粗细。 | |
translations | Partial<SignaturePadTranslations> | ||
onDraw | (details: SignaturePadDrawDetails) => void | 每收进一个点通知一次,清空与表单重置时也通知一次(路径为空)。 | |
onDrawEnd | (details: SignaturePadDrawEndDetails) => void | 签名定稿时通知一次并附带可直接提交的 SVG:抬笔、清空、表单重置三条路径都发出。 |
事件
自定义元素将载荷放在 detail;Vue 使用同名 emit。
| 事件 | 载荷 | 说明 |
|---|---|---|
draw | SignaturePadDrawDetails | 笔迹变化时通知一次(含清空与表单重置);detail 为 { paths: string[], path: string } |
draw-end | SignaturePadDrawEndDetails | 签名定稿时通知一次(抬笔、清空、表单重置);detail 为 { paths: string[], svg: string },svg 可直接存储 |
插槽
仅列出带载荷的插槽。
| Vue 组件 | 插槽 | 载荷 | 说明 |
|---|---|---|---|
XhSignaturePadRoot | default | SignaturePadRootSlotProps |
状态
以下名称仅用于内部状态机。
状态:drawing · idle
事件:DRAW.START · DRAW.MOVE · DRAW.END · STROKES.CLEAR · FORM.RESET · PRESS.START · PRESS.END
判据:canDraw · canPress
connect API
getXxxProps() 返回对应部件的宿主属性。
| 成员 | 类型 | 说明 |
|---|---|---|
paths | readonly string[] | 逐笔的填充轮廓 d 串,按落笔先后排列。 |
empty | boolean | 没有任何笔迹。 |
drawing | boolean | 笔正落在画布上。 |
disabled | boolean | |
readOnly | boolean | |
statusText | string | 是否已签名的文案,写入 status 部件;适配器在作者未自行编写文字时把它填入节点。 |
toSvg | () => string | 当前签名的独立 SVG 文档,与表单影子提交的是同一份;空签名为空串。 |
clear | () => void | |
getRootProps | () => T['element'] | |
getLabelProps | () => T['element'] | |
getControlProps | () => T['element'] | |
getGuideProps | () => T['element'] | |
getPathProps | () => T['element'] | |
getClearTriggerProps | () => T['button'] | |
getStatusProps | () => T['element'] | 状态出口:一块 role=status 的活区域,签名与清空都会播报一次。 |
getHiddenInputProps | () => T['input'] | 表单出口:一份视觉隐藏的原生输入,随表单提交当前签名。 |
无障碍
键盘
规格出处:W3C APG
| 按键 | 生效条件 | 行为 |
|---|---|---|
Enter / Space | focus on clear-trigger, 未禁用且非只读 | 清空整块画布;按钮是原生 button,这两个键由平台翻成 click |
Enter / Space | held in clear-trigger, 未禁用且非只读 | 按住期间投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下 |
ARIA
以下属性由 connect 生成。
| 部件 | 属性 | 值 |
|---|---|---|
control | aria-label | translations?.label |
control | aria-labelledby | label 部件的 id |
control | role | 'img' |
guide | aria-hidden | 'true' |
clear-trigger | aria-label | translations?.clearTrigger |
status | aria-atomic | 'true' |
status | aria-live | 'polite' |
status | role | 'status' |
hidden-input | aria-hidden | 'true' |
hidden-input | aria-invalid | 'true' | 'false' |
- 签名天然依赖指针,键盘和读屏无法完成。凡是要求签名的流程,必须同时提供一条不依赖指针的替代路径:打字签名、上传签名图或线下核验。只放一块画布等于把这些用户挡在流程之外。
- 画布报告为
role="img",不是控件:它不接受键盘、不进入 Tab 序列,伪装成控件只会让读屏用户进入无法操作的位置。 - 画布的名称来自
label部件;未渲染标题时退回translations.label。 - 是否已签名由
status部件播报。画布是role="img",名称固定,签名、清空、表单重置后读屏读出的都是同一句,用户无法确认笔迹是否保留。status是role="status"的活动区域,值每变一次播报一次,文案使用translations.statusEmpty/translations.statusSigned。要求签名的表单请渲染它。 - 基准线是纯画面,带
aria-hidden,读屏不读。没有translations.guide文案:给装饰线命名只会增加无信息量的播报。 - 清空按钮是原生
<button>,Enter / Space 由平台激活;按钮内只有图标时读屏读translations.clearTrigger。
样式参考
皮肤
@xihan-ui/styles/signature-pad.css 使用 [data-scope="signature-pad"][data-part="root"] 部件选择器,位于 xihan.components 层。覆盖样式使用 xihan.overrides。
数据属性
由 connect 生成;条件不成立时不输出无值属性。
| 部件 | 属性 | 值 |
|---|---|---|
root | data-disabled | ''(条件成立时才出现) |
root | data-drawing | ''(条件成立时才出现) |
root | data-empty | ''(条件成立时才出现) |
root | data-invalid | ''(条件成立时才出现) |
root | data-readonly | ''(条件成立时才出现) |
label | data-disabled | ''(条件成立时才出现) |
control | data-disabled | ''(条件成立时才出现) |
control | data-drawing | ''(条件成立时才出现) |
control | data-empty | ''(条件成立时才出现) |
control | data-invalid | ''(条件成立时才出现) |
control | data-readonly | ''(条件成立时才出现) |
guide | data-disabled | ''(条件成立时才出现) |
path | data-empty | ''(条件成立时才出现) |
clear-trigger | data-disabled | ''(条件成立时才出现) |
clear-trigger | data-empty | ''(条件成立时才出现) |
clear-trigger | data-pressed | ''(条件成立时才出现) |
clear-trigger | data-xh-action-control | '' |
clear-trigger | data-xh-action-display | 'always' |
clear-trigger | data-xh-action-profile | 'text' |
clear-trigger | data-xh-action-size | 'sm' |
clear-trigger | data-xh-action-variant | 'outline' |
status | data-empty | ''(条件成立时才出现) |
hidden-input | data-disabled | ''(条件成立时才出现) |
CSS 变量
本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。
| 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 |
|---|---|---|---|---|---|
--xh-signature-pad-aspect-ratio | control | aspect-ratio | default | 5 / 2 | signature-pad 的 control 部件 aspect-ratio 覆盖槽。 |
--xh-signature-pad-bg | control | background | default | transparent | signature-pad 的 control 部件 background 覆盖槽。 |
--xh-signature-pad-bg-disabled | control | background | disabled | --xh-bg-subtle | signature-pad 的 control 部件 background 覆盖槽。 |
--xh-signature-pad-bg-readonly | control | background | disablednot([data-disabled])readonly | --xh-bg-subtle | signature-pad 的 control 部件 background 覆盖槽。 |
--xh-signature-pad-border | control | border | default | --xh-border-control | signature-pad 的 control 部件 border 覆盖槽。 |
--xh-signature-pad-border-disabled | control | border-color | disabled | --xh-border-default | signature-pad 的 control 部件 border-color 覆盖槽。 |
--xh-signature-pad-border-drawing | control | border-color | drawing | --xh-border-control-hover | signature-pad 的 control 部件 border-color 覆盖槽。 |
--xh-signature-pad-clear-bg | clear-trigger | background-color | defaultfocus-visible | --xh-_action-variant-bg-focus-visible--xh-_action-variant-bg-rest | signature-pad 的 clear-trigger 部件 background-color 覆盖槽。 |
--xh-signature-pad-clear-bg-active | clear-trigger | background-color | disabledis(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressed | --xh-_action-variant-bg-pressed | signature-pad 的 clear-trigger 部件 background-color 覆盖槽。 |
--xh-signature-pad-clear-bg-disabled | clear-trigger | background-color | disabled | --xh-_action-variant-bg-disabled | signature-pad 的 clear-trigger 部件 background-color 覆盖槽。 |
--xh-signature-pad-clear-bg-hover | clear-trigger | background-color | disabledhoverloadingnot([data-disabled])not([data-loading]) | --xh-_action-variant-bg-hover | signature-pad 的 clear-trigger 部件 background-color 覆盖槽。 |
--xh-signature-pad-clear-border | clear-trigger | borderborder-color | defaultfocus-visible | --xh-_action-variant-border-focus-visible--xh-_action-variant-border-rest | signature-pad 的 clear-trigger 部件 border、border-color 覆盖槽。 |
--xh-signature-pad-clear-border-hover | clear-trigger | border-color | disabledhoveris(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressed | --xh-_action-variant-border-hover--xh-_action-variant-border-pressed | signature-pad 的 clear-trigger 部件 border-color 覆盖槽。 |
--xh-signature-pad-clear-fg | clear-trigger | color | defaultdisabledfocus-visiblehoveris(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressed | --xh-_action-variant-fg-focus-visible--xh-_action-variant-fg-hover--xh-_action-variant-fg-pressed--xh-_action-variant-fg-rest | signature-pad 的 clear-trigger 部件 color 覆盖槽。 |
--xh-signature-pad-clear-fg-empty | clear-trigger | color | empty | --xh-fg-muted | signature-pad 的 clear-trigger 部件 color 覆盖槽。 |
--xh-signature-pad-clear-font-size | clear-trigger | font-size | default | --xh-_action-profile-font-size | signature-pad 的 clear-trigger 部件 font-size 覆盖槽。 |
--xh-signature-pad-clear-gap | clear-trigger | gap | default | --xh-_action-profile-gap | signature-pad 的 clear-trigger 部件 gap 覆盖槽。 |
--xh-signature-pad-clear-h | clear-trigger | block-size | default | --xh-_action-profile-visual-size | signature-pad 的 clear-trigger 部件 block-size 覆盖槽。 |
--xh-signature-pad-clear-icon-size | clear-trigger | --xh-icon-size | default | --xh-_action-profile-glyph-size | signature-pad 的 clear-trigger 部件 --xh-icon-size 覆盖槽。 |
--xh-signature-pad-clear-px | clear-trigger | padding-inline | default | --xh-_action-profile-padding-inline | signature-pad 的 clear-trigger 部件 padding-inline 覆盖槽。 |
--xh-signature-pad-clear-radius | clear-trigger | border-radius | default | --xh-shape-control | signature-pad 的 clear-trigger 部件 border-radius 覆盖槽。 |
--xh-signature-pad-clear-shadow-hover | clear-trigger | box-shadow | disabledhoverloadingnot([data-disabled])not([data-loading]) | none | signature-pad 的 clear-trigger 部件 box-shadow 覆盖槽。 |
--xh-signature-pad-control-border-invalid | control | border-color | invalid | --xh-border-invalid | signature-pad 的 control 部件 border-color 覆盖槽。 |
--xh-signature-pad-gap | labelroot | gapmargin-block-end | default | --xh-space-2 | signature-pad 的 label、root 部件 gap、margin-block-end 覆盖槽。 |
--xh-signature-pad-guide-stroke | guide | stroke | default | --xh-border-control | signature-pad 的 guide 部件 stroke 覆盖槽。 |
--xh-signature-pad-ink | path | fill | default | --xh-fg-default | signature-pad 的 path 部件 fill 覆盖槽。 |
--xh-signature-pad-label-fg | label | color | default | --xh-fg-default | signature-pad 的 label 部件 color 覆盖槽。 |
--xh-signature-pad-label-fg-disabled | label | color | disabled | --xh-fg-subtle | signature-pad 的 label 部件 color 覆盖槽。 |
--xh-signature-pad-label-font-size | label | font-size | default | --xh-text-label-size | signature-pad 的 label 部件 font-size 覆盖槽。 |
--xh-signature-pad-label-font-weight | label | font-weight | default | --xh-text-label-weight | signature-pad 的 label 部件 font-weight 覆盖槽。 |
--xh-signature-pad-label-gap | label | margin-block-end | default | --xh-space-1 | signature-pad 的 label 部件 margin-block-end 覆盖槽。 |
--xh-signature-pad-radius | control | border-radius | default | --xh-shape-control | signature-pad 的 control 部件 border-radius 覆盖槽。 |
--xh-signature-pad-status-fg | status | color | default | --xh-fg-muted | signature-pad 的 status 部件 color 覆盖槽。 |
--xh-signature-pad-status-font-size | status | font-size | default | --xh-text-secondary-size | signature-pad 的 status 部件 font-size 覆盖槽。 |
动效
background · border-color 走 transition 过渡。时长与缓动读动效令牌,改令牌即改全局节奏。
系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。
响应式
- 画布宽度铺满外层容器,高度由宽高比(
--xh-signature-pad-aspect-ratio,默认 5 / 2)决定,窄屏上自动变矮。 - 签名过程中转屏、拖动面板改变宽度时,已有笔迹按
viewBox整体缩放,后续新笔与它落在同一套坐标。 - 触摸设备上画布关闭浏览器的滚动与缩放手势,避免手指划动时页面滚动。
RTL
皮肤用逻辑属性排布(inline-start 一族),dir="rtl" 下自动镜像。
- 画布与基准线不区分左右:笔迹按落笔坐标记录,方向由书写者决定。
- 标题与清空按钮的排布跟随文档方向,皮肤全部使用逻辑属性。
