Hero Expand
A big title with a small picture set between two words. On scroll the scene pins, the picture opens to full screen and pushes the words aside, then a caption and a button rise. Scroll back up: it rewinds.
View codeA split hero, words on the left and a 4:5 picture on the right: the title lines rise out of their masks while the picture opens from the top edge 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 Hero Split to my page.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<section class="kodza-hero-split">
<div class="kodza-hero-split__inner">
<div class="kodza-hero-split__copy">
<p class="kodza-hero-split__eyebrow"><span class="kodza-hero-split__rise">v2.0 · now with fewer bugs</span></p>
<h1 class="kodza-hero-split__title"><span class="kodza-hero-split__line"><span class="kodza-hero-split__rise">Ship the page,</span></span> <span class="kodza-hero-split__line"><span class="kodza-hero-split__rise">not the meeting.</span></span></h1>
<p class="kodza-hero-split__lead">Forty sections, one paste each. Your client edits the words, you keep the weekend.</p>
<div class="kodza-hero-split__actions">
<a class="kodza-hero-split__button kodza-hero-split__button--ink" href="#">Get the code<svg class="kodza-hero-split__arrow" viewBox="0 0 16 16" aria-hidden="true"><path d="M2.5 8h10.5M9 3.5 13.5 8 9 12.5" /></svg></a>
<a class="kodza-hero-split__button kodza-hero-split__button--soft" href="#">See it live</a>
</div>
</div>
<figure class="kodza-hero-split__media">
<div class="kodza-hero-split__frame"><div class="kodza-hero-split__curtain"><div class="kodza-hero-split__hold"><img src="https://kodza.dev/media/kz-block-1400.webp" alt="Concrete blocks stacked under a flat light" width="1120" height="1400" /></div></div></div>
<figcaption class="kodza-hero-split__caption">fig. 01 — poured in Python, not stock</figcaption>
</figure>
</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 — Hero Split
A split hero: a tight headline and two pill buttons on the left, a 4:5 picture on the right.
On arrival the lines rise out of their masks while the picture opens from the top edge down.
Everything is tuned with the --kz-hero-split-* variables. */
.kodza-hero-split {
--kz-hero-split-ink: #0a0a0a;
--kz-hero-split-paper: #f2f2f0;
--kz-hero-split-dim: rgba(10, 10, 10, 0.62);
--kz-hero-split-faint: rgba(10, 10, 10, 0.6);
--kz-hero-split-soft: rgba(10, 10, 10, 0.07);
--kz-hero-split-bg: transparent;
--kz-hero-split-width: 1240px;
--kz-hero-split-gap: clamp(32px, 5vw, 80px);
--kz-hero-split-pad-y: clamp(72px, 9vw, 140px);
--kz-hero-split-gutter: clamp(16px, 4vw, 40px);
--kz-hero-split-radius: 16px;
--kz-hero-split-ratio: 4 / 5;
--kz-hero-split-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;
--kz-hero-split-ease: cubic-bezier(0.22, 1, 0.36, 1);
container: kodza-hero-split / inline-size;
box-sizing: border-box;
width: 100%;
padding-block: var(--kz-hero-split-pad-y);
padding-left: max(var(--kz-hero-split-gutter), env(safe-area-inset-left, 0px));
padding-right: max(var(--kz-hero-split-gutter), env(safe-area-inset-right, 0px));
background: var(--kz-hero-split-bg);
color: var(--kz-hero-split-ink);
}
.kodza-hero-split__inner {
display: grid;
grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
gap: var(--kz-hero-split-gap);
align-items: center;
max-width: var(--kz-hero-split-width);
margin: 0 auto;
}
.kodza-hero-split__copy {
min-width: 0;
}
.kodza-hero-split__eyebrow {
margin: 0 0 22px;
overflow: hidden;
font-family: var(--kz-hero-split-mono);
font-size: 12.5px;
font-weight: 500;
letter-spacing: 0.02em;
color: var(--kz-hero-split-faint);
}
.kodza-hero-split__title {
margin: 0 0 26px;
font-size: clamp(40px, 6.2vw, 92px);
font-weight: 700;
line-height: 1;
letter-spacing: -0.05em;
color: var(--kz-hero-split-ink);
}
/* Each line is its own mask: the words rise from behind its bottom edge */
.kodza-hero-split__line {
display: block;
overflow: hidden;
padding-bottom: 0.08em;
margin-bottom: -0.08em;
}
.kodza-hero-split__rise {
display: block;
}
.kodza-hero-split__lead {
max-width: 30ch;
margin: 0 0 36px;
font-size: clamp(17px, 1.45vw, 20px);
line-height: 1.55;
color: var(--kz-hero-split-dim);
text-wrap: pretty;
}
.kodza-hero-split__actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.kodza-hero-split__button {
position: relative;
isolation: isolate;
overflow: hidden;
box-sizing: border-box;
display: inline-flex;
align-items: center;
gap: 10px;
height: 52px;
padding: 0 26px;
border: 0;
border-radius: 999px;
font-family: inherit;
font-size: 15px;
font-weight: 500;
line-height: 1;
white-space: nowrap;
text-decoration: none;
cursor: pointer;
}
.kodza-hero-split__button:focus-visible {
outline: 2px solid var(--kz-hero-split-ink);
outline-offset: 3px;
}
/* Solid: the arrow steps forward on hover */
.kodza-hero-split__button--ink {
background: var(--kz-hero-split-ink);
color: var(--kz-hero-split-paper);
}
.kodza-hero-split__arrow {
flex: none;
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform 0.3s var(--kz-hero-split-ease);
}
/* Soft: ink rises from the bottom edge on hover */
.kodza-hero-split__button--soft {
background: var(--kz-hero-split-soft);
color: var(--kz-hero-split-ink);
transition: color 0.4s var(--kz-hero-split-ease);
}
.kodza-hero-split__button--soft::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
border-radius: inherit;
background: var(--kz-hero-split-ink);
transform: translateY(101%);
transition: transform 0.4s var(--kz-hero-split-ease);
}
@media (hover: hover) {
.kodza-hero-split__button--ink:hover .kodza-hero-split__arrow {
transform: translateX(4px);
}
.kodza-hero-split__button--soft:hover {
color: var(--kz-hero-split-paper);
}
.kodza-hero-split__button--soft:hover::before {
transform: none;
}
}
.kodza-hero-split__media {
min-width: 0;
margin: 0;
}
/* The curtain: the middle layer slides down from above while the inner one slides up into
place, so the picture is uncovered from the top edge down and never moves itself */
.kodza-hero-split__frame {
max-width: 100%;
aspect-ratio: var(--kz-hero-split-ratio);
overflow: hidden;
border-radius: var(--kz-hero-split-radius);
}
.kodza-hero-split__curtain,
.kodza-hero-split__hold {
height: 100%;
}
.kodza-hero-split__curtain {
overflow: hidden;
}
.kodza-hero-split__frame img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.kodza-hero-split__caption {
margin-top: 12px;
font-family: var(--kz-hero-split-mono);
font-size: 12.5px;
color: var(--kz-hero-split-faint);
}
/* Waiting for its entrance: the script sets is-shown and plays the motion in the same frame */
@media (prefers-reduced-motion: no-preference) {
.kodza-hero-split.is-ready:not(.is-shown) .kodza-hero-split__rise {
transform: translateY(105%);
}
.kodza-hero-split.is-ready:not(.is-shown) .kodza-hero-split__curtain {
transform: translateY(-100%);
}
.kodza-hero-split.is-ready:not(.is-shown) .kodza-hero-split__hold {
transform: translateY(100%);
}
.kodza-hero-split.is-ready:not(.is-shown) .kodza-hero-split__lead,
.kodza-hero-split.is-ready:not(.is-shown) .kodza-hero-split__actions,
.kodza-hero-split.is-ready:not(.is-shown) .kodza-hero-split__caption {
opacity: 0;
transform: translateY(20px);
}
}
/* One column: the picture goes under the words, wider than tall, and the buttons share the row */
@container kodza-hero-split (max-width: 700px) {
.kodza-hero-split__inner {
grid-template-columns: minmax(0, 1fr);
}
.kodza-hero-split__frame {
aspect-ratio: 4 / 3;
}
.kodza-hero-split__button {
flex: 1 1 auto;
justify-content: center;
}
}
@media (prefers-reduced-motion: reduce) {
.kodza-hero-split *,
.kodza-hero-split *::before,
.kodza-hero-split *::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}
/* kodza-guard a4026e78c4cd — 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-hero-split__eyebrow):not(#_),
:where(.kodza-hero-split__title):not(#_),
:where(.kodza-hero-split__lead):not(#_),
:where(.kodza-hero-split__media):not(#_),
:where(.kodza-hero-split__caption):not(#_),
:where(.kodza-hero-split__hold img:where(:not([class*="kodza-"]))):not(#_) {
transition-duration: revert;
}
:where(.kodza-hero-split__button):not(#_) {
outline: revert;
transition: revert;
outline-offset: revert;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-hero-split__inner:not(#_),
.kodza-hero-split__copy:not(#_),
.kodza-hero-split__rise:not(#_),
.kodza-hero-split__line:not(#_),
.kodza-hero-split__actions:not(#_),
.kodza-hero-split__frame:not(#_),
.kodza-hero-split__curtain:not(#_),
.kodza-hero-split__hold:not(#_) {
box-sizing: revert;
}
.kodza-hero-split__eyebrow:not(#_) {
border: 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;
font-style: revert;
line-height: 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;
transition-delay: revert;
}
.kodza-hero-split__title:not(#_) {
border: 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;
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;
transition-property: revert;
transition-timing-function: revert;
transition-delay: revert;
}
.kodza-hero-split__lead:not(#_) {
border: 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-height: revert;
font-family: revert;
font-weight: revert;
font-style: 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;
transition-delay: revert;
}
.kodza-hero-split__button:not(#_) {
margin: revert;
list-style: revert;
float: revert;
box-shadow: revert;
appearance: revert;
max-width: revert;
max-height: revert;
font-style: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
}
.kodza-hero-split__arrow: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-hero-split__media:not(#_) {
border: 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;
transition-delay: revert;
}
.kodza-hero-split__caption:not(#_) {
border: revert;
padding: revert;
border-radius: revert;
outline: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: revert;
margin-right: revert;
margin-bottom: revert;
margin-left: revert;
background-color: revert;
background-image: revert;
box-shadow: revert;
outline-offset: revert;
appearance: revert;
max-width: revert;
max-height: 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;
transition-delay: revert;
}
.kodza-hero-split__hold img: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;
transition-delay: revert;
}
.kodza-hero-split__eyebrow:not(#_):where(:not([hidden])),
.kodza-hero-split__title:not(#_):where(:not([hidden])),
.kodza-hero-split__lead:not(#_):where(:not([hidden])),
.kodza-hero-split__arrow:not(#_):where(:not([hidden])),
.kodza-hero-split__media:not(#_):where(:not([hidden])),
.kodza-hero-split__caption:not(#_):where(:not([hidden])) {
display: revert;
}
.kodza-hero-split__eyebrow:not(#_):where([hidden]),
.kodza-hero-split__title:not(#_):where([hidden]),
.kodza-hero-split__lead:not(#_):where([hidden]),
.kodza-hero-split__arrow:not(#_):where([hidden]),
.kodza-hero-split__media:not(#_):where([hidden]),
.kodza-hero-split__caption: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-hero-split__eyebrow:not(#_) {
margin: 0 0 22px;
font-family: var(--kz-hero-split-mono);
font-size: 12.5px;
font-weight: 500;
letter-spacing: 0.02em;
color: var(--kz-hero-split-faint);
}
.kodza-hero-split__title:not(#_) {
margin: 0 0 26px;
font-size: clamp(40px, 6.2vw, 92px);
font-weight: 700;
line-height: 1;
letter-spacing: -0.05em;
color: var(--kz-hero-split-ink);
}
.kodza-hero-split__lead:not(#_) {
max-width: 30ch;
margin: 0 0 36px;
font-size: clamp(17px, 1.45vw, 20px);
line-height: 1.55;
color: var(--kz-hero-split-dim);
}
.kodza-hero-split__button:not(#_) {
position: relative;
box-sizing: border-box;
display: inline-flex;
padding: 0 26px;
border: 0;
border-radius: 999px;
font-family: inherit;
font-size: 15px;
font-weight: 500;
line-height: 1;
text-decoration: none;
}
.kodza-hero-split__button:focus-visible:not(#_) {
outline: 2px solid var(--kz-hero-split-ink);
outline-offset: 3px;
}
.kodza-hero-split__button--ink:not(#_) {
background: var(--kz-hero-split-ink);
color: var(--kz-hero-split-paper);
}
.kodza-hero-split__arrow:not(#_) {
transition: transform 0.3s var(--kz-hero-split-ease);
}
.kodza-hero-split__button--soft:not(#_) {
background: var(--kz-hero-split-soft);
color: var(--kz-hero-split-ink);
transition: color 0.4s var(--kz-hero-split-ease);
}
@media (hover: hover) {
.kodza-hero-split__button--soft:hover:not(#_) {
color: var(--kz-hero-split-paper);
}
}
.kodza-hero-split__media:not(#_) {
margin: 0;
}
.kodza-hero-split__frame img:not(#_) {
display: block;
}
.kodza-hero-split__caption:not(#_) {
margin-top: 12px;
font-family: var(--kz-hero-split-mono);
font-size: 12.5px;
color: var(--kz-hero-split-faint);
}
@media (prefers-reduced-motion: reduce) {
.kodza-hero-split *:where(p, h1, a, svg, figure, img, figcaption):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 — Hero Split
// Plays the entrance once the section is in view, its picture decoded and the fonts in: the
// label and title lines rise out of their masks, the picture opens from the top edge down, then
// the intro, buttons and caption follow. "kodza:replay" plays it again.
(() => {
const SELECTOR = '.kodza-hero-split';
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const GLIDE = 'cubic-bezier(0.6, 0, 0.1, 1)'; // slow start, long settle
const SOFT = 'cubic-bezier(0.25, 1, 0.5, 1)';
const decoded = (root) => Promise.all([...root.querySelectorAll('img')].map((img) => img.decode().catch(() => {})));
// One instance, with its own state: two copies on a page never interfere. Everything it
// starts is tied to `signal` (listeners get { signal }, observers and timers stop on its
// 'abort' event), so kodza:destroy stops it all.
function mount(root, signal) {
if (reduceMotion) return root.classList.add('is-shown');
const q = (s) => root.querySelector(s);
let running = [];
const stop = () => {
running.forEach((a) => a.cancel());
running = [];
};
const animate = (el, keyframes, options) => {
if (el) running.push(el.animate(keyframes, { easing: GLIDE, fill: 'backwards', ...options }));
};
const rise = (el, delay, duration) => animate(el, [{ transform: 'translateY(105%)' }, { transform: 'none' }], { duration, delay });
const play = async () => {
await decoded(root);
if (signal.aborted) return;
stop();
root.classList.add('is-shown');
rise(q('.kodza-hero-split__eyebrow .kodza-hero-split__rise'), 0, 1000);
root.querySelectorAll('.kodza-hero-split__line .kodza-hero-split__rise').forEach((line, i) => rise(line, 110 + i * 125, 1170));
// The curtain: both layers on transforms, the picture settles from a slight zoom
animate(q('.kodza-hero-split__curtain'), [{ transform: 'translateY(-100%)' }, { transform: 'none' }], { duration: 1350, delay: 200 });
animate(q('.kodza-hero-split__hold'), [{ transform: 'translateY(100%)' }, { transform: 'none' }], { duration: 1350, delay: 200 });
animate(q('.kodza-hero-split__frame img'), [{ transform: 'scale(1.2)' }, { transform: 'none' }], { duration: 2000, delay: 200, easing: SOFT });
[q('.kodza-hero-split__lead'), q('.kodza-hero-split__actions'), q('.kodza-hero-split__caption')].forEach((el, i) => {
animate(el, [{ opacity: 0, transform: 'translateY(20px)' }, { opacity: 1, transform: 'none' }], { duration: 900, delay: 700 + i * 110, easing: SOFT });
});
};
root.addEventListener('kodza:replay', play, { signal });
signal.addEventListener('abort', stop);
const io = new IntersectionObserver((entries) => {
if (!entries.some((entry) => entry.isIntersecting)) return;
io.disconnect();
play();
}, { rootMargin: '0px 0px -15% 0px' });
signal.addEventListener('abort', () => io.disconnect());
Promise.all([document.fonts?.ready, decoded(root)]).then(() => {
if (!signal.aborted) io.observe(root);
});
}
// 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 — Hero Split, for React. From https://kodza.dev/components/hero-split/
// 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: hero-split.css and hero-split.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 './hero-split.css';
export type HeroSplitProps = Omit<ComponentPropsWithoutRef<'section'>, "children" | "vars"> & {
/** Settings, as CSS variables on the root */
vars?: {
/** default: #0a0a0a */
"--kz-hero-split-ink"?: string | number;
/** default: #f2f2f0 */
"--kz-hero-split-paper"?: string | number;
/** default: rgba(10, 10, 10, 0.62) */
"--kz-hero-split-dim"?: string | number;
/** default: rgba(10, 10, 10, 0.6) */
"--kz-hero-split-faint"?: string | number;
/** default: rgba(10, 10, 10, 0.07) */
"--kz-hero-split-soft"?: string | number;
/** default: transparent */
"--kz-hero-split-bg"?: string | number;
/** default: 1240px */
"--kz-hero-split-width"?: string | number;
/** default: clamp(32px, 5vw, 80px) */
"--kz-hero-split-gap"?: string | number;
/** default: clamp(72px, 9vw, 140px) */
"--kz-hero-split-pad-y"?: string | number;
/** default: clamp(16px, 4vw, 40px) */
"--kz-hero-split-gutter"?: string | number;
/** default: 16px */
"--kz-hero-split-radius"?: string | number;
/** default: 4 / 5 */
"--kz-hero-split-ratio"?: string | number;
/** default: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace */
"--kz-hero-split-mono"?: string | number;
/** default: cubic-bezier(0.22, 1, 0.36, 1) */
"--kz-hero-split-ease"?: string | number;
};
className?: string;
};
export function HeroSplit({ vars, className, style, ...rest }: HeroSplitProps) {
const ref = useRef<ComponentRef<'section'>>(null);
const classes = ["kodza-hero-split", 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('./hero-split.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 key={key} {...rest} className={classes} style={{ ...vars, ...style } as CSSProperties} ref={ref}>
<div className="kodza-hero-split__inner">
<div className="kodza-hero-split__copy">
<p className="kodza-hero-split__eyebrow">
<span className="kodza-hero-split__rise">v2.0 · now with fewer bugs</span>
</p>
<h1 className="kodza-hero-split__title">
<span className="kodza-hero-split__line">
<span className="kodza-hero-split__rise">Ship the page,</span>
</span>
{' '}
<span className="kodza-hero-split__line">
<span className="kodza-hero-split__rise">not the meeting.</span>
</span>
</h1>
<p className="kodza-hero-split__lead">Forty sections, one paste each. Your client edits the words, you keep the weekend.</p>
<div className="kodza-hero-split__actions">
<a className="kodza-hero-split__button kodza-hero-split__button--ink" href="#">Get the code<svg className="kodza-hero-split__arrow" viewBox="0 0 16 16" aria-hidden="true"> <path d="M2.5 8h10.5M9 3.5 13.5 8 9 12.5" /> </svg></a>
{' '}
<a className="kodza-hero-split__button kodza-hero-split__button--soft" href="#">See it live</a>
</div>
</div>
<figure className="kodza-hero-split__media">
<div className="kodza-hero-split__frame">
<div className="kodza-hero-split__curtain">
<div className="kodza-hero-split__hold">
<img src="https://kodza.dev/media/kz-block-1400.webp" alt="Concrete blocks stacked under a flat light" width="1120" height="1400" />
</div>
</div>
</div>
<figcaption className="kodza-hero-split__caption">fig. 01 — poured in Python, not stock</figcaption>
</figure>
</div>
</section>
);
}
Checked
Robots poked at it before you did. Every component goes through the same checks, and again each time it changes.