Horizontal Scroll
A section that pins itself while its cards slide sideways as you scroll down. Pictures drift in their frames, the counter rolls. A swipe on touch.
View codeA round button that shows up a screen down, its ring filling as you read. Its label unrolls on hover, the arrow takes off when you click.
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 Back to Top button to my WordPress blog.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<div class="kodza-top">
<button class="kodza-top__button" type="button" aria-label="Back to top">
<svg class="kodza-top__ring" viewBox="0 0 48 48" aria-hidden="true"><circle class="kodza-top__track" cx="24" cy="24" r="22.5"/><circle class="kodza-top__fill" cx="24" cy="24" r="22.5" pathLength="1"/></svg>
<span class="kodza-top__arrow" aria-hidden="true"><svg viewBox="0 0 16 16"><path d="M8 13V3M3.5 7.5 8 3l4.5 4.5"/></svg></span>
<span class="kodza-top__label" aria-hidden="true">Back to top</span>
</button>
</div>
/* Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license */
/* Kodza — Back to Top
A round button that shows up once the visitor is a screen down, its ring filling as they
read. On hover its label unrolls beside it and the arrow lifts; on click the arrow takes off
and comes back from below while the page glides up. Place it at the end of the page: it
stays at the bottom of the screen (position: sticky), on the right.
Everything is tuned with the --kz-top-* variables. */
.kodza-top {
--kz-top-bg: #ffffff;
--kz-top-fg: #0a0a0a;
--kz-top-line: rgba(10, 10, 10, 0.1);
--kz-top-shadow: 0 10px 30px -10px rgba(10, 10, 10, 0.3);
--kz-top-size: 48px;
--kz-top-gap: 24px;
--kz-top-ease: cubic-bezier(0.22, 1, 0.36, 1);
--kz-top-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
position: sticky;
bottom: var(--kz-top-gap);
z-index: 40;
display: flex;
box-sizing: border-box;
justify-content: flex-end;
height: 0;
padding-right: var(--kz-top-gap);
pointer-events: none;
}
.kodza-top__button {
position: relative;
display: grid;
place-items: center;
width: var(--kz-top-size);
height: var(--kz-top-size);
padding: 0;
border: 0;
border-radius: 50%;
background: var(--kz-top-bg);
box-shadow: var(--kz-top-shadow);
color: var(--kz-top-fg);
cursor: pointer;
opacity: 0;
transform: translateY(calc(-100% + 12px)) scale(0.6);
visibility: hidden;
transition:
opacity 0.25s ease,
transform 0.35s ease-in,
visibility 0s linear 0.35s;
}
/* Shown once the visitor is a screen down */
.kodza-top.is-shown .kodza-top__button {
opacity: 1;
transform: translateY(-100%);
visibility: visible;
pointer-events: auto;
transition:
opacity 0.3s ease,
transform 0.6s var(--kz-top-spring),
visibility 0s;
}
.kodza-top__button:focus-visible {
outline: 2px solid var(--kz-top-fg);
outline-offset: 3px;
}
/* The ring fills with the page */
.kodza-top__ring {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.kodza-top__ring circle {
fill: none;
stroke-width: 2;
}
.kodza-top__track {
stroke: var(--kz-top-line);
}
.kodza-top__fill {
stroke: var(--kz-top-fg);
stroke-dasharray: 1;
stroke-dashoffset: calc(1 - var(--kz-top-progress, 0));
stroke-linecap: round;
}
/* The arrow: lifts on hover, takes off on click and comes back from below */
.kodza-top__arrow {
display: grid;
width: 16px;
height: 16px;
overflow: hidden;
}
.kodza-top__arrow svg {
grid-area: 1 / 1;
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform 0.4s var(--kz-top-ease);
}
.kodza-top__button:hover .kodza-top__arrow svg {
transform: translateY(-2px);
}
.kodza-top.is-flying .kodza-top__arrow svg {
animation: kodza-top-fly 0.7s var(--kz-top-ease);
}
@keyframes kodza-top-fly {
45% {
transform: translateY(-120%);
}
46% {
transform: translateY(120%);
}
100% {
transform: none;
}
}
/* The label unrolls beside the button, from the button outwards */
.kodza-top__label {
position: absolute;
top: 50%;
right: calc(100% + 8px);
padding: 8px 14px;
border-radius: 999px;
background: var(--kz-top-fg);
color: var(--kz-top-bg);
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 12px;
white-space: nowrap;
clip-path: inset(0 0 0 100% round 999px);
transform: translate(8px, -50%);
transition:
clip-path 0.3s var(--kz-top-ease),
transform 0.3s var(--kz-top-ease);
pointer-events: none;
}
.kodza-top__button:hover .kodza-top__label,
.kodza-top__button:focus-visible .kodza-top__label {
clip-path: inset(0 0 0 0 round 999px);
transform: translate(0, -50%);
transition-duration: 0.45s;
}
@media (hover: none) {
.kodza-top__label {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.kodza-top__button,
.kodza-top.is-shown .kodza-top__button,
.kodza-top__arrow svg,
.kodza-top__label {
transition-duration: 0.01ms !important;
}
.kodza-top.is-flying .kodza-top__arrow svg {
animation: none;
}
}
/* Theming example — on a dark background:
.dark-section .kodza-top {
--kz-top-bg: #f2f2f0;
--kz-top-fg: #0a0a0a;
--kz-top-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.8);
}
*/
/* kodza-guard fdde43541006 — 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-top__button):not(#_) {
outline: revert;
outline-offset: revert;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-top__button:not(#_) {
margin: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: 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-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
white-space: revert;
min-height: revert;
}
.kodza-top__ring: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;
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-top__track:not(#_),
.kodza-top__fill:not(#_),
.kodza-top__arrow:not(#_),
.kodza-top__label:not(#_) {
box-sizing: revert;
}
.kodza-top__arrow 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;
}
.kodza-top__ring:not(#_):where(:not([hidden])),
.kodza-top__arrow svg:where(:not([class*="kodza-"])):not(#_):where(:not([hidden])) {
display: revert;
}
.kodza-top__ring:not(#_):where([hidden]),
.kodza-top__arrow 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-top__button:not(#_) {
position: relative;
display: grid;
place-items: center;
width: var(--kz-top-size);
height: var(--kz-top-size);
padding: 0;
border: 0;
border-radius: 50%;
background: var(--kz-top-bg);
box-shadow: var(--kz-top-shadow);
color: var(--kz-top-fg);
cursor: pointer;
opacity: 0;
transform: translateY(calc(-100% + 12px)) scale(0.6);
visibility: hidden;
transition: opacity 0.25s ease,
transform 0.35s ease-in,
visibility 0s linear 0.35s;
}
.kodza-top.is-shown .kodza-top__button:not(#_) {
opacity: 1;
transform: translateY(-100%);
visibility: visible;
pointer-events: auto;
transition: opacity 0.3s ease,
transform 0.6s var(--kz-top-spring),
visibility 0s;
}
.kodza-top__button:focus-visible:not(#_) {
outline: 2px solid var(--kz-top-fg);
outline-offset: 3px;
}
.kodza-top__ring:not(#_) {
position: absolute;
}
.kodza-top__arrow svg:not(#_) {
transition: transform 0.4s var(--kz-top-ease);
}
@media (prefers-reduced-motion: reduce) {
.kodza-top__button:not(#_),
.kodza-top.is-shown .kodza-top__button:not(#_) {
transition-duration: 0.01ms !important;
}
}
@media (prefers-reduced-motion: reduce) {
.kodza-top__arrow svg: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 — Back to Top
// Shows the button once the page has scrolled most of a screen (and while it rides the bottom
// of the screen), fills its ring with the
// reading progress, and glides back to the top on click (the arrow takes off and lands).
(() => {
const SELECTOR = '.kodza-top';
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// One instance. Everything it starts is tied to `signal`, so kodza:destroy stops it all
function mount(root, signal) {
const update = () => {
const max = document.documentElement.scrollHeight - innerHeight;
const progress = max > 0 ? Math.min(1, window.scrollY / max) : 0;
const far = window.scrollY > innerHeight * 0.8;
root.style.setProperty('--kz-top-progress', progress.toFixed(4));
// Only while it rides the bottom of the screen: resting at the end of its section
// (mid-screen), it steps aside
const gap = parseFloat(getComputedStyle(root).bottom) || 0;
const top = root.getBoundingClientRect().top;
const riding = top >= innerHeight - gap - 2 && top <= innerHeight + 2;
root.classList.toggle('is-shown', far && riding);
};
const button = root.querySelector('.kodza-top__button');
button?.addEventListener('click', () => {
root.classList.remove('is-flying');
void root.offsetWidth;
root.classList.add('is-flying');
window.scrollTo({ top: 0, behavior: reduceMotion ? 'auto' : 'smooth' });
}, { signal });
root.addEventListener('animationend', () => root.classList.remove('is-flying'), { signal });
let ticking = false;
window.addEventListener('scroll', () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
ticking = false;
if (!signal.aborted) update();
});
}, { passive: true, signal });
window.addEventListener('resize', update, { signal });
update();
}
// 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 — Back to Top, for React. From https://kodza.dev/components/back-to-top/
// 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: back-to-top.css and back-to-top.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 './back-to-top.css';
export type BackToTopProps = Omit<ComponentPropsWithoutRef<'div'>, "children" | "vars"> & {
/** Settings, as CSS variables on the root */
vars?: {
/** default: #ffffff */
"--kz-top-bg"?: string | number;
/** default: #0a0a0a */
"--kz-top-fg"?: string | number;
/** default: rgba(10, 10, 10, 0.1) */
"--kz-top-line"?: string | number;
/** default: 0 10px 30px -10px rgba(10, 10, 10, 0.3) */
"--kz-top-shadow"?: string | number;
/** default: 48px */
"--kz-top-size"?: string | number;
/** default: 24px */
"--kz-top-gap"?: string | number;
/** default: cubic-bezier(0.22, 1, 0.36, 1) */
"--kz-top-ease"?: string | number;
/** default: cubic-bezier(0.34, 1.56, 0.64, 1) */
"--kz-top-spring"?: string | number;
};
/** The text, plain: the script reads it and rebuilds the markup around it */
children?: string;
className?: string;
};
export function BackToTop({ vars, children = "Back to top", className, style, ...rest }: BackToTopProps) {
const ref = useRef<ComponentRef<'div'>>(null);
const classes = ["kodza-top", 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('./back-to-top.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 key={key} {...rest} className={classes} style={{ ...vars, ...style } as CSSProperties} ref={ref}>
<button className="kodza-top__button" type="button" aria-label="Back to top">
<svg className="kodza-top__ring" viewBox="0 0 48 48" aria-hidden="true">
<circle className="kodza-top__track" cx="24" cy="24" r="22.5" />
<circle className="kodza-top__fill" cx="24" cy="24" r="22.5" pathLength="1" />
</svg>
{' '}
<span className="kodza-top__arrow" aria-hidden="true">
<svg viewBox="0 0 16 16">
<path d="M8 13V3M3.5 7.5 8 3l4.5 4.5" />
</svg>
</span>
{' '}
<span className="kodza-top__label" aria-hidden="true">{children}</span>
</button>
</div>
);
}
Checked
Robots poked at it before you did. Every component goes through the same checks, and again each time it changes.