Share Button
Press Share and the options slide out from behind it, the last one pushing the pill open, while the arrow takes off and a cross draws itself. Copy link turns into a check; X and LinkedIn open in a small window.
View codeA button that fills from the exact point your cursor enters and empties toward where it leaves, with a rolling label and a looping arrow.
With your AI
Say it to your AI. It gets the exact code below, not its best guess, and knows where it goes.
Put the Kodza Fill Button on my sign-up button.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<a class="kodza-fill-button" href="#">
<span class="kodza-fill-button__label">Copy. Paste. Ship.</span>
</a>
<a class="kodza-fill-button kodza-fill-button--solid" href="#">
<span class="kodza-fill-button__label">Get Kodza</span>
</a>
<a class="kodza-fill-button" href="#">
<span class="kodza-fill-button__label">Skip the setup</span>
<span class="kodza-fill-button__icon" aria-hidden="true">
<svg viewBox="0 0 16 16"><path d="M3 8h10M9 4l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>
</span>
</a>
/* Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license */
/* Kodza — Fill Button
The fill grows from the exact point the pointer enters, and leaves where it exits.
Everything is tuned with the --kz-fill-* variables. */
.kodza-fill-button {
--kz-fill-fg: #0a0a0a;
--kz-fill-bg: transparent;
--kz-fill-border: rgba(10, 10, 10, 0.9);
--kz-fill-fill: #0a0a0a;
--kz-fill-fill-fg: #ffffff;
--kz-fill-radius: 999px;
--kz-fill-height: 52px;
--kz-fill-pad-x: 28px;
--kz-fill-gap: 10px;
--kz-fill-duration: 0.65s;
--kz-fill-ease: cubic-bezier(0.22, 1, 0.36, 1);
position: relative;
display: inline-grid;
height: var(--kz-fill-height);
padding: 0;
border: 0;
border-radius: var(--kz-fill-radius);
background: var(--kz-fill-bg);
box-shadow: inset 0 0 0 1.5px var(--kz-fill-border);
color: var(--kz-fill-fg);
font: inherit;
font-weight: 500;
letter-spacing: -0.01em;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
overflow: hidden;
isolation: isolate;
-webkit-tap-highlight-color: transparent;
transition: transform 0.25s var(--kz-fill-ease);
}
.kodza-fill-button:active {
transform: scale(0.97);
}
.kodza-fill-button:focus-visible {
outline: 2px solid var(--kz-fill-fill);
outline-offset: 3px;
}
/* The content exists twice: underneath (resting colors) and inside the fill
(inverted colors). The circle reveals the second copy, so the text changes
color exactly under the edge of the circle. */
.kodza-fill-button__content,
.kodza-fill-button__fill {
grid-area: 1 / 1;
display: flex;
align-items: center;
justify-content: center;
gap: var(--kz-fill-gap);
padding: 0 var(--kz-fill-pad-x);
}
.kodza-fill-button__fill {
background: var(--kz-fill-fill);
color: var(--kz-fill-fill-fg);
clip-path: circle(0% at var(--kz-x, 50%) var(--kz-y, 50%));
transition: clip-path var(--kz-fill-duration) var(--kz-fill-ease);
pointer-events: none;
}
.kodza-fill-button.is-hover .kodza-fill-button__fill,
.kodza-fill-button:focus-visible .kodza-fill-button__fill {
clip-path: circle(150% at var(--kz-x, 50%) var(--kz-y, 50%));
}
/* Rolling label: the line leaves through the top, its copy comes in from below */
.kodza-fill-button__label {
position: relative;
display: block;
overflow: hidden;
line-height: 1.25;
}
.kodza-fill-button__label > span {
display: block;
transition: transform var(--kz-fill-duration) var(--kz-fill-ease);
}
.kodza-fill-button__label > span::after {
content: attr(data-text);
position: absolute;
top: 100%;
left: 0;
}
.kodza-fill-button.is-hover .kodza-fill-button__label > span,
.kodza-fill-button:focus-visible .kodza-fill-button__label > span {
transform: translateY(-100%);
}
/* Optional arrow: it leaves on the right and comes back from the left */
.kodza-fill-button__icon {
position: relative;
display: block;
width: 1em;
height: 1em;
overflow: hidden;
}
.kodza-fill-button__icon svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
transition: transform var(--kz-fill-duration) var(--kz-fill-ease);
}
.kodza-fill-button__icon svg + svg {
transform: translateX(-110%);
}
.kodza-fill-button.is-hover .kodza-fill-button__icon svg,
.kodza-fill-button:focus-visible .kodza-fill-button__icon svg {
transform: translateX(110%);
}
.kodza-fill-button.is-hover .kodza-fill-button__icon svg + svg,
.kodza-fill-button:focus-visible .kodza-fill-button__icon svg + svg {
transform: translateX(0);
}
/* Solid variant: the button starts filled, the circle brings the accent color */
.kodza-fill-button--solid {
--kz-fill-bg: #0a0a0a;
--kz-fill-fg: #ffffff;
--kz-fill-border: transparent;
--kz-fill-fill: #e9e9e6;
--kz-fill-fill-fg: #0a0a0a;
}
/* Without JavaScript: the button simply swaps colors on hover */
.kodza-fill-button:not(.is-ready):hover {
background: var(--kz-fill-fill);
color: var(--kz-fill-fill-fg);
}
@media (prefers-reduced-motion: reduce) {
.kodza-fill-button,
.kodza-fill-button__fill,
.kodza-fill-button__label > span,
.kodza-fill-button__icon svg {
transition-duration: 0.01ms;
}
}
/* Theming example — on a dark background:
.dark-section .kodza-fill-button {
--kz-fill-fg: #ffffff;
--kz-fill-border: rgba(255, 255, 255, 0.85);
--kz-fill-fill: #ffffff;
--kz-fill-fill-fg: #0a0a0a;
}
*/
/* kodza-guard d60305b52cb0 — 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-fill-button):not(#_) {
outline: revert;
outline-offset: revert;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-fill-button:not(#_) {
list-style: revert;
box-sizing: revert;
appearance: revert;
max-height: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
min-height: revert;
}
.kodza-fill-button__content:not(#_),
.kodza-fill-button__label:not(#_),
.kodza-fill-button__fill:not(#_),
.kodza-fill-button__icon:not(#_),
.kodza-fill-button__label span:where(:not([class*="kodza-"])):not(#_) {
box-sizing: revert;
}
.kodza-fill-button__icon svg:where(:not([class*="kodza-"])):not(#_) {
border: revert;
margin: revert;
padding: revert;
border-radius: revert;
outline: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: 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-fill-button__icon svg:where(:not([class*="kodza-"])):not(#_):where(:not([hidden])) {
display: revert;
}
.kodza-fill-button__icon svg:where(:not([class*="kodza-"])):not(#_):where([hidden]) {
display: none;
}
/* 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-fill-button:not(#_) {
position: relative;
display: inline-grid;
height: var(--kz-fill-height);
padding: 0;
border: 0;
border-radius: var(--kz-fill-radius);
background: var(--kz-fill-bg);
box-shadow: inset 0 0 0 1.5px var(--kz-fill-border);
color: var(--kz-fill-fg);
font: inherit;
font-weight: 500;
letter-spacing: -0.01em;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
overflow: hidden;
isolation: isolate;
-webkit-tap-highlight-color: transparent;
transition: transform 0.25s var(--kz-fill-ease);
}
.kodza-fill-button:active:not(#_) {
transform: scale(0.97);
}
.kodza-fill-button:focus-visible:not(#_) {
outline: 2px solid var(--kz-fill-fill);
outline-offset: 3px;
}
.kodza-fill-button__icon svg:not(#_) {
position: absolute;
transition: transform var(--kz-fill-duration) var(--kz-fill-ease);
}
.kodza-fill-button:not(.is-ready):hover:not(#_) {
background: var(--kz-fill-fill);
color: var(--kz-fill-fill-fg);
}
@media (prefers-reduced-motion: reduce) {
.kodza-fill-button:not(#_) {
transition-duration: 0.01ms;
}
}
@media (prefers-reduced-motion: reduce) {
.kodza-fill-button__icon svg:not(#_) {
transition-duration: 0.01ms;
}
}
// Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license
// Kodza — Fill Button
// Prepares every .kodza-fill-button (a copy of its content for the fill, a rolling label)
// and follows where the pointer enters and leaves.
(() => {
const SELECTOR = '.kodza-fill-button';
// One instance. Everything it starts is tied to `signal`, so kodza:destroy stops it all
function mount(button, signal) {
// 1. The original content moves into a resting layer
const content = document.createElement('span');
content.className = 'kodza-fill-button__content';
content.append(...button.childNodes);
button.append(content);
// 2. Each label gets a copy of itself for the roll
content.querySelectorAll('.kodza-fill-button__label').forEach((label) => {
const text = label.textContent.trim();
label.innerHTML = '';
const line = document.createElement('span');
line.dataset.text = text;
line.textContent = text;
label.append(line);
});
// 3. The arrow, if any, is doubled so it can loop
content.querySelectorAll('.kodza-fill-button__icon').forEach((icon) => {
const svg = icon.querySelector('svg');
if (svg) icon.append(svg.cloneNode(true));
});
// 4. The filled layer is a copy, hidden from screen readers
const fill = content.cloneNode(true);
fill.className = 'kodza-fill-button__fill';
fill.setAttribute('aria-hidden', 'true');
button.append(fill);
// 5. The circle grows from the entry point and closes toward the exit point
const track = (event) => {
const rect = button.getBoundingClientRect();
button.style.setProperty('--kz-x', `${((event.clientX - rect.left) / rect.width) * 100}%`);
button.style.setProperty('--kz-y', `${((event.clientY - rect.top) / rect.height) * 100}%`);
};
button.addEventListener('pointerenter', (event) => {
track(event);
button.classList.add('is-hover');
}, { signal });
button.addEventListener('pointerleave', (event) => {
track(event);
button.classList.remove('is-hover');
}, { signal });
// With the keyboard, the fill starts from the center
button.addEventListener('focus', () => {
if (!button.matches(':hover')) {
button.style.setProperty('--kz-x', '50%');
button.style.setProperty('--kz-y', '50%');
}
}, { 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 — Fill Button, for React. From https://kodza.dev/components/button-fill/
// 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: button-fill.css and button-fill.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 './button-fill.css';
type Variant = "solid";
export type FillButtonProps = Omit<ComponentPropsWithoutRef<'a'>, "children" | "variant" | "vars"> & {
/** Modifier class(es): .kodza-fill-button--solid */
variant?: Variant | Variant[];
/** Settings, as CSS variables on the root */
vars?: {
/** default: #0a0a0a */
"--kz-fill-fg"?: string | number;
/** default: transparent */
"--kz-fill-bg"?: string | number;
/** default: rgba(10, 10, 10, 0.9) */
"--kz-fill-border"?: string | number;
/** default: #0a0a0a */
"--kz-fill-fill"?: string | number;
/** default: #ffffff */
"--kz-fill-fill-fg"?: string | number;
/** default: 999px */
"--kz-fill-radius"?: string | number;
/** default: 52px */
"--kz-fill-height"?: string | number;
/** default: 28px */
"--kz-fill-pad-x"?: string | number;
/** default: 10px */
"--kz-fill-gap"?: string | number;
/** default: 0.65s */
"--kz-fill-duration"?: string | number;
/** default: cubic-bezier(0.22, 1, 0.36, 1) */
"--kz-fill-ease"?: string | number;
};
/** The text, plain: the script reads it and rebuilds the markup around it */
children?: string;
className?: string;
};
export function FillButton({ variant, vars, children = "Copy. Paste. Ship.", className, style, ...rest }: FillButtonProps) {
const ref = useRef<ComponentRef<'a'>>(null);
const classes = ["kodza-fill-button", ...([] as string[]).concat(variant ?? []).map((v) => `kodza-fill-button--${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, children, 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('./button-fill.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 (
<a href="#" key={key} {...rest} className={classes} style={{ ...vars, ...style } as CSSProperties} ref={ref}>
<span className="kodza-fill-button__label">{children}</span>
</a>
);
}
Checked
Robots poked at it before you did. Every component goes through the same checks, and again each time it changes.