Hero Collage
A centred hero with pictures scattered around the title: they open one after the other, then follow the mouse a little, each at its own depth.
View codeA showreel hero: a muted loop under the title and a Play reel pill that follows the pointer. A click grows the pill into a player with sound, pause, mute and a seek track.
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 Showreel to my page.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<section class="kodza-hero-showreel">
<div class="kodza-hero-showreel__frame">
<div class="kodza-hero-showreel__media"><div class="kodza-hero-showreel__curtain"><div class="kodza-hero-showreel__hold"><video class="kodza-hero-showreel__loop" src="https://kodza.dev/media/kz-reel-loop.mp4" poster="https://kodza.dev/media/kz-reel-poster.webp" width="1280" height="720" muted loop playsinline preload="metadata" aria-hidden="true"></video></div></div></div>
<div class="kodza-hero-showreel__content">
<p class="kodza-hero-showreel__label">Reel 2026 — Le Havre, Porto, Oslo</p>
<h1 class="kodza-hero-showreel__title"><span class="kodza-hero-showreel__line"><span class="kodza-hero-showreel__rise">A year of work.</span></span> <span class="kodza-hero-showreel__line"><span class="kodza-hero-showreel__rise">Twenty seconds.</span></span></h1>
<p class="kodza-hero-showreel__lead">No logo reveal, no drone shot over the harbour. What we shipped, cut tight.</p>
<div class="kodza-hero-showreel__actions">
<a class="kodza-hero-showreel__button" href="#">See the projects<svg class="kodza-hero-showreel__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>
</div>
</div>
<a class="kodza-hero-showreel__play" href="https://kodza.dev/media/kz-reel.mp4" data-poster="https://kodza.dev/media/kz-reel-poster.webp"><span class="kodza-hero-showreel__dot"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3.2v9.6L12.6 8z" /></svg></span><span class="kodza-hero-showreel__word">Play reel</span><span class="kodza-hero-showreel__time">0:20</span></a>
</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 Showreel
A hero over a silent loop of the reel. "Play reel" waits in the corner; with a mouse it leaves
its place to follow the pointer over the picture, and a click anywhere on it opens the full
reel with sound: the pill grows into the player, and goes back into itself on close. The
player has its own bar (pause, sound, a track to seek, the time). The link is the video file,
so it still plays without the script. Everything is tuned with the --kz-hero-showreel-*
variables. */
.kodza-hero-showreel {
--kz-hero-showreel-light: #f2f2f0;
--kz-hero-showreel-ink: #0a0a0a;
--kz-hero-showreel-dim: rgba(242, 242, 240, 0.8);
--kz-hero-showreel-faint: rgba(242, 242, 240, 0.7);
--kz-hero-showreel-glass: rgba(242, 242, 240, 0.14);
--kz-hero-showreel-shade: rgba(10, 10, 10, 0.8);
--kz-hero-showreel-base: #1c1c1c;
--kz-hero-showreel-curtain: rgba(10, 10, 10, 0.9);
--kz-hero-showreel-bg: transparent;
--kz-hero-showreel-height: clamp(560px, 54vw, 800px);
--kz-hero-showreel-gutter: clamp(12px, 2vw, 24px);
--kz-hero-showreel-pad: clamp(24px, 4.5vw, 64px);
--kz-hero-showreel-radius: 16px;
--kz-hero-showreel-position: 50% 50%;
--kz-hero-showreel-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;
--kz-hero-showreel-ease: cubic-bezier(0.22, 1, 0.36, 1);
--kz-hero-showreel-glide: cubic-bezier(0.6, 0, 0.1, 1);
--kz-hero-showreel-soft-ease: cubic-bezier(0.25, 1, 0.5, 1);
container: kodza-hero-showreel / inline-size;
box-sizing: border-box;
width: 100%;
padding-block: var(--kz-hero-showreel-gutter);
padding-left: max(var(--kz-hero-showreel-gutter), env(safe-area-inset-left, 0px));
padding-right: max(var(--kz-hero-showreel-gutter), env(safe-area-inset-right, 0px));
background: var(--kz-hero-showreel-bg);
color: var(--kz-hero-showreel-light);
}
.kodza-hero-showreel__frame {
position: relative;
isolation: isolate;
display: flex;
flex-direction: column;
justify-content: flex-end;
min-height: var(--kz-hero-showreel-height);
overflow: hidden;
border-radius: var(--kz-hero-showreel-radius);
cursor: pointer;
}
/* With a mouse, the pill is the pointer over the picture */
.kodza-hero-showreel.is-following .kodza-hero-showreel__frame {
cursor: none;
}
/* A dark base under the picture: the words stay readable while it loads. Inset by a pixel so it
never bleeds past the picture's rounded edge */
.kodza-hero-showreel__frame::before {
content: "";
position: absolute;
inset: 1px;
z-index: -1;
border-radius: max(0px, calc(var(--kz-hero-showreel-radius) - 1px));
background: var(--kz-hero-showreel-base);
}
.kodza-hero-showreel__media {
position: absolute;
inset: 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-showreel__curtain,
.kodza-hero-showreel__hold {
height: 100%;
}
.kodza-hero-showreel__curtain {
overflow: hidden;
}
.kodza-hero-showreel__loop {
display: block;
width: 100%;
max-width: none;
height: 100%;
object-fit: cover;
object-position: var(--kz-hero-showreel-position);
}
/* The veil: darkest under the words (bottom left), clear at the top right */
.kodza-hero-showreel__media::after {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(to right, color-mix(in srgb, var(--kz-hero-showreel-shade) 50%, transparent), transparent 70%),
linear-gradient(to top, var(--kz-hero-showreel-shade), color-mix(in srgb, var(--kz-hero-showreel-shade) 35%, transparent) 62%, transparent);
}
.kodza-hero-showreel__content {
position: relative;
padding: var(--kz-hero-showreel-pad);
}
.kodza-hero-showreel__label {
margin: 0 0 20px;
font-family: var(--kz-hero-showreel-mono);
font-size: 12.5px;
line-height: 16px;
color: var(--kz-hero-showreel-faint);
}
.kodza-hero-showreel__title {
margin: 0 0 22px;
/* Bounded by the container too, so each line keeps to one line on a phone */
font-size: min(clamp(38px, 6.4vw, 96px), 10.8cqi);
font-weight: 700;
line-height: 1;
letter-spacing: -0.05em;
color: var(--kz-hero-showreel-light);
}
/* Each line is its own mask: the words rise from behind its bottom edge */
.kodza-hero-showreel__line {
display: block;
overflow: hidden;
padding-bottom: 0.08em;
margin-bottom: -0.08em;
}
.kodza-hero-showreel__rise {
display: block;
}
.kodza-hero-showreel__lead {
max-width: 36ch;
margin: 0 0 32px;
font-size: clamp(16px, 1.35vw, 19px);
line-height: 1.55;
color: var(--kz-hero-showreel-dim);
text-wrap: pretty;
}
.kodza-hero-showreel__actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
/* Glass: frosted over the picture, light rises from the bottom edge on hover */
.kodza-hero-showreel__button {
position: relative;
isolation: isolate;
overflow: hidden;
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
height: 52px;
padding: 0 26px;
border-radius: 999px;
background: var(--kz-hero-showreel-glass);
-webkit-backdrop-filter: blur(14px);
backdrop-filter: blur(14px);
color: var(--kz-hero-showreel-light);
font-family: inherit;
font-size: 15px;
font-weight: 500;
line-height: 1;
white-space: nowrap;
text-decoration: none;
cursor: pointer;
transition: color 0.4s var(--kz-hero-showreel-ease);
}
.kodza-hero-showreel__button::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
border-radius: inherit;
background: var(--kz-hero-showreel-light);
transform: translateY(101%);
transition: transform 0.4s var(--kz-hero-showreel-ease);
}
.kodza-hero-showreel__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-showreel-ease);
}
.kodza-hero-showreel__button:focus-visible,
.kodza-hero-showreel__play:focus-visible {
outline: 2px solid var(--kz-hero-showreel-light);
outline-offset: 3px;
}
@media (hover: hover) {
.kodza-hero-showreel__button:hover {
color: var(--kz-hero-showreel-ink);
}
.kodza-hero-showreel__button:hover::before {
transform: none;
}
.kodza-hero-showreel__button:hover .kodza-hero-showreel__arrow {
transform: translateX(4px);
}
}
/* The pill: at home in the bottom right corner; the script moves it with `translate` */
.kodza-hero-showreel__play {
position: absolute;
z-index: 2;
right: var(--kz-hero-showreel-pad);
bottom: var(--kz-hero-showreel-pad);
box-sizing: border-box;
display: inline-flex;
align-items: center;
gap: 12px;
height: 52px;
padding: 0 20px 0 8px;
border-radius: 999px;
background: var(--kz-hero-showreel-light);
color: var(--kz-hero-showreel-ink);
font-family: inherit;
font-size: 15px;
font-weight: 500;
line-height: 1;
white-space: nowrap;
text-decoration: none;
transition:
scale 0.3s var(--kz-hero-showreel-ease),
opacity 0.2s ease;
}
.kodza-hero-showreel__dot {
display: grid;
flex: none;
place-items: center;
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--kz-hero-showreel-ink);
}
.kodza-hero-showreel__dot svg {
width: 14px;
height: 14px;
fill: var(--kz-hero-showreel-light);
}
.kodza-hero-showreel__time {
font-family: var(--kz-hero-showreel-mono);
font-size: 12.5px;
font-variant-numeric: tabular-nums;
opacity: 0.68;
}
/* Following the pointer, it lets the clicks through to what is under it */
.kodza-hero-showreel.is-following .kodza-hero-showreel__play {
pointer-events: none;
}
.kodza-hero-showreel__play.is-pressed {
scale: 0.92;
}
/* Over a link of the hero, it tucks itself away and the pointer comes back */
.kodza-hero-showreel__play.is-tucked {
scale: 0.3;
opacity: 0;
}
/* While the reel is open, the player holds it */
.kodza-hero-showreel__play.is-away {
visibility: hidden;
}
/* One column: the words take the width, the pill waits in the top right corner */
@container kodza-hero-showreel (max-width: 720px) {
.kodza-hero-showreel__play {
top: var(--kz-hero-showreel-pad);
bottom: auto;
}
.kodza-hero-showreel__button {
flex: 1 1 auto;
}
}
/* The viewer: the whole screen, a shade, the player, its bar */
.kodza-hero-showreel__viewer {
/* A modal dialog sits on the screen itself: 100% is the screen */
width: 100%;
max-width: none;
height: 100%;
max-height: none;
margin: 0;
padding: 0;
overflow: hidden;
border: 0;
background: none;
color: var(--kz-hero-showreel-light);
}
.kodza-hero-showreel__viewer::backdrop {
background: none;
}
.kodza-hero-showreel__shade {
position: absolute;
inset: 0;
background: var(--kz-hero-showreel-curtain);
/* The page behind is blurred out of reading, not just darkened */
-webkit-backdrop-filter: blur(18px);
backdrop-filter: blur(18px);
}
.kodza-hero-showreel__player {
position: fixed;
overflow: hidden;
margin: 0;
border-radius: 12px;
background: #000;
}
.kodza-hero-showreel__video {
display: block;
width: 100%;
max-width: none;
height: 100%;
margin: 0;
object-fit: cover;
cursor: pointer;
}
/* The pill, drawn over the player while it grows out of it or goes back into it */
.kodza-hero-showreel__cover {
position: absolute;
inset: 0;
display: grid;
place-items: center;
background: var(--kz-hero-showreel-light);
opacity: 0;
pointer-events: none;
}
.kodza-hero-showreel__ghost {
box-sizing: border-box;
display: inline-flex;
align-items: center;
gap: 12px;
height: 52px;
padding: 0 20px 0 8px;
color: var(--kz-hero-showreel-ink);
font-size: 15px;
font-weight: 500;
line-height: 1;
white-space: nowrap;
}
.kodza-hero-showreel__bar {
position: fixed;
box-sizing: border-box;
display: flex;
align-items: center;
gap: 8px;
margin: 0;
}
.kodza-hero-showreel__control {
display: inline-grid;
flex: none;
height: 36px;
padding: 0 14px;
overflow: hidden;
border: 0;
border-radius: 999px;
background: var(--kz-hero-showreel-glass);
color: var(--kz-hero-showreel-light);
font-family: inherit;
font-size: 14px;
font-weight: 500;
line-height: 36px;
cursor: pointer;
transition: background-color 0.2s ease;
}
/* The words of a button share one cell: it keeps the width of the longest */
.kodza-hero-showreel__control > span {
grid-area: 1 / 1;
visibility: hidden;
}
.kodza-hero-showreel__control > .is-on,
.kodza-hero-showreel__control > .is-leaving {
visibility: visible;
}
.kodza-hero-showreel__track {
position: relative;
flex: 1 1 auto;
min-width: 48px;
height: 36px;
cursor: pointer;
touch-action: none;
}
.kodza-hero-showreel__track::before,
.kodza-hero-showreel__fill {
content: "";
position: absolute;
top: 17px;
right: 0;
left: 0;
height: 2px;
border-radius: 2px;
}
.kodza-hero-showreel__track::before {
background: var(--kz-hero-showreel-glass);
}
.kodza-hero-showreel__fill {
background: var(--kz-hero-showreel-light);
transform: scaleX(0);
transform-origin: 0 50%;
}
.kodza-hero-showreel__clock {
flex: none;
font-family: var(--kz-hero-showreel-mono);
font-size: 12.5px;
font-variant-numeric: tabular-nums;
color: var(--kz-hero-showreel-faint);
white-space: nowrap;
}
.kodza-hero-showreel__close {
position: absolute;
top: calc(14px + env(safe-area-inset-top, 0px));
right: calc(14px + env(safe-area-inset-right, 0px));
display: grid;
place-items: center;
width: 40px;
height: 40px;
padding: 0;
border: 0;
border-radius: 50%;
background: var(--kz-hero-showreel-glass);
color: var(--kz-hero-showreel-light);
cursor: pointer;
transition: background-color 0.2s ease;
}
.kodza-hero-showreel__close svg {
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 1.5;
stroke-linecap: round;
}
.kodza-hero-showreel__control:focus-visible,
.kodza-hero-showreel__close:focus-visible {
outline: 2px solid var(--kz-hero-showreel-light);
outline-offset: 3px;
}
@media (hover: hover) {
.kodza-hero-showreel__control:hover,
.kodza-hero-showreel__close:hover {
background: color-mix(in srgb, var(--kz-hero-showreel-light) 24%, transparent);
}
}
/* The entrance, on load: the loop opens and settles, the title lines rise, then the rest */
@media (prefers-reduced-motion: no-preference) {
.kodza-hero-showreel__curtain {
animation: kodza-hero-showreel-curtain 1350ms var(--kz-hero-showreel-glide) 100ms both;
}
.kodza-hero-showreel__hold {
animation: kodza-hero-showreel-hold 1350ms var(--kz-hero-showreel-glide) 100ms both;
}
.kodza-hero-showreel__loop {
animation: kodza-hero-showreel-settle 2000ms var(--kz-hero-showreel-soft-ease) 100ms both;
}
.kodza-hero-showreel__label {
animation: kodza-hero-showreel-in 900ms var(--kz-hero-showreel-soft-ease) 250ms both;
}
.kodza-hero-showreel__rise {
animation: kodza-hero-showreel-rise 1170ms var(--kz-hero-showreel-glide) 300ms both;
}
.kodza-hero-showreel__line:nth-child(2) .kodza-hero-showreel__rise {
animation-delay: 425ms;
}
.kodza-hero-showreel__lead {
animation: kodza-hero-showreel-in 900ms var(--kz-hero-showreel-soft-ease) 650ms both;
}
.kodza-hero-showreel__actions {
animation: kodza-hero-showreel-in 900ms var(--kz-hero-showreel-soft-ease) 760ms both;
}
/* Backwards only: once in, the pill's scale and opacity belong to its states */
.kodza-hero-showreel__play {
animation: kodza-hero-showreel-pop 700ms var(--kz-hero-showreel-ease) 600ms backwards;
}
}
/* Closed 2px past the edge: a curtain closed exactly on the frame's edge can leak a hairline */
@keyframes kodza-hero-showreel-curtain {
from {
transform: translateY(calc(-100% - 2px));
}
}
@keyframes kodza-hero-showreel-hold {
from {
transform: translateY(calc(100% + 2px));
}
}
@keyframes kodza-hero-showreel-settle {
from {
transform: scale(1.2);
}
}
@keyframes kodza-hero-showreel-rise {
from {
transform: translateY(105%);
}
}
/* Fully drawn halfway: the words are readable while they still move */
@keyframes kodza-hero-showreel-in {
from {
opacity: 0;
transform: translateY(20px);
}
50% {
opacity: 1;
}
}
@keyframes kodza-hero-showreel-pop {
from {
opacity: 0;
transform: scale(0.6);
}
50% {
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.kodza-hero-showreel *,
.kodza-hero-showreel *::before,
.kodza-hero-showreel *::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}
/* kodza-guard f8d75e023eb5 — 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-showreel__loop):not(#_),
:where(.kodza-hero-showreel__label):not(#_),
:where(.kodza-hero-showreel__title):not(#_),
:where(.kodza-hero-showreel__lead):not(#_),
:where(.kodza-hero-showreel__viewer):not(#_),
:where(.kodza-hero-showreel__video):not(#_),
:where(.kodza-hero-showreel__dot svg:where(:not([class*="kodza-"]))):not(#_),
:where(.kodza-hero-showreel__close svg:where(:not([class*="kodza-"]))):not(#_) {
transition-duration: revert;
}
:where(.kodza-hero-showreel__button):not(#_),
:where(.kodza-hero-showreel__play):not(#_),
:where(.kodza-hero-showreel__control):not(#_),
:where(.kodza-hero-showreel__close):not(#_) {
outline: revert;
outline-offset: revert;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-hero-showreel__frame:not(#_),
.kodza-hero-showreel__media:not(#_),
.kodza-hero-showreel__curtain:not(#_),
.kodza-hero-showreel__hold:not(#_),
.kodza-hero-showreel__content:not(#_),
.kodza-hero-showreel__line:not(#_),
.kodza-hero-showreel__rise:not(#_),
.kodza-hero-showreel__actions:not(#_),
.kodza-hero-showreel__dot:not(#_),
.kodza-hero-showreel__word:not(#_),
.kodza-hero-showreel__time:not(#_),
.kodza-hero-showreel__shade:not(#_),
.kodza-hero-showreel__player:not(#_),
.kodza-hero-showreel__cover:not(#_),
.kodza-hero-showreel__track:not(#_),
.kodza-hero-showreel__fill:not(#_),
.kodza-hero-showreel__clock:not(#_),
.kodza-hero-showreel__now:not(#_),
.kodza-hero-showreel__total:not(#_),
.kodza-hero-showreel__control span:where(:not([class*="kodza-"])):not(#_) {
box-sizing: revert;
}
.kodza-hero-showreel__loop: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-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-showreel__label: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-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-showreel__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-showreel__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-showreel__button:not(#_),
.kodza-hero-showreel__play:not(#_) {
border: revert;
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-showreel__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-showreel__viewer:not(#_) {
border-radius: revert;
outline: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: revert;
box-shadow: revert;
outline-offset: revert;
appearance: 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-showreel__video: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;
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-showreel__control:not(#_) {
margin: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: 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;
white-space: revert;
min-height: revert;
}
.kodza-hero-showreel__close:not(#_) {
margin: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: revert;
box-shadow: 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-hero-showreel__dot svg:where(:not([class*="kodza-"])):not(#_),
.kodza-hero-showreel__close 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;
transition-delay: revert;
}
.kodza-hero-showreel__label:not(#_):where(:not([hidden])),
.kodza-hero-showreel__title:not(#_):where(:not([hidden])),
.kodza-hero-showreel__lead:not(#_):where(:not([hidden])),
.kodza-hero-showreel__arrow:not(#_):where(:not([hidden])),
.kodza-hero-showreel__viewer:not(#_):where(:not([hidden])),
.kodza-hero-showreel__dot svg:where(:not([class*="kodza-"])):not(#_):where(:not([hidden])),
.kodza-hero-showreel__close svg:where(:not([class*="kodza-"])):not(#_):where(:not([hidden])) {
display: revert;
}
.kodza-hero-showreel__label:not(#_):where([hidden]),
.kodza-hero-showreel__title:not(#_):where([hidden]),
.kodza-hero-showreel__lead:not(#_):where([hidden]),
.kodza-hero-showreel__arrow:not(#_):where([hidden]),
.kodza-hero-showreel__viewer:not(#_):where([hidden]),
.kodza-hero-showreel__dot svg:where(:not([class*="kodza-"])):not(#_):where([hidden]),
.kodza-hero-showreel__close svg:where(:not([class*="kodza-"])):not(#_):where([hidden]) {
display: none;
}
.kodza-hero-showreel__frame:not(#_)::before,
.kodza-hero-showreel__media:not(#_)::after,
.kodza-hero-showreel__button:not(#_)::before,
.kodza-hero-showreel__track:not(#_)::before {
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-hero-showreel__loop:not(#_) {
display: block;
max-width: none;
}
.kodza-hero-showreel__label:not(#_) {
margin: 0 0 20px;
font-family: var(--kz-hero-showreel-mono);
font-size: 12.5px;
line-height: 16px;
color: var(--kz-hero-showreel-faint);
}
.kodza-hero-showreel__title:not(#_) {
margin: 0 0 22px;
font-size: min(clamp(38px, 6.4vw, 96px), 10.8cqi);
font-weight: 700;
line-height: 1;
letter-spacing: -0.05em;
color: var(--kz-hero-showreel-light);
}
.kodza-hero-showreel__lead:not(#_) {
max-width: 36ch;
margin: 0 0 32px;
font-size: clamp(16px, 1.35vw, 19px);
line-height: 1.55;
color: var(--kz-hero-showreel-dim);
}
.kodza-hero-showreel__button:not(#_) {
position: relative;
box-sizing: border-box;
display: inline-flex;
padding: 0 26px;
border-radius: 999px;
background: var(--kz-hero-showreel-glass);
color: var(--kz-hero-showreel-light);
font-family: inherit;
font-size: 15px;
font-weight: 500;
line-height: 1;
text-decoration: none;
transition: color 0.4s var(--kz-hero-showreel-ease);
}
.kodza-hero-showreel__arrow:not(#_) {
transition: transform 0.3s var(--kz-hero-showreel-ease);
}
.kodza-hero-showreel__button:focus-visible:not(#_),
.kodza-hero-showreel__play:focus-visible:not(#_) {
outline: 2px solid var(--kz-hero-showreel-light);
outline-offset: 3px;
}
@media (hover: hover) {
.kodza-hero-showreel__button:hover:not(#_) {
color: var(--kz-hero-showreel-ink);
}
}
.kodza-hero-showreel__play:not(#_) {
position: absolute;
box-sizing: border-box;
display: inline-flex;
padding: 0 20px 0 8px;
border-radius: 999px;
background: var(--kz-hero-showreel-light);
color: var(--kz-hero-showreel-ink);
font-family: inherit;
font-size: 15px;
font-weight: 500;
line-height: 1;
text-decoration: none;
transition: scale 0.3s var(--kz-hero-showreel-ease),
opacity 0.2s ease;
}
.kodza-hero-showreel__viewer:not(#_) {
max-width: none;
max-height: none;
margin: 0;
padding: 0;
border: 0;
background: none;
color: var(--kz-hero-showreel-light);
}
.kodza-hero-showreel__video:not(#_) {
display: block;
max-width: none;
margin: 0;
}
.kodza-hero-showreel__control:not(#_) {
display: inline-grid;
flex: none;
height: 36px;
padding: 0 14px;
overflow: hidden;
border: 0;
border-radius: 999px;
background: var(--kz-hero-showreel-glass);
color: var(--kz-hero-showreel-light);
font-family: inherit;
font-size: 14px;
font-weight: 500;
line-height: 36px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.kodza-hero-showreel__close:not(#_) {
position: absolute;
top: calc(14px + env(safe-area-inset-top, 0px));
right: calc(14px + env(safe-area-inset-right, 0px));
display: grid;
place-items: center;
width: 40px;
height: 40px;
padding: 0;
border: 0;
border-radius: 50%;
background: var(--kz-hero-showreel-glass);
color: var(--kz-hero-showreel-light);
cursor: pointer;
transition: background-color 0.2s ease;
}
.kodza-hero-showreel__control:focus-visible:not(#_),
.kodza-hero-showreel__close:focus-visible:not(#_) {
outline: 2px solid var(--kz-hero-showreel-light);
outline-offset: 3px;
}
@media (hover: hover) {
.kodza-hero-showreel__control:hover:not(#_),
.kodza-hero-showreel__close:hover:not(#_) {
background: color-mix(in srgb, var(--kz-hero-showreel-light) 24%, transparent);
}
}
@media (prefers-reduced-motion: reduce) {
.kodza-hero-showreel *:not(#_),
.kodza-hero-showreel *:not(#_)::before,
.kodza-hero-showreel *:not(#_)::after {
transition-duration: 0.01ms !important;
animation-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 Showreel
// The loop under the hero plays muted while it's on screen. "Play reel" is a link to the full
// reel (data-poster: its first picture): with a mouse it follows the pointer over the picture
// and tucks itself away over the hero's own links; a click anywhere on the picture, or the
// link itself, opens the reel with sound in a dialog. The pill grows into the player, and the
// player goes back into the pill on close (Escape, the shade, the close button). In the player:
// Pause, Mute, a track to seek, the time, and the arrow keys for 5 s back or ahead.
(() => {
const SELECTOR = '.kodza-hero-showreel';
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const fine = window.matchMedia('(hover: hover) and (pointer: fine)');
const ease = 'cubic-bezier(0.22, 1, 0.36, 1)';
const grow = 'cubic-bezier(0.65, 0, 0.35, 1)';
const WORDS = { pause: 'Pause', play: 'Play', replay: 'Replay', mute: 'Mute', unmute: 'Unmute', close: 'Close', label: 'Showreel' };
const STEP = 5; // seconds, for the arrow keys
const time = (s) => {
const t = Math.max(0, Math.floor(s || 0));
return `${Math.floor(t / 60)}:${String(t % 60).padStart(2, '0')}`;
};
const words = (...keys) => keys.map((key) => `<span data-word="${key}">${WORDS[key]}</span>`).join('');
// One instance, with its own state: two copies on a page never interfere. Everything it
// starts is tied to `signal` (listeners get { signal }, observers, loops and the dialog stop
// on its 'abort' event), so kodza:destroy stops it all.
function mount(root, signal) {
const frame = root.querySelector('.kodza-hero-showreel__frame');
const pill = root.querySelector('.kodza-hero-showreel__play');
const loop = root.querySelector('.kodza-hero-showreel__loop');
if (!frame || !pill) return;
const viewer = document.createElement('dialog');
viewer.className = 'kodza-hero-showreel__viewer';
viewer.setAttribute('aria-label', WORDS.label);
viewer.innerHTML = `<div class="kodza-hero-showreel__shade"></div>
<div class="kodza-hero-showreel__player"><video class="kodza-hero-showreel__video" playsinline preload="none"></video><div class="kodza-hero-showreel__cover"><span class="kodza-hero-showreel__ghost"></span></div></div>
<div class="kodza-hero-showreel__bar">
<button class="kodza-hero-showreel__control kodza-hero-showreel__toggle" type="button">${words('pause', 'play', 'replay')}</button>
<button class="kodza-hero-showreel__control kodza-hero-showreel__sound" type="button">${words('mute', 'unmute')}</button>
<div class="kodza-hero-showreel__track" aria-hidden="true"><span class="kodza-hero-showreel__fill"></span></div>
<span class="kodza-hero-showreel__clock"><span class="kodza-hero-showreel__now">0:00</span> / <span class="kodza-hero-showreel__total"></span></span>
</div>
<button class="kodza-hero-showreel__close" type="button" aria-label="${WORDS.close}"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8" /></svg></button>`;
root.append(viewer);
const shade = viewer.querySelector('.kodza-hero-showreel__shade');
const player = viewer.querySelector('.kodza-hero-showreel__player');
const video = viewer.querySelector('.kodza-hero-showreel__video');
const cover = viewer.querySelector('.kodza-hero-showreel__cover');
const ghost = viewer.querySelector('.kodza-hero-showreel__ghost');
const bar = viewer.querySelector('.kodza-hero-showreel__bar');
const toggle = viewer.querySelector('.kodza-hero-showreel__toggle');
const sound = viewer.querySelector('.kodza-hero-showreel__sound');
const track = viewer.querySelector('.kodza-hero-showreel__track');
const fill = viewer.querySelector('.kodza-hero-showreel__fill');
const now = viewer.querySelector('.kodza-hero-showreel__now');
const total = viewer.querySelector('.kodza-hero-showreel__total');
const close = viewer.querySelector('.kodza-hero-showreel__close');
total.textContent = pill.querySelector('.kodza-hero-showreel__time')?.textContent.trim() || '0:00';
// The pill's own face, drawn on the player while it grows
ghost.innerHTML = pill.innerHTML;
// The loop plays only while it's on screen and the reel is closed
let seen = false;
const looping = () => {
if (!loop) return;
if (seen && !viewer.open && !reduceMotion) loop.play().catch(() => {});
else loop.pause();
};
const watch = new IntersectionObserver(([entry]) => {
seen = entry.isIntersecting;
looping();
});
watch.observe(frame);
// A word of a button that rolls: the old one up, the new one from below
const say = (button, key) => {
const next = button.querySelector(`[data-word="${key}"]`);
const old = button.querySelector('.is-on');
if (old === next) return;
next.getAnimations().forEach((a) => a.cancel());
next.classList.remove('is-leaving');
next.classList.add('is-on');
if (!old) return;
old.classList.remove('is-on');
if (reduceMotion || !viewer.open) return;
old.classList.add('is-leaving');
const timing = { duration: 420, easing: ease };
old.animate([{ transform: 'none' }, { transform: 'translateY(-100%)' }], timing).finished.then(() => old.classList.remove('is-leaving'), () => {});
next.animate([{ transform: 'translateY(100%)' }, { transform: 'none' }], timing);
};
const state = () => {
say(toggle, video.ended ? 'replay' : video.paused ? 'play' : 'pause');
say(sound, video.muted ? 'unmute' : 'mute');
};
say(toggle, 'pause');
say(sound, 'mute');
// Where the player fits on screen, 16:9: room kept for the close button above and the bar below
const fit = () => {
const ratio = (video.videoWidth / video.videoHeight) || 16 / 9;
const above = 68;
const below = 72;
const w = innerWidth - 32;
const h = innerHeight - above - below;
const width = Math.min(w, h * ratio);
const height = width / ratio;
return { left: (innerWidth - width) / 2, top: above + (h - height) / 2, width, height };
};
let box = null;
const place = () => {
box = fit();
Object.assign(player.style, { left: `${box.left}px`, top: `${box.top}px`, width: `${box.width}px`, height: `${box.height}px` });
Object.assign(bar.style, { left: `${box.left}px`, top: `${box.top + box.height + 14}px`, width: `${box.width}px` });
};
// The player drawn as the pill: moved onto it and cut to its shape, never stretched
const shape = (rect) => {
const x = rect.left + rect.width / 2 - (box.left + box.width / 2);
const y = rect.top + rect.height / 2 - (box.top + box.height / 2);
const side = Math.max(0, (box.width - rect.width) / 2);
const top = Math.max(0, (box.height - rect.height) / 2);
return { transform: `translate(${x}px, ${y}px)`, clipPath: `inset(${top}px ${side}px round ${rect.height / 2}px)` };
};
const full = { transform: 'none', clipPath: 'inset(0px 0px round 12px)' };
// The bar follows the time while the reel is open
let frameId = 0;
const draw = () => {
const d = video.duration || 0;
fill.style.transform = `scaleX(${d ? Math.min(1, video.currentTime / d) : 0})`;
const text = time(video.currentTime);
if (now.textContent !== text) now.textContent = text;
frameId = viewer.open ? requestAnimationFrame(draw) : 0;
};
const settle = () => [player, video, cover, ghost, shade, bar, close].forEach((el) => el.getAnimations().forEach((a) => a.cancel()));
let closing = false;
const open = () => {
if (viewer.open) return;
closing = false;
settle();
if (!video.getAttribute('src')) {
video.preload = 'auto';
if (pill.dataset.poster) video.poster = pill.dataset.poster;
video.src = pill.href;
}
video.currentTime = 0;
const rect = pill.getBoundingClientRect();
// Opened from the picture, the pill is still the trigger: the focus comes back to it
if (document.activeElement !== pill) pill.focus({ preventScroll: true });
viewer.showModal();
place();
pill.classList.add('is-away');
looping();
// With sound, from a click; if the browser refuses, it plays muted and Unmute is there
video.muted = false;
video.play().catch(() => {
video.muted = true;
return video.play();
}).catch(() => {}).finally(state);
state();
cancelAnimationFrame(frameId);
frameId = requestAnimationFrame(draw);
if (reduceMotion) return;
const timing = { duration: 760, easing: grow };
player.animate([shape(rect), full], timing);
video.animate([{ transform: 'scale(1.15)' }, { transform: 'none' }], timing);
cover.animate([{ opacity: 1 }, { opacity: 1, offset: 0.12 }, { opacity: 0, offset: 0.55 }], timing);
ghost.animate([{ opacity: 1 }, { opacity: 0, offset: 0.25 }], timing);
shade.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 480, easing: 'ease' });
[bar, close].forEach((el) => el.animate([{ opacity: 0, transform: 'translateY(8px)' }, { opacity: 1, transform: 'none' }], { duration: 480, delay: 420, easing: ease, fill: 'backwards' }));
};
const shut = () => {
if (closing || !viewer.open) return;
closing = true;
video.pause();
// The pill shows again before the dialog closes, so the focus can go back to it
const done = () => {
pill.classList.remove('is-away');
viewer.close();
settle();
looping();
};
if (reduceMotion) return done();
const rect = pill.getBoundingClientRect();
const there = rect.bottom > 0 && rect.top < innerHeight && rect.right > 0 && rect.left < innerWidth;
const timing = { duration: 640, easing: grow, fill: 'forwards' };
[bar, close].forEach((el) => el.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 200, fill: 'forwards' }));
shade.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 520, delay: 120, easing: 'ease', fill: 'forwards' });
// Back into the pill if it's on screen; otherwise the player simply fades
if (!there) {
player.animate([{ opacity: 1 }, { opacity: 0, transform: 'scale(0.96)' }], { ...timing, duration: 420 }).finished.then(done, () => {});
return;
}
player.animate([full, shape(rect)], timing).finished.then(done, () => {});
video.animate([{ transform: 'none' }, { transform: 'scale(1.15)' }], timing);
cover.animate([{ opacity: 0 }, { opacity: 0, offset: 0.45 }, { opacity: 1, offset: 0.88 }, { opacity: 1 }], timing);
ghost.animate([{ opacity: 0 }, { opacity: 0, offset: 0.75 }, { opacity: 1 }], timing);
};
// The pill follows the pointer over the picture, a little late, and goes home when it leaves
const pos = { x: 0, y: 0 };
const aim = { x: 0, y: 0 };
let chasing = 0;
const chase = () => {
const k = reduceMotion ? 1 : 0.2;
pos.x += (aim.x - pos.x) * k;
pos.y += (aim.y - pos.y) * k;
const still = Math.abs(aim.x - pos.x) < 0.1 && Math.abs(aim.y - pos.y) < 0.1;
if (still) Object.assign(pos, aim);
pill.style.translate = pos.x || pos.y ? `${pos.x}px ${pos.y}px` : '';
chasing = still ? 0 : requestAnimationFrame(chase);
if (still && !aim.x && !aim.y) root.classList.remove('is-following');
};
const go = () => {
if (!chasing) chasing = requestAnimationFrame(chase);
};
// Screen pixels into the frame's own: a parent scaled with transform must not shift it
const toward = (event) => {
const rect = frame.getBoundingClientRect();
const scale = rect.width / frame.offsetWidth || 1;
aim.x = (event.clientX - rect.left) / scale - (pill.offsetLeft + pill.offsetWidth / 2);
aim.y = (event.clientY - rect.top) / scale - (pill.offsetTop + pill.offsetHeight / 2);
go();
};
const other = (target) => {
const hit = target.closest?.('a, button, input, select, textarea, label, [tabindex]');
return hit && hit !== pill && frame.contains(hit) ? hit : null;
};
frame.addEventListener('pointermove', (event) => {
if (event.pointerType !== 'mouse' || !fine.matches || viewer.open) return;
root.classList.add('is-following');
pill.classList.toggle('is-tucked', Boolean(other(event.target)));
toward(event);
}, { signal });
frame.addEventListener('pointerleave', () => {
pill.classList.remove('is-tucked', 'is-pressed');
aim.x = 0;
aim.y = 0;
go();
}, { signal });
frame.addEventListener('pointerdown', (event) => {
if (event.button === 0 && !other(event.target)) pill.classList.add('is-pressed');
}, { signal });
addEventListener('pointerup', () => pill.classList.remove('is-pressed'), { signal });
// A click on the picture or on the pill opens the reel; a modified click on the pill still
// opens the file the browser's way (new tab, download)
frame.addEventListener('click', (event) => {
if (other(event.target)) return;
if (event.target.closest('.kodza-hero-showreel__play') && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) return;
event.preventDefault();
open();
}, { signal });
toggle.addEventListener('click', () => (video.paused || video.ended ? video.play().catch(() => {}) : video.pause()), { signal });
video.addEventListener('click', () => toggle.click(), { signal });
sound.addEventListener('click', () => {
video.muted = !video.muted;
}, { signal });
['play', 'pause', 'ended', 'volumechange'].forEach((type) => video.addEventListener(type, state, { signal }));
video.addEventListener('loadedmetadata', () => {
total.textContent = time(video.duration);
if (viewer.open && !closing) place();
}, { signal });
// The track: a press or a drag puts the reel there
const seek = (event) => {
const rect = track.getBoundingClientRect();
if (video.duration) video.currentTime = Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width)) * video.duration;
};
track.addEventListener('pointerdown', (event) => {
track.setPointerCapture(event.pointerId);
seek(event);
}, { signal });
track.addEventListener('pointermove', (event) => track.hasPointerCapture(event.pointerId) && seek(event), { signal });
close.addEventListener('click', shut, { signal });
shade.addEventListener('click', shut, { signal });
viewer.addEventListener('cancel', (event) => {
event.preventDefault();
shut();
}, { signal });
viewer.addEventListener('keydown', (event) => {
if (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft' || !video.duration) return;
event.preventDefault();
video.currentTime = Math.min(video.duration, Math.max(0, video.currentTime + (event.key === 'ArrowRight' ? STEP : -STEP)));
}, { signal });
// The page stays where it is under the reel
viewer.addEventListener('wheel', (event) => event.preventDefault(), { passive: false, signal });
viewer.addEventListener('touchmove', (event) => event.preventDefault(), { passive: false, signal });
addEventListener('resize', () => viewer.open && !closing && place(), { signal });
// Stopped while open (or closing): the reel stops and the dialog closes at once
signal.addEventListener('abort', () => {
watch.disconnect();
cancelAnimationFrame(frameId);
cancelAnimationFrame(chasing);
video.pause();
if (viewer.open) viewer.close();
});
}
// 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. Each running root
// keeps its markup as written under KODZA, shared by every Kodza script on the page: a Kodza
// component inside another one is saved as written, not half-started, and stopped with it.
const KODZA = Symbol.for('kodza');
const instances = new WeakMap();
const find = (scope = document) => [scope, ...scope.querySelectorAll(SELECTOR)].filter((el) => el.matches?.(SELECTOR));
const attrsOf = (el) => [...el.attributes].map((a) => [a.name, a.value]);
const put = (el, saved) => {
[...el.attributes].forEach((a) => el.removeAttribute(a.name));
saved.attrs.forEach(([name, value]) => el.setAttribute(name, value));
el.innerHTML = saved.html;
};
// The markup inside a root as written: Kodza components already running in it give back their own
const written = (root) => {
const all = [...root.querySelectorAll('*')];
if (!all.some((el) => el[KODZA])) return root.innerHTML;
const copy = root.cloneNode(true);
const twins = copy.querySelectorAll('*');
let last = null;
all.forEach((el, i) => {
if (!el[KODZA] || last?.contains(el)) return;
last = el;
put(twins[i], el[KODZA]);
});
return copy.innerHTML;
};
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();
const saved = { html: written(root), attrs: attrsOf(root), stop: () => destroy(root) };
instances.set(root, { controller, saved });
root[KODZA] = saved;
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);
// The Kodza components running inside it stop first: once its markup is put back, their own
// script could not find them any more
root.querySelectorAll('*').forEach((el) => el[KODZA]?.stop());
instance.controller.abort();
delete root[KODZA];
put(root, instance.saved);
});
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 Showreel, for React. From https://kodza.dev/components/hero-showreel/
// 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-showreel.css and hero-showreel.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-showreel.css';
export type HeroShowreelProps = Omit<ComponentPropsWithoutRef<'section'>, "children" | "vars"> & {
/** Settings, as CSS variables on the root */
vars?: {
/** default: #f2f2f0 */
"--kz-hero-showreel-light"?: string | number;
/** default: #0a0a0a */
"--kz-hero-showreel-ink"?: string | number;
/** default: rgba(242, 242, 240, 0.8) */
"--kz-hero-showreel-dim"?: string | number;
/** default: rgba(242, 242, 240, 0.7) */
"--kz-hero-showreel-faint"?: string | number;
/** default: rgba(242, 242, 240, 0.14) */
"--kz-hero-showreel-glass"?: string | number;
/** default: rgba(10, 10, 10, 0.8) */
"--kz-hero-showreel-shade"?: string | number;
/** default: #1c1c1c */
"--kz-hero-showreel-base"?: string | number;
/** default: rgba(10, 10, 10, 0.9) */
"--kz-hero-showreel-curtain"?: string | number;
/** default: transparent */
"--kz-hero-showreel-bg"?: string | number;
/** default: clamp(560px, 54vw, 800px) */
"--kz-hero-showreel-height"?: string | number;
/** default: clamp(12px, 2vw, 24px) */
"--kz-hero-showreel-gutter"?: string | number;
/** default: clamp(24px, 4.5vw, 64px) */
"--kz-hero-showreel-pad"?: string | number;
/** default: 16px */
"--kz-hero-showreel-radius"?: string | number;
/** default: 50% 50% */
"--kz-hero-showreel-position"?: string | number;
/** default: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace */
"--kz-hero-showreel-mono"?: string | number;
/** default: cubic-bezier(0.22, 1, 0.36, 1) */
"--kz-hero-showreel-ease"?: string | number;
/** default: cubic-bezier(0.6, 0, 0.1, 1) */
"--kz-hero-showreel-glide"?: string | number;
/** default: cubic-bezier(0.25, 1, 0.5, 1) */
"--kz-hero-showreel-soft-ease"?: string | number;
};
className?: string;
};
export function HeroShowreel({ vars, className, style, ...rest }: HeroShowreelProps) {
const ref = useRef<ComponentRef<'section'>>(null);
const classes = ["kodza-hero-showreel", 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-showreel.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-showreel__frame">
<div className="kodza-hero-showreel__media">
<div className="kodza-hero-showreel__curtain">
<div className="kodza-hero-showreel__hold">
<video className="kodza-hero-showreel__loop" src="https://kodza.dev/media/kz-reel-loop.mp4" poster="https://kodza.dev/media/kz-reel-poster.webp" width="1280" height="720" muted loop playsInline preload="metadata" aria-hidden="true" />
</div>
</div>
</div>
<div className="kodza-hero-showreel__content">
<p className="kodza-hero-showreel__label">Reel 2026 — Le Havre, Porto, Oslo</p>
<h1 className="kodza-hero-showreel__title">
<span className="kodza-hero-showreel__line">
<span className="kodza-hero-showreel__rise">A year of work.</span>
</span>
{' '}
<span className="kodza-hero-showreel__line">
<span className="kodza-hero-showreel__rise">Twenty seconds.</span>
</span>
</h1>
<p className="kodza-hero-showreel__lead">No logo reveal, no drone shot over the harbour. What we shipped, cut tight.</p>
<div className="kodza-hero-showreel__actions">
<a className="kodza-hero-showreel__button" href="#">See the projects<svg className="kodza-hero-showreel__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>
</div>
</div>
<a className="kodza-hero-showreel__play" href="https://kodza.dev/media/kz-reel.mp4" data-poster="https://kodza.dev/media/kz-reel-poster.webp">
<span className="kodza-hero-showreel__dot">
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M5 3.2v9.6L12.6 8z" />
</svg>
</span>
<span className="kodza-hero-showreel__word">Play reel</span>
<span className="kodza-hero-showreel__time">0:20</span>
</a>
</div>
</section>
);
}
Checked
Robots poked at it before you did. Every component goes through the same checks, and again each time it changes.