Carousel
A native scroll-snap carousel that works before the script loads. Then: page buttons, a rolling count, a progress bar or dots, and mouse drag with a throw. Never steals the wheel.
View codeFull-bleed slides that change with a curtain while the title rises from a mask. Horizontal or vertical, buttons, arrow keys and swipe. No GSAP.
With your AI
Say it to your AI. It gets the exact code below, not its best guess, and knows where it goes.
Make my homepage hero a Kodza Fullscreen Slider.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<section class="kodza-slider" aria-roledescription="carousel" aria-label="Kodza slides">
<div class="kodza-slider__slides">
<figure class="kodza-slider__slide">
<img src="https://kodza.dev/media/kz-columns-2400.webp" alt="A row of square concrete pillars under a flat roof, casting striped shadows" />
<figcaption>
<h2 class="kodza-slider__title">repeat(7, 1fr)</h2>
<p class="kodza-slider__text">Grid layout, 1968 edition.</p>
</figcaption>
</figure>
<figure class="kodza-slider__slide">
<img src="https://kodza.dev/media/kz-steps-2400.webp" alt="A concrete staircase that climbs nine steps and stops in mid-air" loading="lazy" />
<figcaption>
<h2 class="kodza-slider__title">transition: steps(9)</h2>
<p class="kodza-slider__text">Goes up. Goes nowhere. Ships anyway.</p>
</figcaption>
</figure>
<figure class="kodza-slider__slide">
<img src="https://kodza.dev/media/kz-float-2400.webp" alt="A concrete block hovering above the ground, over its own shadow" loading="lazy" />
<figcaption>
<h2 class="kodza-slider__title">position: absolute</h2>
<p class="kodza-slider__text">Out of the flow, and loving it.</p>
</figcaption>
</figure>
<figure class="kodza-slider__slide">
<img src="https://kodza.dev/media/kz-arch-2400.webp" alt="A concrete wall with a round-topped opening, sunlight pouring through it onto the ground" loading="lazy" />
<figcaption>
<h2 class="kodza-slider__title">border-radius: 50%</h2>
<p class="kodza-slider__text">Even concrete has a soft side.</p>
</figcaption>
</figure>
</div>
<div class="kodza-slider__bar">
<span class="kodza-slider__count"><b>01</b> / 04</span>
<div class="kodza-slider__nav">
<button type="button" data-dir="-1" aria-label="Previous slide"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M10 3L5 8l5 5" /></svg></button>
<button type="button" data-dir="1" aria-label="Next slide"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6 3l5 5-5 5" /></svg></button>
</div>
</div>
</section>
/* Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license */
/* Kodza — Fullscreen Slider
Full-bleed slides that change with a curtain: the next image wipes in while the
current one is pushed out by the same edge, and the title rises from behind a mask.
Horizontal by default, vertical with data-direction="vertical".
It fills its container (give it a height, or let it take the screen).
Everything is tuned with the --kz-slider-* variables. */
.kodza-slider {
--kz-slider-height: 100vh;
--kz-slider-fg: #ffffff;
--kz-slider-shade: rgba(0, 0, 0, 0.38);
--kz-slider-duration: 1s;
--kz-slider-ease: cubic-bezier(0.76, 0, 0.24, 1);
position: relative;
height: var(--kz-slider-height);
height: min(var(--kz-slider-height), 100dvh);
overflow: hidden;
background: #0a0a0a;
color: var(--kz-slider-fg);
touch-action: pan-y;
}
.kodza-slider[data-direction="vertical"] {
touch-action: pan-x;
}
.kodza-slider__slides,
.kodza-slider__slide {
position: absolute;
inset: 0;
margin: 0;
}
.kodza-slider__slide {
overflow: hidden;
visibility: hidden;
}
.kodza-slider__slide.is-current,
.kodza-slider__slide.is-leaving {
visibility: visible;
}
.kodza-slider__slide img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
/* A soft shade at the bottom, so the caption reads on any image */
.kodza-slider__slide::after {
content: "";
position: absolute;
inset: 45% 0 0;
background: linear-gradient(transparent, var(--kz-slider-shade));
pointer-events: none;
}
.kodza-slider__slide figcaption {
position: absolute;
z-index: 1;
left: clamp(20px, 5vw, 56px);
bottom: clamp(84px, 13vh, 120px);
max-width: min(640px, 80%);
}
.kodza-slider__title,
.kodza-slider__text {
margin: 0;
overflow: hidden;
}
.kodza-slider__title {
font-size: clamp(30px, 5vw, 64px);
font-weight: 600;
line-height: 1.05;
letter-spacing: -0.04em;
}
.kodza-slider__text {
margin-top: 10px;
font-size: clamp(15px, 1.4vw, 18px);
opacity: 0.8;
}
/* Each caption line rises from behind its own mask */
.kodza-slider__line {
display: block;
}
.kodza-slider__bar {
position: absolute;
z-index: 2;
right: clamp(20px, 5vw, 56px);
bottom: clamp(24px, 5vh, 44px);
left: clamp(20px, 5vw, 56px);
display: flex;
align-items: center;
justify-content: space-between;
}
.kodza-slider__count {
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 12px;
opacity: 0.75;
}
.kodza-slider__count b {
font-weight: 500;
opacity: 1;
}
.kodza-slider__nav {
display: flex;
gap: 8px;
}
.kodza-slider__nav button {
display: grid;
place-items: center;
width: 46px;
height: 46px;
padding: 0;
border: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.12);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.24);
color: inherit;
cursor: pointer;
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
transition: background-color 0.25s ease;
}
.kodza-slider__nav button:hover {
background: rgba(255, 255, 255, 0.24);
}
.kodza-slider__nav button:focus-visible {
outline: 2px solid var(--kz-slider-fg);
outline-offset: 3px;
}
.kodza-slider__nav svg {
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
}
/* Vertical: the arrows point up and down */
.kodza-slider[data-direction="vertical"] .kodza-slider__nav svg {
transform: rotate(90deg);
}
/* Without the script, the first slide is simply shown */
.kodza-slider:not(.is-ready) .kodza-slider__slide:first-child {
visibility: visible;
}
/* kodza-guard e1a12e0299f1 — 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-slider__nav button:where(:not([class*="kodza-"]))):not(#_) {
outline: revert;
outline-offset: revert;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-slider:not(#_),
.kodza-slider__slides:not(#_),
.kodza-slider__line:not(#_),
.kodza-slider__bar:not(#_),
.kodza-slider__count:not(#_),
.kodza-slider__nav:not(#_) {
box-sizing: revert;
}
.kodza-slider__slide:not(#_) {
border: 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-slider__title:not(#_) {
border: 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-style: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
}
.kodza-slider__text:not(#_) {
border: 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-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-slider__slide img:where(:not([class*="kodza-"]):not(.kodza-slider__slide [class*="kodza-"] *)):not(#_),
.kodza-slider__nav 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;
}
.kodza-slider__slide figcaption:where(:not([class*="kodza-"]):not(.kodza-slider__slide [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;
background-color: revert;
background-image: revert;
box-shadow: revert;
outline-offset: revert;
appearance: 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-slider__count b: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-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-slider__nav button:where(:not([class*="kodza-"])):not(#_) {
margin: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: 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-slider__slide:not(#_):where(:not([hidden])),
.kodza-slider__title:not(#_):where(:not([hidden])),
.kodza-slider__text:not(#_):where(:not([hidden])),
.kodza-slider__slide figcaption:where(:not([class*="kodza-"]):not(.kodza-slider__slide [class*="kodza-"] *)):not(#_):where(:not([hidden])),
.kodza-slider__count b:where(:not([class*="kodza-"])):not(#_):where(:not([hidden])),
.kodza-slider__nav svg:where(:not([class*="kodza-"])):not(#_):where(:not([hidden])) {
display: revert;
}
.kodza-slider__slide:not(#_):where([hidden]),
.kodza-slider__title:not(#_):where([hidden]),
.kodza-slider__text:not(#_):where([hidden]),
.kodza-slider__slide figcaption:where(:not([class*="kodza-"]):not(.kodza-slider__slide [class*="kodza-"] *)):not(#_):where([hidden]),
.kodza-slider__count b:where(:not([class*="kodza-"])):not(#_):where([hidden]),
.kodza-slider__nav svg:where(:not([class*="kodza-"])):not(#_):where([hidden]) {
display: none;
}
.kodza-slider__slide: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-slider__slide:not(#_) {
position: absolute;
margin: 0;
}
.kodza-slider__slide img:not(#_) {
display: block;
}
.kodza-slider__slide figcaption:not(#_) {
position: absolute;
max-width: min(640px, 80%);
}
.kodza-slider__title:not(#_),
.kodza-slider__text:not(#_) {
margin: 0;
}
.kodza-slider__title:not(#_) {
font-size: clamp(30px, 5vw, 64px);
font-weight: 600;
line-height: 1.05;
letter-spacing: -0.04em;
}
.kodza-slider__text:not(#_) {
margin-top: 10px;
font-size: clamp(15px, 1.4vw, 18px);
}
.kodza-slider__count b:not(#_) {
font-weight: 500;
}
.kodza-slider__nav button:not(#_) {
display: grid;
place-items: center;
width: 46px;
height: 46px;
padding: 0;
border: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.12);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.24);
color: inherit;
cursor: pointer;
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
transition: background-color 0.25s ease;
}
.kodza-slider__nav button:hover:not(#_) {
background: rgba(255, 255, 255, 0.24);
}
.kodza-slider__nav button:focus-visible:not(#_) {
outline: 2px solid var(--kz-slider-fg);
outline-offset: 3px;
}
// Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license
// Kodza — Fullscreen Slider
// Curtain transitions between full-bleed slides: the next one wipes in from an edge while
// the current one is pushed out by the same edge, each image drifting inside its own
// frame. Buttons, arrow keys (when the slider has focus or the pointer) and swipe.
(() => {
const SELECTOR = '.kodza-slider';
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const pad = (n) => String(n).padStart(2, '0');
// One instance. Everything it starts is tied to `signal`, so kodza:destroy stops it all
function mount(root, signal) {
const slides = [...root.querySelectorAll('.kodza-slider__slide')];
const count = root.querySelector('.kodza-slider__count b');
if (slides.length < 2) return;
const vertical = root.dataset.direction === 'vertical';
const style = getComputedStyle(root);
const duration = reduceMotion ? 0 : parseFloat(style.getPropertyValue('--kz-slider-duration')) * 1000 || 1000;
const ease = style.getPropertyValue('--kz-slider-ease').trim() || 'ease';
let current = 0;
let busy = false;
// Caption lines get their own mask, so they can rise into place
slides.forEach((slide) => {
slide.querySelectorAll('.kodza-slider__title, .kodza-slider__text').forEach((el) => {
const line = document.createElement('span');
line.className = 'kodza-slider__line';
line.append(...el.childNodes);
el.append(line);
});
});
slides[0].classList.add('is-current');
root.classList.add('is-ready');
// Clip-path insets for each edge: [top, right, bottom, left]
const inset = (side, amount) => {
const v = ['0', '0', '0', '0'];
v[side] = amount;
return `inset(${v.join(' ')})`;
};
async function go(dir) {
if (busy) return;
busy = true;
const from = slides[current];
current = (current + dir + slides.length) % slides.length;
const to = slides[current];
const img = to.querySelector('img');
img.loading = 'eager';
await img.decode().catch(() => {});
// Forward: the new slide comes from the right (or the bottom) and the old one leaves by the left (or the top)
const enterSide = vertical ? (dir > 0 ? 0 : 2) : dir > 0 ? 3 : 1;
const leaveSide = vertical ? (dir > 0 ? 2 : 0) : dir > 0 ? 1 : 3;
const axis = vertical ? 'Y' : 'X';
const timing = { duration, easing: ease, fill: 'forwards' };
from.classList.replace('is-current', 'is-leaving');
to.classList.add('is-current');
to.style.zIndex = 1;
from.style.zIndex = 0;
if (count) count.textContent = pad(current + 1);
const wipeIn = to.animate([{ clipPath: inset(enterSide, '100%') }, { clipPath: inset(enterSide, '0%') }], timing);
to.querySelector('img').animate([{ transform: `translate${axis}(${dir * 14}%) scale(1.1)` }, { transform: 'none' }], timing);
from.animate([{ clipPath: inset(leaveSide, '0%') }, { clipPath: inset(leaveSide, '100%') }], timing);
from.querySelector('img').animate([{ transform: 'none' }, { transform: `translate${axis}(${-dir * 18}%)` }], timing);
to.querySelectorAll('.kodza-slider__line').forEach((line, i) => {
line.animate([{ transform: 'translateY(105%)' }, { transform: 'none' }], {
duration: duration * 0.8,
delay: duration * 0.45 + i * 90,
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
fill: 'backwards',
});
});
await wipeIn.finished.catch(() => {});
from.classList.remove('is-leaving');
[from, to].forEach((slide) => {
slide.getAnimations({ subtree: true }).forEach((a) => a.cancel());
slide.style.zIndex = '';
});
busy = false;
}
root.querySelectorAll('.kodza-slider__nav button').forEach((button) =>
button.addEventListener('click', () => go(Number(button.dataset.dir)), { signal })
);
// Arrow keys work while the pointer is over the slider or the focus is inside it
let pointerInside = false;
root.addEventListener('pointerenter', () => (pointerInside = true), { signal });
root.addEventListener('pointerleave', () => (pointerInside = false), { signal });
document.addEventListener('keydown', (event) => {
if (!pointerInside && !root.contains(document.activeElement)) return;
const next = vertical ? 'ArrowDown' : 'ArrowRight';
const prev = vertical ? 'ArrowUp' : 'ArrowLeft';
if (event.key === next || event.key === prev) {
event.preventDefault();
go(event.key === next ? 1 : -1);
}
}, { signal });
// Swipe along the slider's own direction
let start = null;
root.addEventListener('pointerdown', (event) => (start = { x: event.clientX, y: event.clientY }), { signal });
root.addEventListener('pointerup', (event) => {
if (!start) return;
const delta = vertical ? event.clientY - start.y : event.clientX - start.x;
start = null;
if (Math.abs(delta) > 50) go(delta < 0 ? 1 : -1);
}, { 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 — Fullscreen Slider, for React. From https://kodza.dev/components/slider-horizontal-fullscreen/
// 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: slider-horizontal-fullscreen.css and slider-horizontal-fullscreen.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 './slider-horizontal-fullscreen.css';
export type FullscreenSliderProps = Omit<ComponentPropsWithoutRef<'section'>, "children" | "vars"> & {
/** Settings, as CSS variables on the root */
vars?: {
/** default: 100vh */
"--kz-slider-height"?: string | number;
/** default: #ffffff */
"--kz-slider-fg"?: string | number;
/** default: rgba(0, 0, 0, 0.38) */
"--kz-slider-shade"?: string | number;
/** default: 1s */
"--kz-slider-duration"?: string | number;
/** default: cubic-bezier(0.76, 0, 0.24, 1) */
"--kz-slider-ease"?: string | number;
};
className?: string;
};
export function FullscreenSlider({ vars, className, style, ...rest }: FullscreenSliderProps) {
const ref = useRef<ComponentRef<'section'>>(null);
const classes = ["kodza-slider", 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, 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('./slider-horizontal-fullscreen.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 (
<section aria-roledescription="carousel" aria-label="Kodza slides" key={key} {...rest} className={classes} style={{ ...vars, ...style } as CSSProperties} ref={ref}>
<div className="kodza-slider__slides">
<figure className="kodza-slider__slide">
<img src="https://kodza.dev/media/kz-columns-2400.webp" alt="A row of square concrete pillars under a flat roof, casting striped shadows" />
<figcaption>
<h2 className="kodza-slider__title">repeat(7, 1fr)</h2>
<p className="kodza-slider__text">Grid layout, 1968 edition.</p>
</figcaption>
</figure>
<figure className="kodza-slider__slide">
<img src="https://kodza.dev/media/kz-steps-2400.webp" alt="A concrete staircase that climbs nine steps and stops in mid-air" loading="lazy" />
<figcaption>
<h2 className="kodza-slider__title">transition: steps(9)</h2>
<p className="kodza-slider__text">Goes up. Goes nowhere. Ships anyway.</p>
</figcaption>
</figure>
<figure className="kodza-slider__slide">
<img src="https://kodza.dev/media/kz-float-2400.webp" alt="A concrete block hovering above the ground, over its own shadow" loading="lazy" />
<figcaption>
<h2 className="kodza-slider__title">position: absolute</h2>
<p className="kodza-slider__text">Out of the flow, and loving it.</p>
</figcaption>
</figure>
<figure className="kodza-slider__slide">
<img src="https://kodza.dev/media/kz-arch-2400.webp" alt="A concrete wall with a round-topped opening, sunlight pouring through it onto the ground" loading="lazy" />
<figcaption>
<h2 className="kodza-slider__title">border-radius: 50%</h2>
<p className="kodza-slider__text">Even concrete has a soft side.</p>
</figcaption>
</figure>
</div>
<div className="kodza-slider__bar">
<span className="kodza-slider__count"><b>01</b> / 04</span>
<div className="kodza-slider__nav">
<button type="button" data-dir="-1" aria-label="Previous slide">
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M10 3L5 8l5 5" />
</svg>
</button>
{' '}
<button type="button" data-dir="1" aria-label="Next slide">
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M6 3l5 5-5 5" />
</svg>
</button>
</div>
</div>
</section>
);
}
Checked
Robots poked at it before you did. Every component goes through the same checks, and again each time it changes.