Annotation Arrow
A note that draws a hand-drawn arrow to the element it points at, picked by a CSS selector. The curve avoids both boxes and redraws when the layout moves.
View codeStroke icons that act out their meaning on hover or keyboard focus: the bell rings, the bin lifts its lid, the plane flies off and back, the cog turns, refresh spins, the padlock opens. CSS only.
With your AI
Say it to your AI. It gets the exact code below, not its best guess, and knows where it goes.
Add the Kodza Hover Icons to my page.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<button class="kodza-hover-icons kodza-hover-icons--icon" type="button" aria-label="Refresh">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<g class="kodza-hover-icons__turn">
<path d="M5.19 9.52A7.25 7.25 0 0 1 18.57 8.94M15.63 7.69 18.57 8.94 19.51 5.88"/>
<path d="M18.81 14.48A7.25 7.25 0 0 1 5.43 15.06M8.37 16.31 5.43 15.06 4.49 18.12"/>
</g>
</svg>
</button>
<button class="kodza-hover-icons kodza-hover-icons--icon" type="button" aria-label="Settings">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<g class="kodza-hover-icons__gear">
<path d="M18.6 9.98 20.83 10.28V13.72L18.6 14.02A6.9 6.9 0 0 1 18.09 15.24L19.46 17.03 17.03 19.46 15.24 18.09A6.9 6.9 0 0 1 14.02 18.6L13.72 20.83H10.28L9.98 18.6A6.9 6.9 0 0 1 8.76 18.09L6.97 19.46 4.54 17.03 5.91 15.24A6.9 6.9 0 0 1 5.4 14.02L3.17 13.72V10.28L5.4 9.98A6.9 6.9 0 0 1 5.91 8.76L4.54 6.97 6.97 4.54 8.76 5.91A6.9 6.9 0 0 1 9.98 5.4L10.28 3.17H13.72L14.02 5.4A6.9 6.9 0 0 1 15.24 5.91L17.03 4.54 19.46 6.97 18.09 8.76A6.9 6.9 0 0 1 18.6 9.98Z"/>
<circle cx="12" cy="12" r="2.75"/>
</g>
</svg>
</button>
<button class="kodza-hover-icons kodza-hover-icons--icon" type="button" aria-label="Notifications">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path class="kodza-hover-icons__bell" d="M12 3.25V5.25M5.5 17H18.5C17.4 15.8 16.75 14.4 16.75 12.5V10A4.75 4.75 0 0 0 7.25 10V12.5C7.25 14.4 6.6 15.8 5.5 17Z"/>
<path class="kodza-hover-icons__clapper" d="M10.25 19.5A1.9 1.9 0 0 0 13.75 19.5"/>
</svg>
</button>
<button class="kodza-hover-icons" type="button">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<g class="kodza-hover-icons__shackle">
<path class="kodza-hover-icons__swing" d="M8.25 10.5V7.75A3.75 3.75 0 0 1 15.75 7.75V10.5"/>
</g>
<rect x="5" y="10.5" width="14" height="10" rx="2.5"/>
<path d="M12 14.5V16.5"/>
</svg>
<span class="kodza-hover-icons__label">Unlock</span>
</button>
<button class="kodza-hover-icons" type="button">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path class="kodza-hover-icons__lid" d="M4.5 7H19.5M9.25 7V5.25C9.25 4.7 9.7 4.25 10.25 4.25H13.75C14.3 4.25 14.75 4.7 14.75 5.25V7"/>
<path d="M6.5 7 7.3 18.6C7.37 19.65 8.25 20.5 9.3 20.5H14.7C15.75 20.5 16.63 19.65 16.7 18.6L17.5 7M10.25 11V16.5M13.75 11V16.5"/>
</svg>
<span class="kodza-hover-icons__label">Delete</span>
</button>
<button class="kodza-hover-icons kodza-hover-icons--solid" type="button">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path class="kodza-hover-icons__plane" d="M20.5 3.5 3.5 10.25 10.5 13.5 13.75 20.5ZM20.5 3.5 10.5 13.5"/>
</svg>
<span class="kodza-hover-icons__label">Send</span>
</button>
/* Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license */
/* Kodza — Hover Icons
Icon buttons that act out what they do before you click: the bell rings, the bin lifts its lid,
the plane leaves and comes back, the cog turns one tooth, the refresh arrows go round, the
padlock opens. CSS only, on hover (pointer devices) and on keyboard focus. Each icon is plain
inline SVG drawn with currentColor, so it takes the button's colour, and moves by transforms
only. Three looks: a white pill with a label, the same pill with the icon alone (--icon, give it
an aria-label), and the solid ink pill (--solid). Everything is tuned with the
--kz-hover-icons-* variables. */
.kodza-hover-icons {
--kz-hover-icons-height: 40px;
--kz-hover-icons-icon: 18px;
--kz-hover-icons-stroke: 1.75px; /* in icon units, 24 across */
--kz-hover-icons-ink: #0a0a0a;
--kz-hover-icons-on-ink: #ffffff; /* text and icon of the solid pill */
--kz-hover-icons-bg: #ffffff;
--kz-hover-icons-line: rgba(10, 10, 10, 0.12);
--kz-hover-icons-line-hover: rgba(10, 10, 10, 0.28);
--kz-hover-icons-solid-hover: #2a2a2a;
--kz-hover-icons-duration: 0.5s; /* cog, lid and padlock; the bell, the plane and the arrows take longer */
--kz-hover-icons-ease: cubic-bezier(0.34, 1.45, 0.64, 1); /* a small overshoot, so the part clicks into place */
display: inline-flex;
flex: none;
align-items: center;
justify-content: center;
gap: 8px;
box-sizing: border-box;
height: var(--kz-hover-icons-height);
margin: 0;
padding: 0 16px 0 13px;
border: 1px solid var(--kz-hover-icons-line);
border-radius: var(--kz-radius-pill, 999px);
background: var(--kz-hover-icons-bg);
color: var(--kz-hover-icons-ink);
font: inherit;
font-family: var(--kz-font-label, inherit);
font-size: var(--kz-font-label-size, 15px);
font-weight: var(--kz-font-label-weight, 500);
letter-spacing: var(--kz-font-label-tracking, -0.01em);
text-transform: var(--kz-font-label-case, revert);
line-height: 1;
white-space: nowrap;
text-decoration: none;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
transition:
border-color calc(0.2s * var(--kz-motion-scale, 1)) ease,
background-color calc(0.2s * var(--kz-motion-scale, 1)) ease,
scale calc(0.15s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1);
}
/* The icon alone: a round button, its name in aria-label */
.kodza-hover-icons--icon {
width: var(--kz-hover-icons-height);
padding: 0;
}
.kodza-hover-icons--solid {
border-color: var(--kz-hover-icons-ink);
background: var(--kz-hover-icons-ink);
color: var(--kz-hover-icons-on-ink);
}
.kodza-hover-icons:focus-visible {
outline: 2px solid var(--kz-hover-icons-ink);
outline-offset: 3px;
}
.kodza-hover-icons:active {
scale: 0.96;
}
@media (hover: hover) {
.kodza-hover-icons:hover {
border-color: var(--kz-hover-icons-line-hover);
}
.kodza-hover-icons--solid:hover {
border-color: var(--kz-hover-icons-solid-hover);
background: var(--kz-hover-icons-solid-hover);
}
}
.kodza-hover-icons svg {
display: block;
flex: none;
width: var(--kz-hover-icons-icon);
height: var(--kz-hover-icons-icon);
overflow: hidden; /* the plane leaves through the edge and comes back through the other */
fill: none;
stroke: currentColor;
stroke-width: var(--kz-hover-icons-stroke);
stroke-linecap: round;
stroke-linejoin: round;
pointer-events: none;
}
/* Every moving part turns around a point of its own icon, in icon units */
.kodza-hover-icons svg * {
transform-box: view-box;
}
/* At rest: the way back. Each part returns by a transition, from wherever it was */
.kodza-hover-icons__turn {
transform-origin: 12px 12px;
transition: transform calc(0.6s * var(--kz-motion-scale, 1)) cubic-bezier(0.65, 0, 0.35, 1);
}
.kodza-hover-icons__gear {
transform-origin: 12px 12px;
transition: transform calc(var(--kz-hover-icons-duration) * var(--kz-motion-scale, 1)) var(--kz-hover-icons-ease);
}
.kodza-hover-icons__bell,
.kodza-hover-icons__clapper {
transform-origin: 12px 3.25px;
}
.kodza-hover-icons__lid {
transform-origin: 4.5px 7px;
transition: transform calc(var(--kz-hover-icons-duration) * var(--kz-motion-scale, 1)) var(--kz-hover-icons-ease);
}
/* The shackle comes up, then swings open on its left leg; it closes first, then goes down */
.kodza-hover-icons__shackle {
transition: transform calc(0.25s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1) calc(0.12s * var(--kz-motion-scale, 1));
}
.kodza-hover-icons__swing {
transform-origin: 8.25px 10.5px;
transition: transform calc(0.2s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1);
}
/* The act: on hover with a pointer, and on keyboard focus */
@media (hover: hover) {
.kodza-hover-icons:hover .kodza-hover-icons__turn {
transform: rotate(180deg); /* the two arrows are the same at half a turn: it reads as a full turn */
}
.kodza-hover-icons:hover .kodza-hover-icons__gear {
transform: rotate(45deg); /* one tooth of eight */
}
.kodza-hover-icons:hover .kodza-hover-icons__bell {
animation: kodza-hover-icons-ring calc(0.9s * var(--kz-motion-scale, 1)) ease-out;
}
.kodza-hover-icons:hover .kodza-hover-icons__clapper {
animation: kodza-hover-icons-ring calc(0.9s * var(--kz-motion-scale, 1)) ease-out calc(0.06s * var(--kz-motion-scale, 1)) backwards;
}
.kodza-hover-icons:hover .kodza-hover-icons__lid {
transform: translateY(-1px) rotate(-12deg);
}
.kodza-hover-icons:hover .kodza-hover-icons__shackle {
transform: translateY(-2px);
transition-delay: 0s;
}
.kodza-hover-icons:hover .kodza-hover-icons__swing {
transform: rotate(-18deg);
transition: transform calc(var(--kz-hover-icons-duration) * var(--kz-motion-scale, 1)) var(--kz-hover-icons-ease) calc(0.12s * var(--kz-motion-scale, 1));
}
.kodza-hover-icons:hover .kodza-hover-icons__plane {
animation: kodza-hover-icons-fly calc(0.8s * var(--kz-motion-scale, 1)) both;
}
}
.kodza-hover-icons:focus-visible .kodza-hover-icons__turn {
transform: rotate(180deg);
}
.kodza-hover-icons:focus-visible .kodza-hover-icons__gear {
transform: rotate(45deg);
}
.kodza-hover-icons:focus-visible .kodza-hover-icons__bell {
animation: kodza-hover-icons-ring calc(0.9s * var(--kz-motion-scale, 1)) ease-out;
}
.kodza-hover-icons:focus-visible .kodza-hover-icons__clapper {
animation: kodza-hover-icons-ring calc(0.9s * var(--kz-motion-scale, 1)) ease-out calc(0.06s * var(--kz-motion-scale, 1)) backwards;
}
.kodza-hover-icons:focus-visible .kodza-hover-icons__lid {
transform: translateY(-1px) rotate(-12deg);
}
.kodza-hover-icons:focus-visible .kodza-hover-icons__shackle {
transform: translateY(-2px);
transition-delay: 0s;
}
.kodza-hover-icons:focus-visible .kodza-hover-icons__swing {
transform: rotate(-18deg);
transition: transform calc(var(--kz-hover-icons-duration) * var(--kz-motion-scale, 1)) var(--kz-hover-icons-ease) calc(0.12s * var(--kz-motion-scale, 1));
}
.kodza-hover-icons:focus-visible .kodza-hover-icons__plane {
animation: kodza-hover-icons-fly calc(0.8s * var(--kz-motion-scale, 1)) both;
}
/* A swing that dies down, like a real bell */
@keyframes kodza-hover-icons-ring {
0% { transform: rotate(0deg); }
14% { transform: rotate(18deg); }
30% { transform: rotate(-14deg); }
46% { transform: rotate(9deg); }
62% { transform: rotate(-5deg); }
78% { transform: rotate(2deg); }
100% { transform: rotate(0deg); }
}
/* Out through the top right, speeding up; back in from the bottom left, slowing down */
@keyframes kodza-hover-icons-fly {
0% {
transform: translate(0, 0);
animation-timing-function: cubic-bezier(0.5, 0, 0.75, 0);
}
40% {
transform: translate(20px, -20px);
animation-timing-function: steps(1, end);
}
40.01% {
transform: translate(-20px, 20px);
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
100% {
transform: translate(0, 0);
}
}
@media (prefers-reduced-motion: reduce) {
.kodza-hover-icons,
.kodza-hover-icons svg * {
transition-duration: 0.01ms !important;
transition-delay: 0s !important;
}
/* The icons stay still: what they act out is a bonus, the label says it */
.kodza-hover-icons svg * {
transform: none !important;
animation: none !important;
}
}
/* kodza-guard ed4c08481ced — 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-hover-icons):not(#_) {
outline: revert;
outline-offset: revert;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-hover-icons:not(#_) {
list-style: revert;
position: revert;
box-shadow: revert;
appearance: revert;
max-height: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
min-height: revert;
}
.kodza-hover-icons__turn:not(#_),
.kodza-hover-icons__gear:not(#_),
.kodza-hover-icons__bell:not(#_),
.kodza-hover-icons__clapper:not(#_),
.kodza-hover-icons__shackle:not(#_),
.kodza-hover-icons__swing:not(#_),
.kodza-hover-icons__label:not(#_),
.kodza-hover-icons__lid:not(#_),
.kodza-hover-icons__plane:not(#_) {
box-sizing: revert;
}
.kodza-hover-icons 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;
}
/* 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-hover-icons:not(#_) {
display: inline-flex;
flex: none;
align-items: center;
justify-content: center;
gap: 8px;
box-sizing: border-box;
height: var(--kz-hover-icons-height);
margin: 0;
padding: 0 16px 0 13px;
border: 1px solid var(--kz-hover-icons-line);
border-radius: var(--kz-radius-pill, 999px);
background: var(--kz-hover-icons-bg);
color: var(--kz-hover-icons-ink);
font: inherit;
font-family: var(--kz-font-label, inherit);
font-size: var(--kz-font-label-size, 15px);
font-weight: var(--kz-font-label-weight, 500);
letter-spacing: var(--kz-font-label-tracking, -0.01em);
text-transform: var(--kz-font-label-case, revert);
line-height: 1;
white-space: nowrap;
text-decoration: none;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
transition: border-color calc(0.2s * var(--kz-motion-scale, 1)) ease,
background-color calc(0.2s * var(--kz-motion-scale, 1)) ease,
scale calc(0.15s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1);
}
.kodza-hover-icons--icon:not(#_) {
width: var(--kz-hover-icons-height);
padding: 0;
}
.kodza-hover-icons--solid:not(#_) {
border-color: var(--kz-hover-icons-ink);
background: var(--kz-hover-icons-ink);
color: var(--kz-hover-icons-on-ink);
}
.kodza-hover-icons:focus-visible:not(#_) {
outline: 2px solid var(--kz-hover-icons-ink);
outline-offset: 3px;
}
.kodza-hover-icons:active:not(#_) {
scale: 0.96;
}
@media (hover: hover) {
.kodza-hover-icons:hover:not(#_) {
border-color: var(--kz-hover-icons-line-hover);
}
}
@media (hover: hover) {
.kodza-hover-icons--solid:hover:not(#_) {
border-color: var(--kz-hover-icons-solid-hover);
background: var(--kz-hover-icons-solid-hover);
}
}
.kodza-hover-icons svg:not(#_) {
display: block;
}
@media (prefers-reduced-motion: reduce) {
.kodza-hover-icons:not(#_) {
transition-duration: 0.01ms !important;
transition-delay: 0s !important;
}
}
'use client';
// Kodza — Hover Icons, for React. From https://kodza.dev/components/hover-icons/
// 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: hover-icons.css, the component's CSS from Kodza, as they are.
// Yours to edit: the markup is plain JSX below.
import { useRef, type ComponentPropsWithoutRef, type ComponentRef, type CSSProperties } from 'react';
import './hover-icons.css';
type Variant = "icon" | "solid";
export type HoverIconsProps = Omit<ComponentPropsWithoutRef<'button'>, "children" | "variant" | "vars"> & {
/** Modifier class(es): .kodza-hover-icons--icon, .kodza-hover-icons--solid */
variant?: Variant | Variant[];
/** Settings, as CSS variables on the root */
vars?: {
/** default: 40px */
"--kz-hover-icons-height"?: string | number;
/** default: 18px */
"--kz-hover-icons-icon"?: string | number;
/** in icon units, 24 across — default: 1.75px */
"--kz-hover-icons-stroke"?: string | number;
/** default: #0a0a0a */
"--kz-hover-icons-ink"?: string | number;
/** text and icon of the solid pill — default: #ffffff */
"--kz-hover-icons-on-ink"?: string | number;
/** default: #ffffff */
"--kz-hover-icons-bg"?: string | number;
/** default: rgba(10, 10, 10, 0.12) */
"--kz-hover-icons-line"?: string | number;
/** default: rgba(10, 10, 10, 0.28) */
"--kz-hover-icons-line-hover"?: string | number;
/** default: #2a2a2a */
"--kz-hover-icons-solid-hover"?: string | number;
/** cog, lid and padlock; the bell, the plane and the arrows take longer — default: 0.5s */
"--kz-hover-icons-duration"?: string | number;
/** a small overshoot, so the part clicks into place — default: cubic-bezier(0.34, 1.45, 0.64, 1) */
"--kz-hover-icons-ease"?: string | number;
};
className?: string;
};
export function HoverIcons({ variant, vars, className, style, ...rest }: HoverIconsProps) {
const ref = useRef<ComponentRef<'button'>>(null);
const classes = ["kodza-hover-icons", "kodza-hover-icons--icon", ...([] as string[]).concat(variant ?? []).map((v) => `kodza-hover-icons--${v}`), className].filter(Boolean).join(' ');
return (
<button type="button" aria-label="Refresh" {...rest} className={classes} style={{ ...vars, ...style } as CSSProperties} ref={ref}>
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<g className="kodza-hover-icons__turn">
<path d="M5.19 9.52A7.25 7.25 0 0 1 18.57 8.94M15.63 7.69 18.57 8.94 19.51 5.88" />
<path d="M18.81 14.48A7.25 7.25 0 0 1 5.43 15.06M8.37 16.31 5.43 15.06 4.49 18.12" />
</g>
</svg>
</button>
);
}
Checked
Robots poked at it before you did. Every component goes through the same checks, and again each time it changes.