Alert
A message in the page: note, success, warning or error, told apart by the icon, not by color. The icon draws itself, a dismissed alert folds away and an error unfolds its log.
View codeA bar or a ring driven by data-value: the percentage rolls like an odometer, a segment slides while nobody knows, and at 100 a tick draws itself.
With your AI
Say it to your AI. It gets the exact code below, not its best guess, and knows where it goes.
Show my upload progress with a Kodza Progress bar.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<div class="kodza-progress" role="progressbar" aria-label="Uploading hero.webp" data-value="0" data-done="hero.webp is live">
<div class="kodza-progress__head">
<span class="kodza-progress__label">Uploading hero.webp</span>
<span class="kodza-progress__value">0%</span>
</div>
<div class="kodza-progress__track"><span class="kodza-progress__fill"></span></div>
</div>
<div class="kodza-progress" role="progressbar" aria-label="Waiting for a CI runner">
<div class="kodza-progress__head">
<span class="kodza-progress__label">Waiting for a CI runner</span>
<span class="kodza-progress__value"></span>
</div>
<div class="kodza-progress__track"><span class="kodza-progress__fill"></span></div>
</div>
<div class="kodza-progress kodza-progress--ring" role="progressbar" aria-label="Tests passing" data-value="0" data-done="All green">
<div class="kodza-progress__dial">
<svg class="kodza-progress__ring" viewBox="0 0 48 48" aria-hidden="true"><circle cx="24" cy="24" r="21" /><circle cx="24" cy="24" r="21" pathLength="100" /></svg>
<span class="kodza-progress__value">0%</span>
</div>
<span class="kodza-progress__label">Tests passing</span>
</div>
/* Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license */
/* Kodza — Progress
A bar or a ring, driven by data-value (0–100; leave it out for "we don't know yet"). The
percentage rolls digit by digit like an odometer; at 100 it gives way to a tick that draws
itself, and the label rolls to data-done if you set one. Without a value, a segment slides
along the bar (or an arc turns around the ring). Change data-value from your code, the
component follows. Everything is tuned with the --kz-progress-* variables. */
.kodza-progress {
--kz-progress-fg: #0a0a0a;
--kz-progress-muted: rgba(10, 10, 10, 0.62);
--kz-progress-track: rgba(10, 10, 10, 0.09);
--kz-progress-fill: #0a0a0a;
--kz-progress-height: 6px;
--kz-progress-dial: 104px;
--kz-progress-ease: cubic-bezier(0.22, 1, 0.36, 1);
--kz-progress-v: 0;
display: grid;
gap: 12px;
color: var(--kz-progress-fg);
font-size: 15px;
line-height: 1.3;
}
.kodza-progress__head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16px;
}
/* The label, and the words it rolls to once done (one cell: the layout never moves) */
.kodza-progress__label {
display: inline-grid;
min-width: 0;
overflow: hidden;
padding-block: 0.12em;
margin-block: -0.12em;
font-weight: 500;
}
.kodza-progress__label > span {
grid-area: 1 / 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: transform 0.55s var(--kz-progress-ease), opacity 0.3s ease;
}
.kodza-progress__label > span:nth-child(2) {
opacity: 0;
transform: translateY(105%);
}
.kodza-progress.is-complete .kodza-progress__label > span:first-child {
opacity: 0;
transform: translateY(-105%);
}
.kodza-progress.is-complete .kodza-progress__label > span:nth-child(2) {
opacity: 1;
transform: none;
transition-delay: 0.1s;
}
/* The value: rolling digits, then a tick */
.kodza-progress__value {
display: inline-grid;
justify-items: end;
overflow: hidden;
padding-block: 0.12em;
margin-block: -0.12em;
color: var(--kz-progress-muted);
font-variant-numeric: tabular-nums;
font-weight: 500;
}
.kodza-progress__value > * {
display: inline-flex;
grid-area: 1 / 1;
align-items: center;
transition: transform 0.55s var(--kz-progress-ease), opacity 0.3s ease;
}
.kodza-progress__digit {
display: inline-block;
height: 1.3em;
overflow: hidden;
line-height: 1.3;
}
.kodza-progress__digit.is-off {
width: 0;
opacity: 0;
}
.kodza-progress__digit > span {
display: block;
transform: translateY(calc(var(--kz-progress-d, 0) * -10%));
transition: transform 0.5s var(--kz-progress-ease);
}
.kodza-progress__digit > span > span {
display: block;
height: 1.3em;
}
.kodza-progress__tick {
color: var(--kz-progress-fg);
opacity: 0;
transform: translateY(105%);
}
.kodza-progress__tick svg {
width: 1.1em;
height: 1.1em;
fill: none;
stroke: currentColor;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
.kodza-progress__tick path {
stroke-dasharray: 1;
stroke-dashoffset: 1;
transition: stroke-dashoffset 0.2s ease;
}
.kodza-progress.is-complete .kodza-progress__number {
opacity: 0;
transform: translateY(-105%);
}
.kodza-progress.is-complete .kodza-progress__tick {
opacity: 1;
transform: none;
transition-delay: 0.1s;
}
.kodza-progress.is-complete .kodza-progress__tick path {
stroke-dashoffset: 0;
transition: stroke-dashoffset 0.4s var(--kz-progress-ease) 0.18s;
}
.kodza-progress.is-indeterminate .kodza-progress__value {
visibility: hidden;
}
/* The bar: a full-width fill slid in from the left, so its rounded end keeps its shape */
.kodza-progress__track {
position: relative;
height: var(--kz-progress-height);
overflow: hidden;
border-radius: 999px;
background: var(--kz-progress-track);
}
.kodza-progress__fill {
position: absolute;
inset: 0;
border-radius: inherit;
background: var(--kz-progress-fill);
transform: translateX(calc((var(--kz-progress-v) - 100) * 1%));
transition: transform 0.6s var(--kz-progress-ease);
}
.kodza-progress.is-indeterminate .kodza-progress__fill {
right: auto;
width: 38%;
transform: translateX(-100%);
transition: none;
animation: kodza-progress-slide 1.5s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}
@keyframes kodza-progress-slide {
to {
transform: translateX(265%);
}
}
/* The ring: the value in the middle, the label below */
.kodza-progress--ring {
justify-items: center;
gap: 14px;
}
.kodza-progress__dial {
display: grid;
place-items: center;
width: var(--kz-progress-dial);
height: var(--kz-progress-dial);
}
.kodza-progress__dial > * {
grid-area: 1 / 1;
}
.kodza-progress__ring {
width: 100%;
height: 100%;
overflow: visible;
fill: none;
stroke-width: 3;
stroke-linecap: round;
transform: rotate(-90deg);
}
.kodza-progress__ring circle:first-child {
stroke: var(--kz-progress-track);
}
.kodza-progress__ring circle:last-child {
stroke: var(--kz-progress-fill);
stroke-dasharray: var(--kz-progress-v) 100;
transition: stroke-dasharray 0.6s var(--kz-progress-ease), opacity 0.2s ease;
}
.kodza-progress[data-value="0"] .kodza-progress__ring circle:last-child {
opacity: 0;
}
.kodza-progress--ring .kodza-progress__value {
justify-items: center;
color: var(--kz-progress-fg);
font-size: 22px;
font-weight: 600;
letter-spacing: -0.02em;
}
.kodza-progress--ring .kodza-progress__tick svg {
width: 30px;
height: 30px;
stroke-width: 1.6;
}
.kodza-progress--ring .kodza-progress__label {
color: var(--kz-progress-muted);
font-size: 14px;
}
.kodza-progress--ring .kodza-progress__label > span {
justify-self: center;
}
.kodza-progress--ring.is-indeterminate .kodza-progress__ring {
animation: kodza-progress-turn 1.4s linear infinite;
}
.kodza-progress--ring.is-indeterminate .kodza-progress__ring circle:last-child {
stroke-dasharray: 28 100;
opacity: 1;
}
@keyframes kodza-progress-turn {
to {
transform: rotate(270deg);
}
}
@media (prefers-reduced-motion: reduce) {
.kodza-progress *,
.kodza-progress__value > * {
transition-duration: 0.01ms !important;
transition-delay: 0s !important;
}
.kodza-progress.is-indeterminate .kodza-progress__fill,
.kodza-progress--ring.is-indeterminate .kodza-progress__ring {
animation: none;
}
.kodza-progress.is-indeterminate .kodza-progress__fill {
transform: translateX(80%);
}
}
/* Theming example — on a dark background:
.dark-section .kodza-progress {
--kz-progress-fg: #f2f2f0;
--kz-progress-muted: rgba(242, 242, 240, 0.62);
--kz-progress-track: rgba(255, 255, 255, 0.12);
--kz-progress-fill: #f2f2f0;
}
*/
/* kodza-guard c0f0bf4d362f — 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-progress__ring):not(#_),
:where(.kodza-progress__tick svg:where(:not([class*="kodza-"]))):not(#_) {
transition-duration: revert;
transition-delay: revert;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-progress:not(#_),
.kodza-progress__head:not(#_),
.kodza-progress__label:not(#_),
.kodza-progress__value:not(#_),
.kodza-progress__number:not(#_),
.kodza-progress__digit:not(#_),
.kodza-progress__tick:not(#_),
.kodza-progress__track:not(#_),
.kodza-progress__fill:not(#_),
.kodza-progress__dial:not(#_),
.kodza-progress__label span:where(:not([class*="kodza-"])):not(#_),
.kodza-progress__digit span:where(:not([class*="kodza-"])):not(#_) {
box-sizing: revert;
}
.kodza-progress__ring:not(#_),
.kodza-progress__tick 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;
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;
transition-property: revert;
transition-timing-function: revert;
}
.kodza-progress__ring:not(#_):where(:not([hidden])),
.kodza-progress__tick svg:where(:not([class*="kodza-"])):not(#_):where(:not([hidden])) {
display: revert;
}
.kodza-progress__ring:not(#_):where([hidden]),
.kodza-progress__tick 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) */
@media (prefers-reduced-motion: reduce) {
.kodza-progress *:where(svg):not(#_) {
transition-duration: 0.01ms !important;
transition-delay: 0s !important;
}
}
// Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license
// Kodza — Progress
// Follows data-value (0–100, or none for indeterminate): fills the bar or the ring, rolls the
// digits of the percentage, keeps aria-valuenow in step, and marks .is-complete at 100.
// From your code: el.dataset.value = 42 (or el.removeAttribute('data-value')).
(() => {
const SELECTOR = '.kodza-progress';
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const ease = 'cubic-bezier(0.22, 1, 0.36, 1)';
const make = (tag, className) => Object.assign(document.createElement(tag), className ? { className } : {});
// One instance. Everything it starts is tied to `signal`, so kodza:destroy stops it all
function mount(root, signal) {
const value = root.querySelector('.kodza-progress__value');
const label = root.querySelector('.kodza-progress__label');
if (!value) return;
root.setAttribute('aria-valuemin', '0');
root.setAttribute('aria-valuemax', '100');
// The percentage: three columns of digits (0 to 9, stacked) and a tick for the end
const number = make('span', 'kodza-progress__number');
const columns = [0, 1, 2].map(() => {
const column = make('span', 'kodza-progress__digit');
const strip = make('span');
for (let d = 0; d < 10; d++) strip.append(Object.assign(make('span'), { textContent: d }));
column.append(strip);
return column;
});
number.append(...columns, '%');
const tick = make('span', 'kodza-progress__tick');
tick.innerHTML = '<svg viewBox="0 0 16 16"><path d="M3.5 8.4l2.9 2.8 6.1-6.4" pathLength="1"/></svg>';
value.replaceChildren(number, tick);
value.setAttribute('aria-hidden', 'true');
// The words it rolls to once done
if (label && root.dataset.done) {
const now = Object.assign(make('span'), { textContent: label.textContent.trim() });
const done = Object.assign(make('span'), { textContent: root.dataset.done });
label.replaceChildren(now, done);
}
let settle = 0;
const render = (first) => {
const raw = root.getAttribute('data-value');
const unknown = raw === null || raw === '' || Number.isNaN(parseFloat(raw));
root.classList.toggle('is-indeterminate', unknown);
if (unknown) {
root.removeAttribute('aria-valuenow');
clearTimeout(settle);
root.classList.remove('is-complete');
return;
}
const v = Math.round(Math.min(100, Math.max(0, parseFloat(raw))));
root.style.setProperty('--kz-progress-v', v);
root.setAttribute('aria-valuenow', v);
const digits = String(v).padStart(3, ' ');
columns.forEach((column, i) => {
const on = digits[i] !== ' ';
column.style.setProperty('--kz-progress-d', on ? digits[i] : 0);
if (on === !column.classList.contains('is-off')) return;
// A column that comes or goes eases its width: the number never jumps
const from = column.offsetWidth;
column.classList.toggle('is-off', !on);
const to = column.offsetWidth;
if (!first && !reduceMotion) column.animate([{ width: `${from}px` }, { width: `${to}px` }], { duration: 450, easing: ease });
});
// 100% lands first, then gives way to the tick
clearTimeout(settle);
if (v < 100) root.classList.remove('is-complete');
else if (first || reduceMotion) root.classList.add('is-complete');
else settle = setTimeout(() => root.classList.add('is-complete'), 550);
};
render(true);
const observer = new MutationObserver(() => render(false));
observer.observe(root, { attributes: true, attributeFilter: ['data-value'] });
signal.addEventListener('abort', () => {
observer.disconnect();
clearTimeout(settle);
});
}
// 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 — Progress, for React. From https://kodza.dev/components/progress-bar/
// 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: progress-bar.css and progress-bar.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 './progress-bar.css';
type Variant = "ring";
export type ProgressProps = Omit<ComponentPropsWithoutRef<'div'>, "children" | "variant" | "vars" | "value" | "done"> & {
/** Modifier class(es): .kodza-progress--ring */
variant?: Variant | Variant[];
/** data-value on the root (default: 0) */
value?: "0";
/** data-done on the root (default: hero.webp is live) */
done?: "hero.webp is live" | "All green";
/** Settings, as CSS variables on the root */
vars?: {
/** default: #0a0a0a */
"--kz-progress-fg"?: string | number;
/** default: rgba(10, 10, 10, 0.62) */
"--kz-progress-muted"?: string | number;
/** default: rgba(10, 10, 10, 0.09) */
"--kz-progress-track"?: string | number;
/** default: #0a0a0a */
"--kz-progress-fill"?: string | number;
/** default: 6px */
"--kz-progress-height"?: string | number;
/** default: 104px */
"--kz-progress-dial"?: string | number;
/** default: cubic-bezier(0.22, 1, 0.36, 1) */
"--kz-progress-ease"?: string | number;
};
className?: string;
};
export function Progress({ variant, value = "0", done = "hero.webp is live", vars, className, style, ...rest }: ProgressProps) {
const ref = useRef<ComponentRef<'div'>>(null);
const classes = ["kodza-progress", ...([] as string[]).concat(variant ?? []).map((v) => `kodza-progress--${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, value, done, 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('./progress-bar.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="progressbar" aria-label="Uploading hero.webp" key={key} {...rest} className={classes} data-value={value} data-done={done} style={{ ...vars, ...style } as CSSProperties} ref={ref}>
<div className="kodza-progress__head">
<span className="kodza-progress__label">Uploading hero.webp</span>
{' '}
<span className="kodza-progress__value">0%</span>
</div>
<div className="kodza-progress__track">
<span className="kodza-progress__fill" />
</div>
</div>
);
}
Checked
Robots poked at it before you did. Every component goes through the same checks, and again each time it changes.