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 with real thickness: the face sinks into its base, even the quickest tap goes all the way down, and it springs back with a small bounce. A keycap, a brutalist slab, or a toggle that stays down.
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 3D Press Button to my page.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<button class="kodza-press-button" type="button">
<span class="kodza-press-button__face">Deploy on Friday</span>
</button>
<button class="kodza-press-button kodza-press-button--offset" type="button">
<span class="kodza-press-button__face">Push to prod</span>
</button>
<button class="kodza-press-button" type="button" aria-pressed="false">
<span class="kodza-press-button__face">
<span class="kodza-press-button__dot" aria-hidden="true"></span>
Mute Slack
</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 — 3D Press Button
A button with real thickness: the face sinks into its base when pressed, every tap goes all
the way down, and it springs back with a small bounce.
Everything is tuned with the --kz-press-* variables. */
.kodza-press-button {
--kz-press-face: #ffffff;
--kz-press-fg: #0a0a0a;
--kz-press-edge: #0a0a0a; /* the thickness, and the outline of the face */
--kz-press-focus: #0a0a0a;
--kz-press-line: 1.5px;
--kz-press-radius: 999px;
--kz-press-height: 52px;
--kz-press-pad-x: 28px;
--kz-press-gap: 10px;
--kz-press-depth-x: 0px;
--kz-press-depth-y: 6px;
--kz-press-lift: calc(1 / 3); /* share of the depth the face rises on hover (2px of 6: whole pixels, crisp text) */
--kz-press-latch: 0.5; /* how far down a latched toggle stays (3px of 6: whole pixels, crisp text) */
--kz-press-down: 0.08s cubic-bezier(0.3, 0, 0.6, 1);
--kz-press-up: 0.45s cubic-bezier(0.3, 1.9, 0.5, 1);
--kz-press-hover: 0.25s cubic-bezier(0.22, 1, 0.36, 1);
position: relative;
display: inline-grid;
box-sizing: border-box;
margin: 0;
/* The room the face travels into */
padding: 0 var(--kz-press-depth-x) var(--kz-press-depth-y) 0;
border: 0;
border-radius: var(--kz-press-radius);
background: none;
color: var(--kz-press-fg);
font: inherit;
font-weight: 500;
line-height: 1.25;
letter-spacing: -0.01em;
text-decoration: none;
white-space: nowrap;
vertical-align: middle;
cursor: pointer;
isolation: isolate;
user-select: none;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
.kodza-press-button:focus-visible {
outline: 2px solid var(--kz-press-focus);
outline-offset: 4px;
}
/* The base: the same shape as the face, pushed down by the depth. What shows below the face
is the thickness. */
.kodza-press-button::before {
content: "";
grid-area: 1 / 1;
z-index: -1;
border-radius: var(--kz-press-radius);
background: var(--kz-press-edge);
transform: translate(var(--kz-press-depth-x), var(--kz-press-depth-y));
}
/* The face is filled with the edge color and its inner layer, cut in by the line width, brings
the face color: the outer edge matches the base, so no hairline shows between them (an inset
shadow leaves one). Both edges of the line are cut by clip-path, which is not snapped to whole
pixels like a border, an inset or a background: the line stays even wherever the face lands */
.kodza-press-button__face {
position: relative;
grid-area: 1 / 1;
display: flex;
align-items: center;
justify-content: center;
gap: var(--kz-press-gap);
box-sizing: border-box;
height: var(--kz-press-height);
padding: 0 var(--kz-press-pad-x);
border-radius: var(--kz-press-radius);
background: var(--kz-press-edge);
box-shadow: 0 0 0 1px var(--kz-press-edge); /* covers the snapped background, the clip cuts it */
clip-path: inset(0 round var(--kz-press-radius));
isolation: isolate;
transition: transform var(--kz-press-hover);
}
.kodza-press-button__face::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
background: var(--kz-press-face);
clip-path: inset(var(--kz-press-line) round calc(var(--kz-press-radius) - var(--kz-press-line)));
}
/* Hover: the face rises a little, the thickness grows */
@media (hover: hover) {
.kodza-press-button:hover .kodza-press-button__face {
transform: translate(calc(var(--kz-press-depth-x) * var(--kz-press-lift) * -1), calc(var(--kz-press-depth-y) * var(--kz-press-lift) * -1));
}
}
/* A latching toggle (aria-pressed="true") stays part of the way down */
.kodza-press-button[aria-pressed="true"] .kodza-press-button__face {
transform: translate(calc(var(--kz-press-depth-x) * var(--kz-press-latch)), calc(var(--kz-press-depth-y) * var(--kz-press-latch)));
}
/* Pressed: the face meets its base, fast */
.kodza-press-button.is-pressed .kodza-press-button__face,
.kodza-press-button:not(.is-ready):active .kodza-press-button__face {
transform: translate(var(--kz-press-depth-x), var(--kz-press-depth-y));
transition: transform var(--kz-press-down);
}
/* Let go: it springs back past its rest, then settles */
.kodza-press-button.is-releasing .kodza-press-button__face {
transition: transform var(--kz-press-up);
}
/* Disabled: flush with its base, it cannot go any lower */
.kodza-press-button:disabled,
.kodza-press-button[aria-disabled="true"] {
cursor: not-allowed;
}
.kodza-press-button:disabled .kodza-press-button__face,
.kodza-press-button[aria-disabled="true"] .kodza-press-button__face {
transform: translate(var(--kz-press-depth-x), var(--kz-press-depth-y));
color: color-mix(in srgb, currentColor 45%, transparent);
}
/* The toggle's light: an empty ring that fills while it is on */
.kodza-press-button__dot {
position: relative;
flex: none;
width: 8px;
height: 8px;
border-radius: 50%;
box-shadow: inset 0 0 0 var(--kz-press-line) currentColor;
}
.kodza-press-button__dot::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: currentColor;
transform: scale(0);
transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.kodza-press-button[aria-pressed="true"] .kodza-press-button__dot::after {
transform: scale(1);
}
/* Offset variant: a brutalist slab, its hard shadow cast down and to the right */
.kodza-press-button--offset {
--kz-press-radius: 12px;
--kz-press-depth-x: 6px;
--kz-press-depth-y: 6px;
}
@media (prefers-reduced-motion: reduce) {
.kodza-press-button__face,
.kodza-press-button__dot::after {
transition-duration: 0.01ms !important;
}
}
/* Theming example — on a dark background:
.dark-section .kodza-press-button {
--kz-press-face: #161616;
--kz-press-fg: #f2f2f0;
--kz-press-edge: #3d3d3c;
--kz-press-focus: #f2f2f0;
}
*/
/* kodza-guard 834988a88cc3 — 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-press-button):not(#_) {
outline: revert;
outline-offset: revert;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-press-button:not(#_) {
transition: revert;
list-style: revert;
box-shadow: revert;
appearance: revert;
max-height: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
min-height: revert;
}
.kodza-press-button__dot:not(#_) {
box-sizing: revert;
}
.kodza-press-button:not(#_)::before,
.kodza-press-button__face:not(#_)::before,
.kodza-press-button__dot: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-press-button:not(#_) {
position: relative;
display: inline-grid;
box-sizing: border-box;
margin: 0;
padding: 0 var(--kz-press-depth-x) var(--kz-press-depth-y) 0;
border: 0;
border-radius: var(--kz-press-radius);
background: none;
color: var(--kz-press-fg);
font: inherit;
font-weight: 500;
line-height: 1.25;
letter-spacing: -0.01em;
text-decoration: none;
white-space: nowrap;
vertical-align: middle;
cursor: pointer;
isolation: isolate;
user-select: none;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
.kodza-press-button:focus-visible:not(#_) {
outline: 2px solid var(--kz-press-focus);
outline-offset: 4px;
}
.kodza-press-button:not(#_)::before {
content: "";
grid-area: 1 / 1;
z-index: -1;
border-radius: var(--kz-press-radius);
background: var(--kz-press-edge);
transform: translate(var(--kz-press-depth-x), var(--kz-press-depth-y));
}
.kodza-press-button:disabled:not(#_),
.kodza-press-button[aria-disabled="true"]:not(#_) {
cursor: not-allowed;
}
// Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license
// Kodza — 3D Press Button
// Presses the face for the pointer, Enter and Space alike, makes every tap go all the way down
// before it springs back, and flips aria-pressed on a latching toggle.
(() => {
const SELECTOR = '.kodza-press-button';
const TRAVEL = 110; // ms: a tap shorter than the way down (80 ms, plus a late frame) still reaches the bottom
const SPRING = 450; // ms: the bounce back, as long as --kz-press-up
// One instance. Everything it starts is tied to `signal`, so kodza:destroy stops it all
function mount(button, signal) {
let downAt = 0;
let timer = 0;
let key = null;
let pointer = null; // listens for the end of a pointer press, wherever the pointer goes
signal.addEventListener('abort', () => {
clearTimeout(timer);
pointer?.abort();
});
const disabled = () => button.disabled || button.getAttribute('aria-disabled') === 'true';
const press = () => {
clearTimeout(timer);
button.classList.remove('is-releasing');
button.classList.add('is-pressed');
downAt = performance.now();
};
const release = () => {
if (!button.classList.contains('is-pressed')) return;
clearTimeout(timer);
// A quick tap still lands at the bottom before the spring sends it back up
timer = setTimeout(() => {
button.classList.remove('is-pressed');
button.classList.add('is-releasing');
timer = setTimeout(() => button.classList.remove('is-releasing'), SPRING);
}, Math.max(0, TRAVEL - (performance.now() - downAt)));
};
button.addEventListener('pointerdown', (event) => {
if (event.button !== 0 || disabled()) return;
press();
pointer?.abort();
pointer = new AbortController();
const end = () => {
pointer.abort();
release();
};
document.addEventListener('pointerup', end, { signal: pointer.signal });
document.addEventListener('pointercancel', end, { signal: pointer.signal });
}, { signal });
// The keyboard presses it too: Enter on links and buttons, Space on buttons
button.addEventListener('keydown', (event) => {
if (event.repeat || key || disabled()) return;
if (event.key === 'Enter' || (event.key === ' ' && button.localName === 'button')) {
key = event.key;
press();
}
}, { signal });
button.addEventListener('keyup', (event) => {
if (event.key !== key) return;
key = null;
release();
}, { signal });
button.addEventListener('blur', () => {
if (!key) return;
key = null;
release();
}, { signal });
// A latching toggle: each press flips it, and the face stays down while it is on
if (button.hasAttribute('aria-pressed')) {
button.addEventListener('click', () => {
if (disabled()) return;
button.setAttribute('aria-pressed', String(button.getAttribute('aria-pressed') !== 'true'));
}, { 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 — 3D Press Button, for React. From https://kodza.dev/components/press-button/
// 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: press-button.css and press-button.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 './press-button.css';
type Variant = "offset";
export type 3DPressButtonProps = Omit<ComponentPropsWithoutRef<'button'>, "children" | "variant" | "vars"> & {
/** Modifier class(es): .kodza-press-button--offset */
variant?: Variant | Variant[];
/** Settings, as CSS variables on the root */
vars?: {
/** default: #ffffff */
"--kz-press-face"?: string | number;
/** default: #0a0a0a */
"--kz-press-fg"?: string | number;
/** the thickness, and the outline of the face — default: #0a0a0a */
"--kz-press-edge"?: string | number;
/** default: #0a0a0a */
"--kz-press-focus"?: string | number;
/** default: 1.5px */
"--kz-press-line"?: string | number;
/** default: 999px */
"--kz-press-radius"?: string | number;
/** default: 52px */
"--kz-press-height"?: string | number;
/** default: 28px */
"--kz-press-pad-x"?: string | number;
/** default: 10px */
"--kz-press-gap"?: string | number;
/** default: 0px */
"--kz-press-depth-x"?: string | number;
/** default: 6px */
"--kz-press-depth-y"?: string | number;
/** share of the depth the face rises on hover (2px of 6: whole pixels, crisp text) — default: calc(1 / 3) */
"--kz-press-lift"?: string | number;
/** how far down a latched toggle stays (3px of 6: whole pixels, crisp text) — default: 0.5 */
"--kz-press-latch"?: string | number;
/** default: 0.08s cubic-bezier(0.3, 0, 0.6, 1) */
"--kz-press-down"?: string | number;
/** default: 0.45s cubic-bezier(0.3, 1.9, 0.5, 1) */
"--kz-press-up"?: string | number;
/** default: 0.25s cubic-bezier(0.22, 1, 0.36, 1) */
"--kz-press-hover"?: string | number;
};
/** The text, plain: the script reads it and rebuilds the markup around it */
children?: string;
className?: string;
};
export function 3DPressButton({ variant, vars, children = "Deploy on Friday", className, style, ...rest }: 3DPressButtonProps) {
const ref = useRef<ComponentRef<'button'>>(null);
const classes = ["kodza-press-button", ...([] as string[]).concat(variant ?? []).map((v) => `kodza-press-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('./press-button.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 (
<button type="button" key={key} {...rest} className={classes} style={{ ...vars, ...style } as CSSProperties} ref={ref}>
<span className="kodza-press-button__face">{children}</span>
</button>
);
}
Checked
Robots poked at it before you did. Every component goes through the same checks, and again each time it changes.