Cookie Consent
An honest cookie card: Reject weighs as much as Accept, Choose unfolds the categories, the choice is kept and a link brings the card back.
View codeOne bubble per group: it waits on the first hover, then glides from button to button, resizing to its words, with the keyboard shortcut inside.
With your AI
Say it to your AI. It gets the exact code below, not its best guess, and knows where it goes.
Add Kodza Tooltips to the icon buttons of my toolbar.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<div class="kodza-tooltip kodza-tooltip--toolbar" role="toolbar" aria-label="Formatting">
<button class="kodza-tooltip__trigger" type="button" data-tip="Bold" data-keys="⌘ B" aria-label="Bold"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3h4a2.5 2.5 0 0 1 0 5H5zM5 8h4.5a2.5 2.5 0 0 1 0 5H5z"/></svg></button>
<button class="kodza-tooltip__trigger" type="button" data-tip="Italic" data-keys="⌘ I" aria-label="Italic"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M7 3h5M4 13h5M10 3 6 13"/></svg></button>
<button class="kodza-tooltip__trigger" type="button" data-tip="Strikethrough" data-keys="⌘ ⇧ X" aria-label="Strikethrough"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 8h10M11 4.5C10.5 3.6 9.4 3 8 3 6.3 3 5 3.9 5 5.2c0 .7.4 1.3 1 1.8M5 11.5c.5.9 1.6 1.5 3 1.5 1.7 0 3-.9 3-2.2 0-.5-.2-1-.5-1.3"/></svg></button>
<span class="kodza-tooltip__divider" aria-hidden="true"></span>
<button class="kodza-tooltip__trigger" type="button" data-tip="Link" data-keys="⌘ K" aria-label="Link"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M7 9a2.5 2.5 0 0 0 3.5 0l2-2A2.5 2.5 0 0 0 9 3.5l-.7.7M9 7a2.5 2.5 0 0 0-3.5 0l-2 2A2.5 2.5 0 0 0 7 12.5l.7-.7"/></svg></button>
<button class="kodza-tooltip__trigger" type="button" data-tip="Code" data-keys="⌘ E" aria-label="Code"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6 4.5 2.5 8 6 11.5M10 4.5 13.5 8 10 11.5"/></svg></button>
<button class="kodza-tooltip__trigger" type="button" data-tip="Ship it" data-keys="⌘ ↵" aria-label="Ship it"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 13V3M3.5 7.5 8 3l4.5 4.5"/></svg></button>
</div>
<p class="kodza-tooltip kodza-tooltip--bottom">
Built with
<span class="kodza-tooltip__trigger kodza-tooltip__term" tabindex="0" data-tip="Cascading Style Sheets. Cascading on purpose.">CSS</span>,
a little
<span class="kodza-tooltip__trigger kodza-tooltip__term" tabindex="0" data-tip="Document Object Model. The tree you keep climbing.">DOM</span>
and zero
<span class="kodza-tooltip__trigger kodza-tooltip__term" tabindex="0" data-tip="node_modules. The heaviest object in the known universe.">npm installs</span>.
</p>
/* Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license */
/* Kodza — Tooltip
One bubble per group: the first hover waits a little (500 ms), then moving to a neighbour
is instant: the bubble glides over, resizes to its new words and keeps its arrow on the
trigger. Keyboard focus shows it at once, Escape hides it. Put the text in data-tip and a
shortcut in data-keys. Above by default, below with --bottom, flipped when there is no room.
Everything is tuned with the --kz-tip-* variables. */
.kodza-tooltip {
--kz-tip-bg: #0a0a0a;
--kz-tip-fg: #ffffff;
--kz-tip-muted: rgba(255, 255, 255, 0.62);
--kz-tip-key: rgba(255, 255, 255, 0.14);
--kz-tip-surface: #ffffff;
--kz-tip-ink: #0a0a0a;
--kz-tip-line: rgba(10, 10, 10, 0.12);
--kz-tip-soft: rgba(10, 10, 10, 0.06);
--kz-tip-delay: 500ms;
--kz-tip-ease: cubic-bezier(0.22, 1, 0.36, 1);
position: relative;
color: var(--kz-tip-ink);
}
/* A toolbar of icon buttons */
.kodza-tooltip--toolbar {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 4px;
border-radius: 14px;
background: var(--kz-tip-surface);
box-shadow: inset 0 0 0 1px var(--kz-tip-line);
}
.kodza-tooltip--toolbar .kodza-tooltip__trigger {
display: grid;
place-items: center;
width: 36px;
height: 36px;
padding: 0;
border: 0;
border-radius: 10px;
background: none;
color: inherit;
cursor: pointer;
transition: background-color 0.2s ease;
}
.kodza-tooltip--toolbar .kodza-tooltip__trigger:hover {
background: var(--kz-tip-soft);
}
.kodza-tooltip--toolbar svg {
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 1.6;
stroke-linecap: round;
stroke-linejoin: round;
}
.kodza-tooltip__divider {
width: 1px;
height: 20px;
margin: 0 4px;
background: var(--kz-tip-line);
}
/* Terms in running text */
.kodza-tooltip--bottom {
margin: 0;
font-size: 17px;
line-height: 1.6;
}
.kodza-tooltip__term {
border-radius: 3px;
text-decoration: underline dotted;
text-decoration-thickness: 1.5px;
text-underline-offset: 4px;
cursor: help;
}
.kodza-tooltip__trigger:focus-visible {
outline: 2px solid var(--kz-tip-ink);
outline-offset: 2px;
}
/* The bubble */
.kodza-tooltip__bubble {
position: absolute;
top: 0;
left: 0;
z-index: 30;
display: flex;
align-items: center;
gap: 8px;
max-width: 260px;
padding: 7px 10px;
border-radius: 9px;
background: var(--kz-tip-bg);
color: var(--kz-tip-fg);
font-size: 13px;
font-weight: 500;
line-height: 1.35;
white-space: nowrap;
pointer-events: none;
visibility: hidden;
opacity: 0;
transform: translate(var(--kz-tip-x, 0), var(--kz-tip-y, 0)) scale(0.92);
transform-origin: var(--kz-tip-arrow, 50%) 100%;
transition: opacity 0.15s ease, transform 0.2s var(--kz-tip-ease), visibility 0s linear 0.2s;
}
.kodza-tooltip__bubble[data-side="bottom"] {
transform-origin: var(--kz-tip-arrow, 50%) 0;
}
.kodza-tooltip__bubble.is-wrapping {
white-space: normal;
width: max-content;
}
.kodza-tooltip__bubble.is-on {
visibility: visible;
opacity: 1;
transform: translate(var(--kz-tip-x, 0), var(--kz-tip-y, 0));
transition: opacity 0.15s ease, transform 0.25s var(--kz-tip-ease), visibility 0s;
}
/* Moving between neighbours: no fade, the bubble glides */
.kodza-tooltip__bubble.is-gliding {
transition: transform 0.25s var(--kz-tip-ease);
}
.kodza-tooltip__bubble::after {
content: "";
position: absolute;
top: 100%;
left: var(--kz-tip-arrow, 50%);
border: 5px solid transparent;
border-top-color: var(--kz-tip-bg);
transform: translateX(-50%);
}
.kodza-tooltip__bubble[data-side="bottom"]::after {
top: auto;
bottom: 100%;
border-top-color: transparent;
border-bottom-color: var(--kz-tip-bg);
}
.kodza-tooltip__words {
position: relative;
display: inline-grid;
overflow: hidden;
}
.kodza-tooltip__words > span {
grid-area: 1 / 1;
}
.kodza-tooltip__words > .is-leaving {
position: absolute;
top: 0;
left: 0;
}
.kodza-tooltip__keys {
display: flex;
gap: 3px;
}
.kodza-tooltip__keys kbd {
display: grid;
place-items: center;
min-width: 18px;
height: 18px;
padding: 0 4px;
border-radius: 5px;
background: var(--kz-tip-key);
color: var(--kz-tip-muted);
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 11px;
box-sizing: border-box;
}
.kodza-tooltip__keys:empty {
display: none;
}
@media (prefers-reduced-motion: reduce) {
.kodza-tooltip__bubble,
.kodza-tooltip__bubble.is-on,
.kodza-tooltip__bubble.is-gliding,
.kodza-tooltip--toolbar .kodza-tooltip__trigger {
transition-duration: 0.01ms !important;
}
}
/* Theming example — on a dark background:
.dark-section .kodza-tooltip {
--kz-tip-bg: #f2f2f0;
--kz-tip-fg: #0a0a0a;
--kz-tip-muted: rgba(10, 10, 10, 0.6);
--kz-tip-key: rgba(10, 10, 10, 0.08);
--kz-tip-surface: #161616;
--kz-tip-ink: #f2f2f0;
--kz-tip-line: rgba(255, 255, 255, 0.14);
--kz-tip-soft: rgba(255, 255, 255, 0.08);
}
*/
/* kodza-guard a9dde8c8b39f — generated by Kodza, do not edit.
Theme guard: keeps a site's own CSS (WordPress themes, Bootstrap, CSS resets…) off this
component. :not(#_) is always true and weighs as much as an id, so these lines win over
theme rules made of classes and tags. Customise the component through its --kz-* variables. */
/* What the browser gives, where the component sets it only in some states: at the lowest weight, so its own rules below put it back */
:where(.kodza-tooltip):not(#_) {
font-size: revert;
line-height: revert;
}
:where(.kodza-tooltip__trigger):not(#_) {
border: revert;
padding: revert;
outline: revert;
transition: revert;
background-color: revert;
background-image: revert;
outline-offset: revert;
color: revert;
}
:where(.kodza-tooltip__trigger):not(#_):where(:not([hidden])) {
display: revert;
}
:where(.kodza-tooltip__trigger):not(#_):where([hidden]) {
display: none;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-tooltip:not(#_) {
border: revert;
outline: revert;
text-decoration: revert;
transition: revert;
list-style: revert;
box-sizing: revert;
outline-offset: revert;
appearance: revert;
max-height: revert;
font-family: revert;
font-weight: revert;
font-style: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
}
.kodza-tooltip__trigger:not(#_) {
margin: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: revert;
box-shadow: revert;
appearance: revert;
max-width: revert;
max-height: revert;
font-family: revert;
font-size: revert;
font-weight: revert;
font-style: revert;
line-height: revert;
letter-spacing: revert;
text-transform: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
white-space: revert;
min-height: revert;
}
.kodza-tooltip__divider:not(#_),
.kodza-tooltip__bubble:not(#_),
.kodza-tooltip__words:not(#_),
.kodza-tooltip__keys:not(#_),
.kodza-tooltip__words span:where(:not([class*="kodza-"])):not(#_) {
box-sizing: revert;
}
.kodza-tooltip__trigger svg:where(:not([class*="kodza-"])):not(#_) {
border: revert;
margin: revert;
padding: revert;
border-radius: revert;
outline: revert;
text-decoration: revert;
transition: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: revert;
background-color: revert;
background-image: revert;
box-shadow: revert;
outline-offset: revert;
appearance: revert;
max-width: revert;
max-height: revert;
color: revert;
font-family: revert;
font-size: revert;
font-weight: revert;
font-style: revert;
line-height: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
}
.kodza-tooltip__trigger svg:where(:not([class*="kodza-"])):not(#_):where(:not([hidden])) {
display: revert;
}
.kodza-tooltip__trigger svg:where(:not([class*="kodza-"])):not(#_):where([hidden]) {
display: none;
}
.kodza-tooltip__bubble:not(#_)::after {
box-sizing: revert;
}
/* The component's own rules, one step stronger: real buttons and fields, what it sets on tags themes style (links, lists, headings, images…), and the root width (a theme may narrow it, never widen it) */
.kodza-tooltip:where(p):not(#_) {
position: relative;
color: var(--kz-tip-ink);
}
.kodza-tooltip--toolbar .kodza-tooltip__trigger:not(#_) {
display: grid;
place-items: center;
width: 36px;
height: 36px;
padding: 0;
border: 0;
border-radius: 10px;
background: none;
color: inherit;
cursor: pointer;
transition: background-color 0.2s ease;
}
.kodza-tooltip--toolbar .kodza-tooltip__trigger:hover:not(#_) {
background: var(--kz-tip-soft);
}
.kodza-tooltip--bottom:not(#_) {
font-size: 17px;
line-height: 1.6;
}
.kodza-tooltip__trigger:focus-visible:not(#_) {
outline: 2px solid var(--kz-tip-ink);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.kodza-tooltip--toolbar .kodza-tooltip__trigger:not(#_) {
transition-duration: 0.01ms !important;
}
}
// Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license
// Kodza — Tooltip
// One bubble per group, described by every trigger's data-tip (and data-keys). The first hover
// waits --kz-tip-delay; within 400 ms of leaving a trigger, the next one is instant and the
// bubble glides over, its words rolling. Focus shows it at once, Escape hides it. Positions are
// measured in the group's own pixels, so a scaled parent does not shift it.
(() => {
const SELECTOR = '.kodza-tooltip';
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let uid = 0;
// One instance. Everything it starts is tied to `signal`, so kodza:destroy stops it all
function mount(root, signal) {
const triggers = [...root.querySelectorAll('.kodza-tooltip__trigger[data-tip]')];
if (!triggers.length) return;
const below = root.classList.contains('kodza-tooltip--bottom');
const delay = parseFloat(getComputedStyle(root).getPropertyValue('--kz-tip-delay')) || 500;
const bubble = document.createElement('span');
bubble.className = 'kodza-tooltip__bubble';
bubble.id = `kodza-tooltip-${++uid}`;
bubble.setAttribute('role', 'tooltip');
bubble.innerHTML = '<span class="kodza-tooltip__words"><span></span></span><span class="kodza-tooltip__keys"></span>';
root.append(bubble);
const words = bubble.querySelector('.kodza-tooltip__words');
const keys = bubble.querySelector('.kodza-tooltip__keys');
let current = null;
let warm = false;
let timer = 0;
let cool = 0;
const write = (trigger, roll) => {
const text = trigger.dataset.tip;
const prev = words.lastElementChild;
const span = Object.assign(document.createElement('span'), { textContent: text });
words.append(span);
if (!roll || reduceMotion) prev.remove();
else {
// The leaving word steps out of the flow: the bubble sizes itself to the new one only
prev.classList.add('is-leaving');
const dir = triggers.indexOf(trigger) > triggers.indexOf(current) ? 1 : -1;
const timing = { duration: 220, easing: 'cubic-bezier(0.22, 1, 0.36, 1)' };
prev.animate([{ transform: 'none', opacity: 1 }, { transform: `translateX(${dir * -40}%)`, opacity: 0 }], { ...timing, fill: 'forwards' }).finished.then(() => prev.remove());
span.animate([{ transform: `translateX(${dir * 40}%)`, opacity: 0 }, { transform: 'none', opacity: 1 }], timing);
}
keys.replaceChildren(...(trigger.dataset.keys || '').split(/\s+/).filter(Boolean).map((key) => Object.assign(document.createElement('kbd'), { textContent: key })));
bubble.classList.toggle('is-wrapping', text.length > 40);
};
// Above (or below) the trigger, centred, kept inside the screen, arrow on the trigger
const place = (trigger) => {
const k = root.getBoundingClientRect().width / root.offsetWidth || 1;
const r = root.getBoundingClientRect();
const t = trigger.getBoundingClientRect();
const w = bubble.offsetWidth;
const h = bubble.offsetHeight;
const roomAbove = t.top - h * k - 12 > 0;
const roomBelow = t.bottom + h * k + 12 < innerHeight;
const side = below ? (roomBelow || !roomAbove ? 'bottom' : 'top') : (roomAbove || !roomBelow ? 'top' : 'bottom');
const centre = (t.left + t.width / 2 - r.left) / k;
const minX = (8 - r.left) / k;
const maxX = (innerWidth - 8 - r.left) / k - w;
const x = Math.min(maxX, Math.max(minX, centre - w / 2));
const y = side === 'top' ? (t.top - r.top) / k - h - 10 : (t.bottom - r.top) / k + 10;
bubble.dataset.side = side;
bubble.style.setProperty('--kz-tip-x', `${x}px`);
bubble.style.setProperty('--kz-tip-y', `${y}px`);
bubble.style.setProperty('--kz-tip-arrow', `${centre - x}px`);
};
const show = (trigger) => {
clearTimeout(timer);
clearTimeout(cool);
const gliding = bubble.classList.contains('is-on') && current && current !== trigger;
// The content width (the one `width` sets), read mid-glide if one is running
const width = () => parseFloat(getComputedStyle(bubble).width);
const from = width();
write(trigger, gliding);
bubble.classList.toggle('is-gliding', gliding);
// The bubble eases to its new width
if (gliding && !reduceMotion) {
const to = width();
if (Math.abs(to - from) > 1) bubble.animate([{ width: `${from}px` }, { width: `${to}px` }], { duration: 250, easing: 'cubic-bezier(0.22, 1, 0.36, 1)' });
}
place(trigger);
current?.removeAttribute('aria-describedby');
trigger.setAttribute('aria-describedby', bubble.id);
current = trigger;
bubble.classList.add('is-on');
warm = true;
};
const hide = (soon = true) => {
clearTimeout(timer);
bubble.classList.remove('is-on', 'is-gliding');
current?.removeAttribute('aria-describedby');
// Coming back to a neighbour quickly skips the wait
clearTimeout(cool);
cool = setTimeout(() => (warm = false), soon ? 400 : 0);
};
triggers.forEach((trigger) => {
trigger.addEventListener('pointerenter', () => {
clearTimeout(timer);
if (warm) show(trigger);
else timer = setTimeout(() => show(trigger), delay);
}, { signal });
trigger.addEventListener('pointerleave', () => {
clearTimeout(timer);
timer = setTimeout(() => hide(), 60);
}, { signal });
trigger.addEventListener('focus', () => show(trigger), { signal });
trigger.addEventListener('blur', () => hide(), { signal });
trigger.addEventListener('pointerdown', () => hide(false), { signal });
});
root.addEventListener('keydown', (event) => event.key === 'Escape' && hide(false), { signal });
// A toolbar moves with the arrow keys
if (root.getAttribute('role') === 'toolbar') {
triggers.forEach((trigger, i) => (trigger.tabIndex = i === 0 ? 0 : -1));
root.addEventListener('keydown', (event) => {
const index = triggers.indexOf(document.activeElement);
const step = { ArrowRight: 1, ArrowLeft: -1 }[event.key];
if (index < 0 || !step) return;
event.preventDefault();
const next = triggers[(index + step + triggers.length) % triggers.length];
triggers.forEach((trigger) => (trigger.tabIndex = trigger === next ? 0 : -1));
next.focus();
}, { signal });
}
}
// Kodza lifecycle, the same in every component: `kodza:init` starts the instances inside
// event.detail.root (the whole page by default), `kodza:destroy` stops them and puts their
// markup back. For themes and apps that swap content without a page load.
const instances = new WeakMap();
const find = (scope = document) => [scope, ...scope.querySelectorAll(SELECTOR)].filter((el) => el.matches?.(SELECTOR));
const init = (scope) => find(scope).forEach((root) => {
if (root.classList.contains('is-ready')) return; // already running, maybe from another copy of this script
const controller = new AbortController();
instances.set(root, { controller, html: root.innerHTML, attrs: [...root.attributes].map((a) => [a.name, a.value]) });
mount(root, controller.signal);
root.classList.add('is-ready');
});
const destroy = (scope) => find(scope).forEach((root) => {
const instance = instances.get(root);
if (!instance) return;
instances.delete(root);
instance.controller.abort();
[...root.attributes].forEach((a) => root.removeAttribute(a.name));
instance.attrs.forEach(([name, value]) => root.setAttribute(name, value));
root.innerHTML = instance.html;
});
document.addEventListener('kodza:init', (event) => init(event.detail?.root));
document.addEventListener('kodza:destroy', (event) => destroy(event.detail?.root));
init();
// Loaded in <head>, the script runs before the markup exists: once more when it is parsed
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => init(), { once: true });
})();
'use client';
// Kodza — Tooltip, for React. From https://kodza.dev/components/tooltip-component/
// Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license
// Next to this file: tooltip-component.css and tooltip-component.js, the component's CSS and JS from Kodza, as they are.
// Yours to edit: the markup is plain JSX below.
// The script changes this markup once React has rendered it (it adds layers, splits text, moves
// nodes), so React must not patch it afterwards. When a prop changes, the key gives React a fresh
// element: kodza:destroy stops the script on the old one, kodza:init starts it on the new one.
import { useRef, useEffect, type ComponentPropsWithoutRef, type ComponentRef, type CSSProperties } from 'react';
import './tooltip-component.css';
type Variant = "bottom" | "toolbar";
export type TooltipProps = Omit<ComponentPropsWithoutRef<'div'>, "children" | "variant" | "vars"> & {
/** Modifier class(es): .kodza-tooltip--bottom, .kodza-tooltip--toolbar */
variant?: Variant | Variant[];
/** Settings, as CSS variables on the root */
vars?: {
/** default: #0a0a0a */
"--kz-tip-bg"?: string | number;
/** default: #ffffff */
"--kz-tip-fg"?: string | number;
/** default: rgba(255, 255, 255, 0.62) */
"--kz-tip-muted"?: string | number;
/** default: rgba(255, 255, 255, 0.14) */
"--kz-tip-key"?: string | number;
/** default: #ffffff */
"--kz-tip-surface"?: string | number;
/** default: #0a0a0a */
"--kz-tip-ink"?: string | number;
/** default: rgba(10, 10, 10, 0.12) */
"--kz-tip-line"?: string | number;
/** default: rgba(10, 10, 10, 0.06) */
"--kz-tip-soft"?: string | number;
/** default: 500ms */
"--kz-tip-delay"?: string | number;
/** default: cubic-bezier(0.22, 1, 0.36, 1) */
"--kz-tip-ease"?: string | number;
};
className?: string;
};
export function Tooltip({ variant, vars, className, style, ...rest }: TooltipProps) {
const ref = useRef<ComponentRef<'div'>>(null);
const classes = ["kodza-tooltip", "kodza-tooltip--toolbar", ...([] as string[]).concat(variant ?? []).map((v) => `kodza-tooltip--${v}`), className].filter(Boolean).join(' ');
// Everything the markup is made of: when it changes, React gets a fresh element (see the top)
const key = JSON.stringify([classes, vars, style, rest]);
// Starts the script on this element once React has rendered it, stops it when it goes
useEffect(() => {
const root = ref.current;
let live = true;
// @ts-ignore: a plain script, no types to find
import('./tooltip-component.js').then(() => {
if (live) document.dispatchEvent(new CustomEvent('kodza:init', { detail: { root } }));
});
return () => {
live = false;
document.dispatchEvent(new CustomEvent('kodza:destroy', { detail: { root } }));
};
}, [key]);
return (
<div role="toolbar" aria-label="Formatting" key={key} {...rest} className={classes} style={{ ...vars, ...style } as CSSProperties} ref={ref}>
<button className="kodza-tooltip__trigger" type="button" data-tip="Bold" data-keys="⌘ B" aria-label="Bold">
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M5 3h4a2.5 2.5 0 0 1 0 5H5zM5 8h4.5a2.5 2.5 0 0 1 0 5H5z" />
</svg>
</button>
{' '}
<button className="kodza-tooltip__trigger" type="button" data-tip="Italic" data-keys="⌘ I" aria-label="Italic">
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M7 3h5M4 13h5M10 3 6 13" />
</svg>
</button>
{' '}
<button className="kodza-tooltip__trigger" type="button" data-tip="Strikethrough" data-keys="⌘ ⇧ X" aria-label="Strikethrough">
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M3 8h10M11 4.5C10.5 3.6 9.4 3 8 3 6.3 3 5 3.9 5 5.2c0 .7.4 1.3 1 1.8M5 11.5c.5.9 1.6 1.5 3 1.5 1.7 0 3-.9 3-2.2 0-.5-.2-1-.5-1.3" />
</svg>
</button>
{' '}
<span className="kodza-tooltip__divider" aria-hidden="true" />
{' '}
<button className="kodza-tooltip__trigger" type="button" data-tip="Link" data-keys="⌘ K" aria-label="Link">
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M7 9a2.5 2.5 0 0 0 3.5 0l2-2A2.5 2.5 0 0 0 9 3.5l-.7.7M9 7a2.5 2.5 0 0 0-3.5 0l-2 2A2.5 2.5 0 0 0 7 12.5l.7-.7" />
</svg>
</button>
{' '}
<button className="kodza-tooltip__trigger" type="button" data-tip="Code" data-keys="⌘ E" aria-label="Code">
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M6 4.5 2.5 8 6 11.5M10 4.5 13.5 8 10 11.5" />
</svg>
</button>
{' '}
<button className="kodza-tooltip__trigger" type="button" data-tip="Ship it" data-keys="⌘ ↵" aria-label="Ship it">
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M8 13V3M3.5 7.5 8 3l4.5 4.5" />
</svg>
</button>
</div>
);
}
Checked
Robots poked at it before you did. Every component goes through the same checks, and again each time it changes.