Cookie Consent
An honest cookie card: Reject weighs as much as Accept, Choose unfolds the categories, the choice is kept and a link brings the card back.
View codeA panel that slides in from the right, and on phones rises from the bottom as a sheet you drag, with half and full stops and a flick to dismiss.
With your AI
Say it to your AI. It gets the exact code below, not its best guess, and knows where it goes.
Open my cart in a Kodza Sheet.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<div class="kodza-sheet">
<button class="kodza-sheet__trigger" type="button">Cart <span class="kodza-sheet__badge">3</span></button>
<dialog class="kodza-sheet__panel">
<div class="kodza-sheet__grip" aria-hidden="true"><span></span></div>
<header class="kodza-sheet__head">
<h2 class="kodza-sheet__title">Your cart</h2>
<button class="kodza-sheet__close" type="button" aria-label="Close"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8"/></svg></button>
</header>
<ul class="kodza-sheet__items">
<li class="kodza-sheet__item"><img src="https://kodza.dev/media/kz-block-1400.webp" alt=""><span class="kodza-sheet__name">Fill Button<small>HTML, CSS, a pinch of JS</small></span><span class="kodza-sheet__price">€0.00</span></li>
<li class="kodza-sheet__item"><img src="https://kodza.dev/media/kz-stack-1400.webp" alt=""><span class="kodza-sheet__name">Sticky Cards<small>Scroll, then scroll some more</small></span><span class="kodza-sheet__price">€0.00</span></li>
<li class="kodza-sheet__item"><img src="https://kodza.dev/media/kz-arch-1400.webp" alt=""><span class="kodza-sheet__name">Date Picker<small>Fridays included, at your own risk</small></span><span class="kodza-sheet__price">€0.00</span></li>
</ul>
<footer class="kodza-sheet__foot">
<p class="kodza-sheet__total"><span>Total</span><b>€0.00</b></p>
<p class="kodza-sheet__note">Everything is free. The cart is just for the feeling.</p>
<button class="kodza-sheet__cta" type="button">Copy everything</button>
</footer>
</dialog>
</div>
/* Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license */
/* Kodza — Sheet
A panel over the page, in a real dialog element: on a computer it slides in from the right; on a
phone it rises from the bottom as a sheet you can drag, with stops at half height and full
height, and a flick down sends it away. Its content arrives in a short cascade. The focus
stays inside, Escape and a click on the page close it, the page behind does not scroll.
Everything is tuned with the --kz-sheet-* variables. */
.kodza-sheet {
--kz-sheet-bg: #ffffff;
--kz-sheet-fg: #0a0a0a;
--kz-sheet-muted: rgba(10, 10, 10, 0.6);
--kz-sheet-line: rgba(10, 10, 10, 0.1);
--kz-sheet-soft: rgba(10, 10, 10, 0.05);
--kz-sheet-ink: #0a0a0a;
--kz-sheet-on-ink: #ffffff;
--kz-sheet-backdrop: rgba(10, 10, 10, 0.3);
--kz-sheet-width: 420px;
--kz-sheet-radius: 20px;
--kz-sheet-duration: 0.55s;
--kz-sheet-ease: cubic-bezier(0.32, 0.72, 0, 1);
display: inline-block;
color: var(--kz-sheet-fg);
}
.kodza-sheet__trigger,
.kodza-sheet__cta {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 44px;
padding: 0 18px;
border: 0;
border-radius: 999px;
background: var(--kz-sheet-ink);
color: var(--kz-sheet-on-ink);
font: inherit;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.2s ease, transform 0.2s ease;
}
.kodza-sheet__trigger:hover,
.kodza-sheet__cta:hover {
opacity: 0.86;
}
.kodza-sheet__trigger:active,
.kodza-sheet__cta:active {
transform: scale(0.97);
}
.kodza-sheet__badge {
display: grid;
place-items: center;
min-width: 20px;
height: 20px;
padding: 0 6px;
border-radius: 999px;
background: var(--kz-sheet-on-ink);
color: var(--kz-sheet-ink);
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 11px;
box-sizing: border-box;
}
.kodza-sheet__trigger:focus-visible,
.kodza-sheet__cta:focus-visible,
.kodza-sheet__close:focus-visible {
outline: 2px solid var(--kz-sheet-fg);
outline-offset: 3px;
}
/* The panel: pinned to the right edge, full height */
.kodza-sheet__panel {
position: fixed;
inset: 8px 8px 8px auto;
display: grid;
grid-template-rows: auto auto 1fr auto;
width: min(var(--kz-sheet-width), calc(100% - 16px));
max-width: none;
height: calc(100% - 16px);
max-height: none;
margin: 0;
padding: 0;
border: 0;
border-radius: var(--kz-sheet-radius);
background: var(--kz-sheet-bg);
box-shadow: 0 0 0 1px var(--kz-sheet-line), -20px 0 60px -20px rgba(10, 10, 10, 0.3);
color: var(--kz-sheet-fg);
font: inherit;
transform: translateX(calc(100% + 16px));
}
.kodza-sheet__panel:not([open]) {
display: none;
}
.kodza-sheet__panel.is-open {
transform: none;
}
.kodza-sheet__panel.is-moving {
transition: transform var(--kz-sheet-duration) var(--kz-sheet-ease);
}
.kodza-sheet__panel::backdrop {
background: transparent;
transition: background-color var(--kz-sheet-duration) ease;
}
.kodza-sheet__panel.is-open::backdrop {
background: var(--kz-sheet-backdrop);
}
.kodza-sheet__grip {
display: none;
grid-row: 1;
}
.kodza-sheet__head {
grid-row: 2;
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 20px 12px 24px;
}
.kodza-sheet__title {
margin: 0;
font-size: 22px;
font-weight: 600;
letter-spacing: -0.025em;
}
.kodza-sheet__close {
display: grid;
place-items: center;
width: 32px;
height: 32px;
padding: 0;
border: 0;
border-radius: 50%;
background: var(--kz-sheet-soft);
color: var(--kz-sheet-fg);
cursor: pointer;
transition: background-color 0.2s ease;
}
.kodza-sheet__close:hover {
background: var(--kz-sheet-line);
}
.kodza-sheet__close svg {
width: 14px;
height: 14px;
fill: none;
stroke: currentColor;
stroke-width: 1.6;
stroke-linecap: round;
}
.kodza-sheet__items {
grid-row: 3;
margin: 0;
padding: 0 24px;
overflow-y: auto;
overscroll-behavior: contain;
list-style: none;
}
.kodza-sheet__item {
display: grid;
grid-template-columns: 56px minmax(0, 1fr) auto;
align-items: center;
gap: 14px;
padding: 14px 0;
border-bottom: 1px solid var(--kz-sheet-line);
}
.kodza-sheet__item img {
width: 56px;
height: 56px;
border-radius: 12px;
object-fit: cover;
}
.kodza-sheet__name {
display: grid;
gap: 2px;
font-size: 15px;
font-weight: 500;
}
.kodza-sheet__name small {
font-size: 13px;
font-weight: 400;
color: var(--kz-sheet-muted);
}
.kodza-sheet__price {
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 13px;
}
.kodza-sheet__foot {
grid-row: 4;
display: grid;
gap: 10px;
padding: 16px 24px 24px;
border-top: 1px solid var(--kz-sheet-line);
}
.kodza-sheet__total {
display: flex;
justify-content: space-between;
margin: 0;
font-size: 15px;
font-weight: 500;
}
.kodza-sheet__total b {
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-weight: 500;
}
.kodza-sheet__note {
margin: 0 0 6px;
font-size: 13px;
color: var(--kz-sheet-muted);
}
.kodza-sheet__cta {
width: 100%;
height: 48px;
}
/* The content arrives in a short cascade once the panel is moving in */
.kodza-sheet__panel.is-open .kodza-sheet__head,
.kodza-sheet__panel.is-open .kodza-sheet__item,
.kodza-sheet__panel.is-open .kodza-sheet__foot {
animation: kodza-sheet-in 0.6s cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: calc(0.12s + var(--i, 0) * 50ms);
}
@keyframes kodza-sheet-in {
from {
opacity: 0;
transform: translateX(24px);
}
}
/* Phones: a sheet from the bottom, with a grip, stops at half and full height */
@media (max-width: 640px) {
.kodza-sheet__panel {
inset: auto 0 0;
width: 100%;
height: calc(100% - 40px);
border-radius: var(--kz-sheet-radius) var(--kz-sheet-radius) 0 0;
box-shadow: 0 -20px 60px -20px rgba(10, 10, 10, 0.3);
transform: translateY(100%);
touch-action: none;
}
.kodza-sheet__panel.is-open {
transform: translateY(var(--kz-sheet-y, 0px));
}
.kodza-sheet__grip {
display: grid;
place-items: center;
height: 22px;
cursor: grab;
}
.kodza-sheet__grip span {
width: 40px;
height: 5px;
border-radius: 999px;
background: var(--kz-sheet-line);
}
.kodza-sheet__head {
padding-top: 4px;
}
.kodza-sheet__items {
touch-action: pan-y;
}
/* The total and the button stay at the bottom of the screen, whatever the stop */
.kodza-sheet__foot {
position: relative;
z-index: 1;
background: var(--kz-sheet-bg);
transform: translateY(calc(var(--kz-sheet-y, 0px) * -1));
}
.kodza-sheet__panel.is-moving .kodza-sheet__foot {
transition: transform var(--kz-sheet-duration) var(--kz-sheet-ease);
}
@keyframes kodza-sheet-in {
from {
opacity: 0;
transform: translateY(16px);
}
}
}
@media (prefers-reduced-motion: reduce) {
.kodza-sheet__panel.is-moving,
.kodza-sheet__panel::backdrop,
.kodza-sheet__trigger,
.kodza-sheet__cta {
transition-duration: 0.01ms !important;
}
.kodza-sheet__panel.is-open .kodza-sheet__head,
.kodza-sheet__panel.is-open .kodza-sheet__item,
.kodza-sheet__panel.is-open .kodza-sheet__foot {
animation: none;
}
}
/* Theming example — on a dark background:
.dark-section .kodza-sheet {
--kz-sheet-bg: #161616;
--kz-sheet-fg: #f2f2f0;
--kz-sheet-muted: rgba(242, 242, 240, 0.62);
--kz-sheet-line: rgba(255, 255, 255, 0.12);
--kz-sheet-soft: rgba(255, 255, 255, 0.07);
--kz-sheet-ink: #f2f2f0;
--kz-sheet-on-ink: #0a0a0a;
--kz-sheet-backdrop: rgba(0, 0, 0, 0.55);
}
*/
/* kodza-guard 89fced0e7f53 — 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-sheet__trigger):not(#_),
:where(.kodza-sheet__close):not(#_),
:where(.kodza-sheet__cta):not(#_) {
outline: revert;
outline-offset: revert;
}
:where(.kodza-sheet__panel):not(#_) {
transition: revert;
}
:where(.kodza-sheet__foot):not(#_) {
transition: revert;
position: revert;
background-color: revert;
background-image: revert;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-sheet:not(#_),
.kodza-sheet__grip:not(#_),
.kodza-sheet__name:not(#_),
.kodza-sheet__price:not(#_),
.kodza-sheet__grip span:where(:not([class*="kodza-"])):not(#_),
.kodza-sheet__total span:where(:not([class*="kodza-"])):not(#_) {
box-sizing: revert;
}
.kodza-sheet__trigger:not(#_),
.kodza-sheet__cta: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;
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-sheet__panel:not(#_) {
outline: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: revert;
outline-offset: revert;
appearance: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
}
.kodza-sheet__head:not(#_) {
border: revert;
margin: 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-sheet__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;
line-height: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
}
.kodza-sheet__close: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-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-sheet__items:not(#_) {
border: revert;
border-radius: revert;
outline: revert;
text-decoration: revert;
transition: 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-sheet__item:not(#_) {
margin: revert;
border-radius: revert;
outline: revert;
text-decoration: revert;
transition: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: revert;
border-top-width: revert;
border-right-width: revert;
border-left-width: revert;
border-top-style: revert;
border-right-style: revert;
border-left-style: revert;
border-top-color: revert;
border-right-color: revert;
border-left-color: 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-sheet__foot:not(#_) {
margin: revert;
border-radius: revert;
outline: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: revert;
border-right-width: revert;
border-bottom-width: revert;
border-left-width: revert;
border-right-style: revert;
border-bottom-style: revert;
border-left-style: revert;
border-right-color: revert;
border-bottom-color: revert;
border-left-color: 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-sheet__total: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;
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-sheet__note: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;
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-sheet__close 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-sheet__item img:where(:not([class*="kodza-"])):not(#_) {
border: revert;
margin: revert;
padding: 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-sheet__name small: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;
font-family: 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-sheet__total 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-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-sheet__title:not(#_):where(:not([hidden])),
.kodza-sheet__items:not(#_):where(:not([hidden])),
.kodza-sheet__note:not(#_):where(:not([hidden])),
.kodza-sheet__close svg:where(:not([class*="kodza-"])):not(#_):where(:not([hidden])),
.kodza-sheet__item img:where(:not([class*="kodza-"])):not(#_):where(:not([hidden])),
.kodza-sheet__name small:where(:not([class*="kodza-"])):not(#_):where(:not([hidden])),
.kodza-sheet__total b:where(:not([class*="kodza-"])):not(#_):where(:not([hidden])) {
display: revert;
}
.kodza-sheet__title:not(#_):where([hidden]),
.kodza-sheet__items:not(#_):where([hidden]),
.kodza-sheet__note:not(#_):where([hidden]),
.kodza-sheet__close svg:where(:not([class*="kodza-"])):not(#_):where([hidden]),
.kodza-sheet__item img:where(:not([class*="kodza-"])):not(#_):where([hidden]),
.kodza-sheet__name small:where(:not([class*="kodza-"])):not(#_):where([hidden]),
.kodza-sheet__total b:where(:not([class*="kodza-"])):not(#_):where([hidden]) {
display: none;
}
/* The component's own rules, one step stronger: real buttons and fields, what it sets on tags themes style (links, lists, headings, images…), and the root width (a theme may narrow it, never widen it) */
.kodza-sheet__trigger:not(#_),
.kodza-sheet__cta:not(#_) {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 44px;
padding: 0 18px;
border: 0;
border-radius: 999px;
background: var(--kz-sheet-ink);
color: var(--kz-sheet-on-ink);
font: inherit;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.2s ease, transform 0.2s ease;
}
.kodza-sheet__trigger:hover:not(#_),
.kodza-sheet__cta:hover:not(#_) {
opacity: 0.86;
}
.kodza-sheet__trigger:active:not(#_),
.kodza-sheet__cta:active:not(#_) {
transform: scale(0.97);
}
.kodza-sheet__trigger:focus-visible:not(#_),
.kodza-sheet__cta:focus-visible:not(#_),
.kodza-sheet__close:focus-visible:not(#_) {
outline: 2px solid var(--kz-sheet-fg);
outline-offset: 3px;
}
.kodza-sheet__panel:not(#_) {
position: fixed;
display: grid;
max-width: none;
max-height: none;
margin: 0;
padding: 0;
border: 0;
border-radius: var(--kz-sheet-radius);
background: var(--kz-sheet-bg);
box-shadow: 0 0 0 1px var(--kz-sheet-line), -20px 0 60px -20px rgba(10, 10, 10, 0.3);
color: var(--kz-sheet-fg);
font: inherit;
}
.kodza-sheet__panel:not([open]):not(#_) {
display: none;
}
.kodza-sheet__panel.is-moving:not(#_) {
transition: transform var(--kz-sheet-duration) var(--kz-sheet-ease);
}
.kodza-sheet__head:not(#_) {
display: flex;
padding: 20px 20px 12px 24px;
}
.kodza-sheet__title:not(#_) {
margin: 0;
font-size: 22px;
font-weight: 600;
letter-spacing: -0.025em;
}
.kodza-sheet__close:not(#_) {
display: grid;
place-items: center;
width: 32px;
height: 32px;
padding: 0;
border: 0;
border-radius: 50%;
background: var(--kz-sheet-soft);
color: var(--kz-sheet-fg);
cursor: pointer;
transition: background-color 0.2s ease;
}
.kodza-sheet__close:hover:not(#_) {
background: var(--kz-sheet-line);
}
.kodza-sheet__items:not(#_) {
margin: 0;
padding: 0 24px;
list-style: none;
}
.kodza-sheet__item:not(#_) {
display: grid;
padding: 14px 0;
border-bottom: 1px solid var(--kz-sheet-line);
}
.kodza-sheet__item img:not(#_) {
border-radius: 12px;
}
.kodza-sheet__name small:not(#_) {
font-size: 13px;
font-weight: 400;
color: var(--kz-sheet-muted);
}
.kodza-sheet__foot:not(#_) {
display: grid;
padding: 16px 24px 24px;
border-top: 1px solid var(--kz-sheet-line);
}
.kodza-sheet__total:not(#_) {
display: flex;
margin: 0;
font-size: 15px;
font-weight: 500;
}
.kodza-sheet__total b:not(#_) {
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-weight: 500;
}
.kodza-sheet__note:not(#_) {
margin: 0 0 6px;
font-size: 13px;
color: var(--kz-sheet-muted);
}
.kodza-sheet__cta:not(#_) {
width: 100%;
height: 48px;
}
@media (max-width: 640px) {
.kodza-sheet__panel:not(#_) {
border-radius: var(--kz-sheet-radius) var(--kz-sheet-radius) 0 0;
box-shadow: 0 -20px 60px -20px rgba(10, 10, 10, 0.3);
}
}
@media (max-width: 640px) {
.kodza-sheet__head:not(#_) {
padding-top: 4px;
}
}
@media (max-width: 640px) {
.kodza-sheet__foot:not(#_) {
position: relative;
background: var(--kz-sheet-bg);
}
}
@media (max-width: 640px) {
.kodza-sheet__panel.is-moving .kodza-sheet__foot:not(#_) {
transition: transform var(--kz-sheet-duration) var(--kz-sheet-ease);
}
}
@media (prefers-reduced-motion: reduce) {
.kodza-sheet__panel.is-moving:not(#_) {
transition-duration: 0.01ms !important;
}
}
@media (prefers-reduced-motion: reduce) {
.kodza-sheet__trigger:not(#_),
.kodza-sheet__cta: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 — Sheet
// Opens the panel as a modal dialog and slides it in; closes it with the slide (close button,
// Escape, a click on the page) and gives the focus back. On phones, the sheet can be dragged by
// its grip or its header: it rests at the half or full stop nearest to where it is let go, and
// a quick flick down (or a drag below the half stop) closes it.
(() => {
const SELECTOR = '.kodza-sheet';
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// The page behind stays put while it is open: a wheel or a swipe only scrolls what can
// scroll inside, under the pointer. Listening only while open costs nothing the rest of the time.
const holdPage = (layer, signal) => {
const inside = (target, x, y, dy) => {
for (let el = target; el instanceof Element && layer.contains(el); el = el.parentElement) {
const box = el.getBoundingClientRect();
if (x < box.left || x > box.right || y < box.top || y > box.bottom) continue;
if (!/(auto|scroll)/.test(getComputedStyle(el).overflowY) || el.scrollHeight <= el.clientHeight + 1) continue;
if (dy > 0 ? el.scrollTop + el.clientHeight < el.scrollHeight - 1 : el.scrollTop > 0) return true;
}
return false;
};
let last = 0;
const wheel = (event) => !inside(event.target, event.clientX, event.clientY, event.deltaY) && event.preventDefault();
const touchstart = (event) => (last = event.touches[0].clientY);
const touchmove = (event) => {
const touch = event.touches[0];
const dy = last - touch.clientY;
last = touch.clientY;
if (!inside(event.target, touch.clientX, touch.clientY, dy)) event.preventDefault();
};
const hold = (on) => {
const method = on ? 'addEventListener' : 'removeEventListener';
document[method]('wheel', wheel, { passive: false });
document[method]('touchstart', touchstart, { passive: true });
document[method]('touchmove', touchmove, { passive: false });
};
const observer = new MutationObserver(() => hold(layer.open));
observer.observe(layer, { attributes: true, attributeFilter: ['open'] });
// Stopped while open: the page is let go
signal.addEventListener('abort', () => {
observer.disconnect();
hold(false);
});
};
const phone = window.matchMedia('(max-width: 640px)');
let uid = 0;
// One instance. Everything it starts is tied to `signal`, so kodza:destroy stops it all
function mount(root, signal) {
const trigger = root.querySelector('.kodza-sheet__trigger');
const panel = root.querySelector('.kodza-sheet__panel');
holdPage(panel, signal);
if (!trigger || !panel) return;
const title = panel.querySelector('.kodza-sheet__title');
if (title) {
title.id = title.id || `kodza-sheet-${++uid}`;
panel.setAttribute('aria-labelledby', title.id);
}
trigger.setAttribute('aria-haspopup', 'dialog');
// The cascade order
[panel.querySelector('.kodza-sheet__head'), ...panel.querySelectorAll('.kodza-sheet__item'), panel.querySelector('.kodza-sheet__foot')]
.filter(Boolean)
.forEach((el, i) => el.style.setProperty('--i', i));
const duration = reduceMotion ? 0 : parseFloat(getComputedStyle(root).getPropertyValue('--kz-sheet-duration')) * 1000 || 550;
let timer = 0;
// The half stop: how far down the sheet rests when half open, in pixels
const halfStop = () => Math.round(panel.offsetHeight * 0.45);
const setY = (y) => panel.style.setProperty('--kz-sheet-y', `${Math.max(0, y)}px`);
const open = () => {
clearTimeout(timer);
if (!panel.open) {
panel.showModal();
}
setY(phone.matches ? halfStop() : 0);
panel.classList.add('is-moving');
requestAnimationFrame(() => requestAnimationFrame(() => panel.classList.add('is-open')));
trigger.setAttribute('aria-expanded', 'true');
};
const close = () => {
if (!panel.open || !panel.classList.contains('is-open')) return;
panel.classList.add('is-moving');
panel.classList.remove('is-open');
trigger.setAttribute('aria-expanded', 'false');
clearTimeout(timer);
timer = setTimeout(() => {
panel.close();
panel.classList.remove('is-moving');
trigger.focus({ preventScroll: true });
}, duration);
};
trigger.addEventListener('click', open, { signal });
panel.querySelector('.kodza-sheet__close')?.addEventListener('click', close, { signal });
panel.addEventListener('cancel', (event) => {
event.preventDefault();
close();
}, { signal });
panel.addEventListener('click', (event) => {
if (event.target !== panel) return;
const box = panel.getBoundingClientRect();
if (event.clientX < box.left || event.clientX > box.right || event.clientY < box.top || event.clientY > box.bottom) close();
}, { signal });
// Dragging the sheet on phones
let start = null;
const handles = [panel.querySelector('.kodza-sheet__grip'), panel.querySelector('.kodza-sheet__head')].filter(Boolean);
handles.forEach((handle) => {
handle.addEventListener('pointerdown', (event) => {
if (!phone.matches || event.target.closest('button')) return;
const y = parseFloat(getComputedStyle(panel).getPropertyValue('--kz-sheet-y')) || 0;
start = { pointer: event.clientY, y, time: performance.now(), last: event.clientY };
handle.setPointerCapture(event.pointerId);
panel.classList.remove('is-moving');
}, { signal });
handle.addEventListener('pointermove', (event) => {
if (!start) return;
const dy = event.clientY - start.pointer;
// Above the full stop, it resists like a rubber band
const y = start.y + dy;
setY(y < 0 ? y / 4 : y);
start.velocity = (event.clientY - start.last) / Math.max(1, performance.now() - start.time);
start.last = event.clientY;
start.time = performance.now();
}, { signal });
const release = () => {
if (!start) return;
const y = parseFloat(getComputedStyle(panel).getPropertyValue('--kz-sheet-y')) || 0;
const half = halfStop();
const flick = (start.velocity || 0) > 0.6;
start = null;
panel.classList.add('is-moving');
if (flick || y > half + panel.offsetHeight * 0.18) return close();
setY(y < half / 2 ? 0 : half);
};
handle.addEventListener('pointerup', release, { signal });
handle.addEventListener('pointercancel', release, { signal });
});
// Stopped while open (or closing): the dialog closes and the page scrolls again
signal.addEventListener('abort', () => {
clearTimeout(timer);
if (!panel.open) return;
panel.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.
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 — Sheet, for React. From https://kodza.dev/components/drawer/
// 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: drawer.css and drawer.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 './drawer.css';
export type SheetProps = Omit<ComponentPropsWithoutRef<'div'>, "children" | "vars"> & {
/** Settings, as CSS variables on the root */
vars?: {
/** default: #ffffff */
"--kz-sheet-bg"?: string | number;
/** default: #0a0a0a */
"--kz-sheet-fg"?: string | number;
/** default: rgba(10, 10, 10, 0.6) */
"--kz-sheet-muted"?: string | number;
/** default: rgba(10, 10, 10, 0.1) */
"--kz-sheet-line"?: string | number;
/** default: rgba(10, 10, 10, 0.05) */
"--kz-sheet-soft"?: string | number;
/** default: #0a0a0a */
"--kz-sheet-ink"?: string | number;
/** default: #ffffff */
"--kz-sheet-on-ink"?: string | number;
/** default: rgba(10, 10, 10, 0.3) */
"--kz-sheet-backdrop"?: string | number;
/** default: 420px */
"--kz-sheet-width"?: string | number;
/** default: 20px */
"--kz-sheet-radius"?: string | number;
/** default: 0.55s */
"--kz-sheet-duration"?: string | number;
/** default: cubic-bezier(0.32, 0.72, 0, 1) */
"--kz-sheet-ease"?: string | number;
};
className?: string;
};
export function Sheet({ vars, className, style, ...rest }: SheetProps) {
const ref = useRef<ComponentRef<'div'>>(null);
const classes = ["kodza-sheet", 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('./drawer.js').then(() => {
if (live) document.dispatchEvent(new CustomEvent('kodza:init', { detail: { root } }));
});
return () => {
live = false;
document.dispatchEvent(new CustomEvent('kodza:destroy', { detail: { root } }));
};
}, [key]);
return (
<div key={key} {...rest} className={classes} style={{ ...vars, ...style } as CSSProperties} ref={ref}>
<button className="kodza-sheet__trigger" type="button">Cart <span className="kodza-sheet__badge">3</span></button>
<dialog className="kodza-sheet__panel">
<div className="kodza-sheet__grip" aria-hidden="true">
<span />
</div>
<header className="kodza-sheet__head">
<h2 className="kodza-sheet__title">Your cart</h2>
<button className="kodza-sheet__close" type="button" aria-label="Close">
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M4 4l8 8M12 4l-8 8" />
</svg>
</button>
</header>
<ul className="kodza-sheet__items">
<li className="kodza-sheet__item">
<img src="https://kodza.dev/media/kz-block-1400.webp" alt="" />
<span className="kodza-sheet__name">Fill Button<small>HTML, CSS, a pinch of JS</small></span>
<span className="kodza-sheet__price">€0.00</span>
</li>
<li className="kodza-sheet__item">
<img src="https://kodza.dev/media/kz-stack-1400.webp" alt="" />
<span className="kodza-sheet__name">Sticky Cards<small>Scroll, then scroll some more</small></span>
<span className="kodza-sheet__price">€0.00</span>
</li>
<li className="kodza-sheet__item">
<img src="https://kodza.dev/media/kz-arch-1400.webp" alt="" />
<span className="kodza-sheet__name">Date Picker<small>Fridays included, at your own risk</small></span>
<span className="kodza-sheet__price">€0.00</span>
</li>
</ul>
<footer className="kodza-sheet__foot">
<p className="kodza-sheet__total">
<span>Total</span>
<b>€0.00</b>
</p>
<p className="kodza-sheet__note">Everything is free. The cart is just for the feeling.</p>
<button className="kodza-sheet__cta" type="button">Copy everything</button>
</footer>
</dialog>
</div>
);
}
Checked
Robots poked at it before you did. Every component goes through the same checks, and again each time it changes.