macOS Magnify Dock
A floating icon dock whose items swell under the pointer, the neighbours lifting less than the one you are on.
6 个框架高级
A floating strip of formatting actions with real separators and full arrow-key navigation.
<!--
A toolbar is commands, not navigation - so it is role="toolbar" with buttons,
NOT a <nav> with links. The difference is not pedantry: role="toolbar" makes
the whole strip one Tab stop and hands the arrow keys to the buttons inside,
which is exactly what the roving tabindex below implements.
-->
<div class="toolbar" role="toolbar" aria-label="Formatting" aria-orientation="horizontal">
<button class="toolbar__btn" type="button" aria-label="Bold" aria-pressed="true" tabindex="0">
<svg class="toolbar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M7 5h6a3.5 3.5 0 0 1 0 7H7Zm0 7h7a3.5 3.5 0 0 1 0 7H7Z" />
</svg>
</button>
<button class="toolbar__btn" type="button" aria-label="Italic" aria-pressed="false" tabindex="-1">
<svg class="toolbar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false">
<path d="M15 5h-5M14 19H9M14 5l-4 14" />
</svg>
</button>
<button class="toolbar__btn" type="button" aria-label="Underline" aria-pressed="false" tabindex="-1">
<svg class="toolbar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false">
<path d="M7 4v7a5 5 0 0 0 10 0V4M5 20h14" />
</svg>
</button>
<!-- A real separator, not a styled div: it is announced as a group break. -->
<div class="toolbar__sep" role="separator" aria-orientation="vertical"></div>
<button class="toolbar__btn" type="button" aria-label="Add link" tabindex="-1">
<svg class="toolbar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M10 13a5 5 0 0 0 7 0l2-2a5 5 0 0 0-7-7l-1 1" />
<path d="M14 11a5 5 0 0 0-7 0l-2 2a5 5 0 0 0 7 7l1-1" />
</svg>
</button>
<button class="toolbar__btn" type="button" aria-label="Add comment" tabindex="-1">
<svg class="toolbar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M4 6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H9l-5 4Z" />
</svg>
</button>
<div class="toolbar__sep" role="separator" aria-orientation="vertical"></div>
<button class="toolbar__btn toolbar__btn--danger" type="button" aria-label="Delete" tabindex="-1">
<svg class="toolbar__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M5 7h14M10 11v6M14 11v6M6 7l1 12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-12M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
</svg>
</button>
</div>
<script>
// Roving tabindex: the toolbar is ONE Tab stop, and the arrow keys move
// between the buttons inside it. Without this every icon is its own stop and
// a seven-button toolbar costs a keyboard user seven presses to walk past.
document.querySelectorAll('.toolbar').forEach(function (toolbar) {
function buttons() {
return Array.prototype.slice.call(toolbar.querySelectorAll('.toolbar__btn:not([disabled])'));
}
function focusAt(index) {
var items = buttons();
var next = items[(index + items.length) % items.length];
items.forEach(function (item) {
item.tabIndex = item === next ? 0 : -1;
});
next.focus();
}
toolbar.addEventListener('keydown', function (event) {
var items = buttons();
var current = items.indexOf(document.activeElement);
if (current === -1) return;
if (event.key === 'ArrowRight') {
event.preventDefault();
focusAt(current + 1);
} else if (event.key === 'ArrowLeft') {
event.preventDefault();
focusAt(current - 1);
} else if (event.key === 'Home') {
event.preventDefault();
focusAt(0);
} else if (event.key === 'End') {
event.preventDefault();
focusAt(items.length - 1);
}
});
toolbar.addEventListener('click', function (event) {
var button = event.target.closest('.toolbar__btn');
if (!button || !button.hasAttribute('aria-pressed')) return;
button.setAttribute('aria-pressed', String(button.getAttribute('aria-pressed') !== 'true'));
});
});
</script>| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
actions | readonly ToolbarAction[] | DEFAULT_ACTIONS | 要渲染的项目数组。 |
onSelect | (id: string) => void | - | 用户选择菜单项时触发,参数为所选项。 |
disabled | boolean | false | 禁止交互并将控件置灰。 |
ariaLabel | string | 'Formatting' | 按钮的无障碍名称。仅有图标的按钮必须设置。 |
className | string | - | 合并到根元素上的额外类名。 |
This one is commands, not navigation, and the distinction drives the whole markup: `role="toolbar"` with `<button>`s rather than a `<nav>` of links. That role is a promise - it tells assistive tech the strip is a single Tab stop whose arrow keys move within it - so the roving tabindex here is not a nicety, it is what makes the promise true. Exactly one button holds `tabindex="0"` at a time; Arrow Left/Right walk and wrap, Home and End jump to the ends. Skip it and a seven-icon toolbar costs a keyboard user seven Tab presses just to get past. The toggles expose `aria-pressed` and are tinted off that same attribute, so "bold is on" is one fact rather than two that can disagree. Separators are `role="separator"` elements, not decorative `<div>`s, because the grouping they imply visually should be announced too. The focus ring is inset (`ring-inset`) rather than offset: an offset ring on a floating surface bleeds onto whatever the toolbar happens to be hovering over, which is exactly where it stops being visible.