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 real dialog that grows out of the button that opened it and goes back into it. The alert version asks you to type the name before it deletes anything.
With your AI
Say it to your AI. It gets the exact code below, not its best guess, and knows where it goes.
Confirm account deletion with a Kodza Dialog.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<div class="kodza-dialog">
<button class="kodza-dialog__trigger" type="button">Edit profile</button>
<dialog class="kodza-dialog__panel">
<form class="kodza-dialog__form" method="dialog">
<header class="kodza-dialog__head">
<h2 class="kodza-dialog__title">Edit profile</h2>
<p class="kodza-dialog__text">This is how other devs see you. Choose wisely.</p>
</header>
<div class="kodza-dialog__body">
<label class="kodza-dialog__field"><span>Name</span><input type="text" name="name" value="Ada Lovelace" autocomplete="name"></label>
<label class="kodza-dialog__field"><span>Bio</span><input type="text" name="bio" value="Wrote the first program. Still no merge conflicts." autocomplete="off"></label>
</div>
<footer class="kodza-dialog__foot">
<button class="kodza-dialog__button" type="submit" value="cancel" formnovalidate>Cancel</button>
<button class="kodza-dialog__button kodza-dialog__button--ink" type="submit" value="save">Save changes</button>
</footer>
<button class="kodza-dialog__close" type="submit" value="cancel" formnovalidate aria-label="Close"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8"/></svg></button>
</form>
</dialog>
</div>
<div class="kodza-dialog kodza-dialog--alert" data-confirm="kodza-site">
<button class="kodza-dialog__trigger kodza-dialog__trigger--danger" type="button">Delete project</button>
<dialog class="kodza-dialog__panel">
<form class="kodza-dialog__form" method="dialog">
<header class="kodza-dialog__head">
<span class="kodza-dialog__icon" aria-hidden="true"><svg viewBox="0 0 20 20"><path d="M10 3 18 17H2z"/><path d="M10 8v4M10 14.5v.2"/></svg></span>
<h2 class="kodza-dialog__title">Delete kodza-site?</h2>
<p class="kodza-dialog__text">This deletes the project, its 214 deploys and every "final-v3" file. There is no undo, and no backup either. We checked.</p>
</header>
<div class="kodza-dialog__body">
<label class="kodza-dialog__field"><span>Type <b>kodza-site</b> to confirm</span><input class="kodza-dialog__confirm" type="text" name="confirm" autocomplete="off" spellcheck="false"></label>
</div>
<footer class="kodza-dialog__foot">
<button class="kodza-dialog__button" type="submit" value="cancel" formnovalidate>Keep it</button>
<button class="kodza-dialog__button kodza-dialog__button--ink kodza-dialog__go" type="submit" value="delete" disabled>Delete forever</button>
</footer>
</form>
</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 — Dialog
A real dialog element that grows out of the button that opened it and goes back into it when it
closes, over a blurred, dimmed page. The focus stays inside, Escape and a click on the page
close it, and the page behind does not scroll. --alert is the destructive confirmation:
role="alertdialog", and the button only wakes up once the name is typed (data-confirm).
Everything is tuned with the --kz-dialog-* variables. */
.kodza-dialog {
--kz-dialog-bg: #ffffff;
--kz-dialog-fg: #0a0a0a;
--kz-dialog-muted: rgba(10, 10, 10, 0.6);
--kz-dialog-line: rgba(10, 10, 10, 0.12);
--kz-dialog-soft: rgba(10, 10, 10, 0.05);
--kz-dialog-ink: #0a0a0a;
--kz-dialog-on-ink: #ffffff;
--kz-dialog-backdrop: rgba(10, 10, 10, 0.3);
--kz-dialog-shadow: 0 30px 70px -20px rgba(10, 10, 10, 0.45);
--kz-dialog-width: 440px;
--kz-dialog-radius: 20px;
--kz-dialog-ease: cubic-bezier(0.22, 1, 0.36, 1);
display: inline-block;
color: var(--kz-dialog-fg);
}
.kodza-dialog__trigger,
.kodza-dialog__button {
display: inline-flex;
align-items: center;
justify-content: center;
height: 44px;
padding: 0 20px;
border: 0;
border-radius: 999px;
background: var(--kz-dialog-bg);
box-shadow: inset 0 0 0 1px var(--kz-dialog-line);
color: var(--kz-dialog-fg);
font: inherit;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: box-shadow 0.25s ease, background-color 0.25s ease, opacity 0.25s ease, transform 0.2s var(--kz-dialog-ease);
}
.kodza-dialog__trigger:hover,
.kodza-dialog__button:hover {
box-shadow: inset 0 0 0 1px var(--kz-dialog-muted);
}
.kodza-dialog__trigger:active,
.kodza-dialog__button:active {
transform: scale(0.97);
}
.kodza-dialog__trigger--danger,
.kodza-dialog__button--ink {
background: var(--kz-dialog-ink);
box-shadow: none;
color: var(--kz-dialog-on-ink);
}
.kodza-dialog__trigger--danger:hover,
.kodza-dialog__button--ink:hover {
box-shadow: none;
opacity: 0.86;
}
.kodza-dialog__button:disabled {
cursor: not-allowed;
opacity: 0.35;
transform: none;
}
.kodza-dialog__trigger:focus-visible,
.kodza-dialog__button:focus-visible,
.kodza-dialog__close:focus-visible {
outline: 2px solid var(--kz-dialog-fg);
outline-offset: 3px;
}
/* The panel */
.kodza-dialog__panel {
width: min(var(--kz-dialog-width), calc(100% - 32px));
max-width: none;
max-height: calc(100% - 48px);
margin: auto;
padding: 0;
overflow: auto;
overscroll-behavior: contain;
/* A field reached by Tab stops above the pinned actions, not under them */
scroll-padding-bottom: 96px;
border: 0;
border-radius: var(--kz-dialog-radius);
background: var(--kz-dialog-bg);
box-shadow: 0 0 0 1px var(--kz-dialog-line), var(--kz-dialog-shadow);
color: var(--kz-dialog-fg);
font: inherit;
}
.kodza-dialog__panel::backdrop {
background: transparent;
-webkit-backdrop-filter: blur(0);
backdrop-filter: blur(0);
transition: background-color 0.35s ease, backdrop-filter 0.35s ease;
}
.kodza-dialog__panel.is-open::backdrop {
background: var(--kz-dialog-backdrop);
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
}
.kodza-dialog__form {
position: relative;
display: flex;
flex-direction: column;
gap: 24px;
margin: 0;
padding: 28px 28px 0;
}
.kodza-dialog__head {
display: grid;
gap: 8px;
padding-right: 32px;
}
.kodza-dialog__icon {
display: grid;
place-items: center;
width: 40px;
height: 40px;
margin-bottom: 8px;
border-radius: 12px;
background: var(--kz-dialog-soft);
}
.kodza-dialog__icon svg {
width: 20px;
height: 20px;
fill: none;
stroke: currentColor;
stroke-width: 1.6;
stroke-linecap: round;
stroke-linejoin: round;
}
.kodza-dialog__title {
margin: 0;
font-size: 22px;
font-weight: 600;
line-height: 1.2;
letter-spacing: -0.025em;
}
.kodza-dialog__text {
margin: 0;
font-size: 15px;
line-height: 1.55;
color: var(--kz-dialog-muted);
text-wrap: pretty;
}
/* The content arrives a beat after the panel, line by line (the buttons, not the pinned foot:
its background stays opaque over the text running underneath) */
.kodza-dialog__panel.is-open .kodza-dialog__head > *,
.kodza-dialog__panel.is-open .kodza-dialog__body,
.kodza-dialog__panel.is-open .kodza-dialog__foot > * {
animation: kodza-dialog-in 0.5s var(--kz-dialog-ease) both;
}
.kodza-dialog__panel.is-open .kodza-dialog__head > :nth-child(2) {
animation-delay: 40ms;
}
.kodza-dialog__panel.is-open .kodza-dialog__head > :nth-child(3) {
animation-delay: 80ms;
}
.kodza-dialog__panel.is-open .kodza-dialog__body {
animation-delay: 110ms;
}
.kodza-dialog__panel.is-open .kodza-dialog__foot > * {
animation-delay: 150ms;
}
@keyframes kodza-dialog-in {
from {
opacity: 0;
transform: translateY(8px);
}
}
.kodza-dialog__body {
display: grid;
gap: 12px;
}
.kodza-dialog__field {
display: grid;
gap: 6px;
font-size: 13px;
color: var(--kz-dialog-muted);
}
.kodza-dialog__field b {
color: var(--kz-dialog-fg);
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-weight: 500;
}
.kodza-dialog__field input {
box-sizing: border-box;
height: 44px;
padding: 0 14px;
border: 0;
border-radius: 10px;
background: var(--kz-dialog-bg);
box-shadow: inset 0 0 0 1px var(--kz-dialog-line);
color: var(--kz-dialog-fg);
font: inherit;
font-size: 15px;
outline: none;
transition: box-shadow 0.2s ease;
}
.kodza-dialog__field input:focus {
box-shadow: inset 0 0 0 1.5px var(--kz-dialog-fg), 0 0 0 4px var(--kz-dialog-soft);
}
/* On a short screen the text scrolls and the actions stay pinned at the bottom, a line above
them once the text runs underneath (is-clipped, set by the script) */
.kodza-dialog__foot {
position: sticky;
bottom: 0;
z-index: 1;
display: flex;
justify-content: flex-end;
gap: 8px;
margin: -16px -28px 0;
padding: 16px 28px 28px;
background: var(--kz-dialog-bg);
box-shadow: 0 -1px 0 transparent;
transition: box-shadow 0.2s ease;
}
.kodza-dialog__panel.is-clipped .kodza-dialog__foot {
box-shadow: 0 -1px 0 var(--kz-dialog-line);
}
.kodza-dialog__close {
position: absolute;
top: 18px;
right: 18px;
display: grid;
place-items: center;
width: 32px;
height: 32px;
padding: 0;
border: 0;
border-radius: 50%;
background: none;
color: var(--kz-dialog-muted);
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease;
}
.kodza-dialog__close:hover {
background: var(--kz-dialog-soft);
color: var(--kz-dialog-fg);
}
.kodza-dialog__close svg {
width: 14px;
height: 14px;
fill: none;
stroke: currentColor;
stroke-width: 1.6;
stroke-linecap: round;
}
/* A shake when the alert is dismissed by a click on the page: it insists */
.kodza-dialog__panel.is-shaking {
animation: kodza-dialog-shake 0.4s cubic-bezier(0.36, 0, 0.66, -0.56);
}
@keyframes kodza-dialog-shake {
25% {
transform: scale(1.02);
}
60% {
transform: scale(0.99);
}
}
@media (max-width: 520px) {
.kodza-dialog__panel {
scroll-padding-bottom: 148px;
}
.kodza-dialog__foot {
flex-direction: column-reverse;
}
.kodza-dialog__foot .kodza-dialog__button {
width: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
.kodza-dialog__panel::backdrop,
.kodza-dialog__trigger,
.kodza-dialog__button {
transition-duration: 0.01ms !important;
}
.kodza-dialog__panel.is-open .kodza-dialog__head > *,
.kodza-dialog__panel.is-open .kodza-dialog__body,
.kodza-dialog__panel.is-open .kodza-dialog__foot > *,
.kodza-dialog__panel.is-shaking {
animation: none;
}
}
/* Theming example — on a dark background:
.dark-section .kodza-dialog {
--kz-dialog-bg: #161616;
--kz-dialog-fg: #f2f2f0;
--kz-dialog-muted: rgba(242, 242, 240, 0.62);
--kz-dialog-line: rgba(255, 255, 255, 0.14);
--kz-dialog-soft: rgba(255, 255, 255, 0.07);
--kz-dialog-ink: #f2f2f0;
--kz-dialog-on-ink: #0a0a0a;
--kz-dialog-backdrop: rgba(0, 0, 0, 0.55);
--kz-dialog-shadow: 0 30px 70px -20px rgba(0, 0, 0, 0.9);
}
*/
/* kodza-guard cdf1407b6190 — 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-dialog__trigger):not(#_),
:where(.kodza-dialog__button):not(#_),
:where(.kodza-dialog__close):not(#_) {
outline: revert;
outline-offset: revert;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-dialog:not(#_),
.kodza-dialog__form:not(#_),
.kodza-dialog__body:not(#_),
.kodza-dialog__icon:not(#_),
.kodza-dialog__field span:where(:not([class*="kodza-"])):not(#_) {
box-sizing: revert;
}
.kodza-dialog__trigger:not(#_),
.kodza-dialog__button:not(#_) {
margin: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: 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-dialog__panel:not(#_) {
outline: revert;
text-decoration: revert;
transition: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: 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-dialog__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;
padding-top: revert;
padding-bottom: revert;
padding-left: 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-dialog__title:not(#_) {
border: revert;
padding: revert;
border-radius: revert;
outline: revert;
text-decoration: revert;
transition: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: revert;
background-color: revert;
background-image: revert;
box-shadow: revert;
outline-offset: revert;
appearance: revert;
max-width: revert;
max-height: revert;
color: revert;
font-family: revert;
font-style: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
}
.kodza-dialog__text:not(#_) {
border: revert;
padding: revert;
border-radius: revert;
outline: revert;
text-decoration: revert;
transition: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: revert;
background-color: revert;
background-image: revert;
box-shadow: revert;
outline-offset: revert;
appearance: revert;
max-width: revert;
max-height: revert;
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;
}
.kodza-dialog__field: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-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-dialog__foot:not(#_) {
border: revert;
border-radius: revert;
outline: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: 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-dialog__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-dialog__confirm:not(#_),
.kodza-dialog__field input:where(:not([class*="kodza-"])):not(#_) {
margin: revert;
text-decoration: revert;
list-style: revert;
float: revert;
position: revert;
outline-offset: 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-dialog__close svg:where(:not([class*="kodza-"])):not(#_),
.kodza-dialog__icon 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-dialog__field 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;
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-dialog__panel:not(#_):where(:not([hidden])),
.kodza-dialog__title:not(#_):where(:not([hidden])),
.kodza-dialog__text:not(#_):where(:not([hidden])),
.kodza-dialog__confirm:not(#_):where(:not([hidden])),
.kodza-dialog__field input:where(:not([class*="kodza-"])):not(#_):where(:not([hidden])),
.kodza-dialog__close svg:where(:not([class*="kodza-"])):not(#_):where(:not([hidden])),
.kodza-dialog__icon svg:where(:not([class*="kodza-"])):not(#_):where(:not([hidden])),
.kodza-dialog__field b:where(:not([class*="kodza-"])):not(#_):where(:not([hidden])) {
display: revert;
}
.kodza-dialog__panel:not(#_):where([hidden]),
.kodza-dialog__title:not(#_):where([hidden]),
.kodza-dialog__text:not(#_):where([hidden]),
.kodza-dialog__confirm:not(#_):where([hidden]),
.kodza-dialog__field input:where(:not([class*="kodza-"])):not(#_):where([hidden]),
.kodza-dialog__close svg:where(:not([class*="kodza-"])):not(#_):where([hidden]),
.kodza-dialog__icon svg:where(:not([class*="kodza-"])):not(#_):where([hidden]),
.kodza-dialog__field 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-dialog__trigger:not(#_),
.kodza-dialog__button:not(#_) {
display: inline-flex;
align-items: center;
justify-content: center;
height: 44px;
padding: 0 20px;
border: 0;
border-radius: 999px;
background: var(--kz-dialog-bg);
box-shadow: inset 0 0 0 1px var(--kz-dialog-line);
color: var(--kz-dialog-fg);
font: inherit;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: box-shadow 0.25s ease, background-color 0.25s ease, opacity 0.25s ease, transform 0.2s var(--kz-dialog-ease);
}
.kodza-dialog__trigger:hover:not(#_),
.kodza-dialog__button:hover:not(#_) {
box-shadow: inset 0 0 0 1px var(--kz-dialog-muted);
}
.kodza-dialog__trigger:active:not(#_),
.kodza-dialog__button:active:not(#_) {
transform: scale(0.97);
}
.kodza-dialog__trigger--danger:not(#_),
.kodza-dialog__button--ink:not(#_) {
background: var(--kz-dialog-ink);
box-shadow: none;
color: var(--kz-dialog-on-ink);
}
.kodza-dialog__trigger--danger:hover:not(#_),
.kodza-dialog__button--ink:hover:not(#_) {
box-shadow: none;
opacity: 0.86;
}
.kodza-dialog__button:disabled:not(#_) {
cursor: not-allowed;
opacity: 0.35;
transform: none;
}
.kodza-dialog__trigger:focus-visible:not(#_),
.kodza-dialog__button:focus-visible:not(#_),
.kodza-dialog__close:focus-visible:not(#_) {
outline: 2px solid var(--kz-dialog-fg);
outline-offset: 3px;
}
.kodza-dialog__panel:not(#_) {
max-width: none;
max-height: calc(100% - 48px);
margin: auto;
padding: 0;
border: 0;
border-radius: var(--kz-dialog-radius);
background: var(--kz-dialog-bg);
box-shadow: 0 0 0 1px var(--kz-dialog-line), var(--kz-dialog-shadow);
color: var(--kz-dialog-fg);
font: inherit;
}
.kodza-dialog__head:not(#_) {
display: grid;
padding-right: 32px;
}
.kodza-dialog__title:not(#_) {
margin: 0;
font-size: 22px;
font-weight: 600;
line-height: 1.2;
letter-spacing: -0.025em;
}
.kodza-dialog__text:not(#_) {
margin: 0;
font-size: 15px;
line-height: 1.55;
color: var(--kz-dialog-muted);
}
.kodza-dialog__panel.is-open .kodza-dialog__foot > *:not(#_) {
animation: kodza-dialog-in 0.5s var(--kz-dialog-ease) both;
}
.kodza-dialog__panel.is-open .kodza-dialog__foot > *:not(#_) {
animation-delay: 150ms;
}
.kodza-dialog__field:not(#_) {
display: grid;
font-size: 13px;
color: var(--kz-dialog-muted);
}
.kodza-dialog__field b:not(#_) {
color: var(--kz-dialog-fg);
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-weight: 500;
}
.kodza-dialog__field input:not(#_) {
box-sizing: border-box;
height: 44px;
padding: 0 14px;
border: 0;
border-radius: 10px;
background: var(--kz-dialog-bg);
box-shadow: inset 0 0 0 1px var(--kz-dialog-line);
color: var(--kz-dialog-fg);
font: inherit;
font-size: 15px;
outline: none;
transition: box-shadow 0.2s ease;
}
.kodza-dialog__field input:focus:not(#_) {
box-shadow: inset 0 0 0 1.5px var(--kz-dialog-fg), 0 0 0 4px var(--kz-dialog-soft);
}
.kodza-dialog__foot:not(#_) {
position: sticky;
display: flex;
margin: -16px -28px 0;
padding: 16px 28px 28px;
background: var(--kz-dialog-bg);
box-shadow: 0 -1px 0 transparent;
transition: box-shadow 0.2s ease;
}
.kodza-dialog__panel.is-clipped .kodza-dialog__foot:not(#_) {
box-shadow: 0 -1px 0 var(--kz-dialog-line);
}
.kodza-dialog__close:not(#_) {
position: absolute;
top: 18px;
right: 18px;
display: grid;
place-items: center;
width: 32px;
height: 32px;
padding: 0;
border: 0;
border-radius: 50%;
background: none;
color: var(--kz-dialog-muted);
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease;
}
.kodza-dialog__close:hover:not(#_) {
background: var(--kz-dialog-soft);
color: var(--kz-dialog-fg);
}
@media (max-width: 520px) {
.kodza-dialog__foot .kodza-dialog__button:not(#_) {
width: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
.kodza-dialog__trigger:not(#_),
.kodza-dialog__button:not(#_) {
transition-duration: 0.01ms !important;
}
}
@media (prefers-reduced-motion: reduce) {
.kodza-dialog__panel.is-open .kodza-dialog__foot > *:not(#_) {
animation: none;
}
}
// Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license
// Kodza — Dialog
// Opens the <dialog> out of its trigger (the panel scales from the button's position) and sends
// it back there on close, whatever closes it: a button of the form (method="dialog"), Escape, or
// a click on the page. The alert version (--alert) is an alertdialog: a click on the page makes
// it insist instead of closing, and its main button waits for the name in data-confirm.
// Fires "kodza:close" with { value } (the button's value, or "cancel").
(() => {
const SELECTOR = '.kodza-dialog';
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 ease = 'cubic-bezier(0.22, 1, 0.36, 1)';
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-dialog__trigger');
const panel = root.querySelector('.kodza-dialog__panel');
holdPage(panel, signal);
if (!trigger || !panel) return;
const alert = root.classList.contains('kodza-dialog--alert');
const id = `kodza-dialog-${++uid}`;
const title = panel.querySelector('.kodza-dialog__title');
const text = panel.querySelector('.kodza-dialog__text');
if (title) panel.setAttribute('aria-labelledby', (title.id = `${id}-title`));
if (text) panel.setAttribute('aria-describedby', (text.id = `${id}-text`));
if (alert) panel.setAttribute('role', 'alertdialog');
trigger.setAttribute('aria-haspopup', 'dialog');
// The confirmation: the main button wakes up once the name is typed
const confirm = panel.querySelector('.kodza-dialog__confirm');
const go = panel.querySelector('.kodza-dialog__go');
if (confirm && go && root.dataset.confirm) {
confirm.addEventListener('input', () => (go.disabled = confirm.value.trim() !== root.dataset.confirm), { signal });
}
// A short screen: the text runs under the pinned actions, and a line shows it. Measured on
// the form's layout height, which the lines arriving from 8px lower do not stretch
const form = panel.querySelector('.kodza-dialog__form');
const clip = () => form && panel.classList.toggle('is-clipped', panel.scrollTop + panel.clientHeight < form.offsetHeight - 1);
panel.addEventListener('scroll', clip, { signal, passive: true });
const sizes = new ResizeObserver(clip);
sizes.observe(panel);
if (form) sizes.observe(form);
signal.addEventListener('abort', () => sizes.disconnect());
// Where the trigger sits, seen from the panel: the panel grows from there
const fromTrigger = () => {
const a = trigger.getBoundingClientRect();
const b = panel.getBoundingClientRect();
const x = a.left + a.width / 2 - b.left;
const y = a.top + a.height / 2 - b.top;
const scale = Math.max(0.08, Math.min(a.width / b.width, a.height / b.height));
return { origin: `${x}px ${y}px`, scale };
};
let closing = false;
const open = () => {
if (panel.open) return;
if (confirm) {
confirm.value = '';
if (go) go.disabled = true;
}
panel.showModal();
panel.classList.add('is-open');
if (reduceMotion) return;
const { origin, scale } = fromTrigger();
panel.style.transformOrigin = origin;
panel.animate([
{ transform: `scale(${scale})`, opacity: 0 },
{ opacity: 1, offset: 0.3 },
{ transform: 'none', opacity: 1 },
], { duration: 520, easing: ease });
};
// Back into the trigger, then really closed
const shut = (value) => {
if (!panel.open || closing) return;
closing = true;
panel.classList.remove('is-open');
const finish = () => {
if (signal.aborted) return;
closing = false;
panel.close(value);
// Back to the button that opened it (Safari does not focus buttons on click)
trigger.focus({ preventScroll: true });
root.dispatchEvent(new CustomEvent('kodza:close', { detail: { value }, bubbles: true }));
};
if (reduceMotion) return finish();
const { origin, scale } = fromTrigger();
panel.style.transformOrigin = origin;
panel.animate([
{ transform: 'none', opacity: 1 },
{ opacity: 1, offset: 0.6 },
{ transform: `scale(${scale})`, opacity: 0 },
], { duration: 380, easing: 'cubic-bezier(0.65, 0, 0.35, 1)' }).finished.then(finish);
};
trigger.addEventListener('click', open, { signal });
// A button of the form closes it with its value (the form would close it at once)
form?.addEventListener('submit', (event) => {
event.preventDefault();
shut(event.submitter?.value || 'cancel');
}, { signal });
// Enter in a field means the main button (the ink one), not the first of the form: Cancel
form?.addEventListener('keydown', (event) => {
if (event.key !== 'Enter' || event.isComposing || !/^(text|search|email|url|tel|password|number)$/.test(event.target.type || '')) return;
const main = form.querySelector('.kodza-dialog__button--ink');
if (!main) return;
event.preventDefault();
if (!main.disabled) form.requestSubmit(main);
}, { signal });
panel.addEventListener('cancel', (event) => {
event.preventDefault();
shut('cancel');
}, { signal });
// A click on the page: closes it, or makes an alert insist
panel.addEventListener('click', (event) => {
if (event.target !== panel) return;
const box = panel.getBoundingClientRect();
const inside = event.clientX >= box.left && event.clientX <= box.right && event.clientY >= box.top && event.clientY <= box.bottom;
if (inside) return;
if (!alert) return shut('cancel');
panel.classList.remove('is-shaking');
void panel.offsetWidth;
panel.classList.add('is-shaking');
}, { signal });
panel.addEventListener('animationend', (event) => event.animationName === 'kodza-dialog-shake' && panel.classList.remove('is-shaking'), { signal });
// Stopped while open (or closing): the dialog closes and the page scrolls again
signal.addEventListener('abort', () => {
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 — Dialog, for React. From https://kodza.dev/components/modal-component/
// 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: modal-component.css and modal-component.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 './modal-component.css';
type Variant = "alert";
export type DialogProps = Omit<ComponentPropsWithoutRef<'div'>, "children" | "variant" | "vars" | "confirm" | "onClose"> & {
/** Modifier class(es): .kodza-dialog--alert */
variant?: Variant | Variant[];
/** data-confirm on the root */
confirm?: "kodza-site";
/** Settings, as CSS variables on the root */
vars?: {
/** default: #ffffff */
"--kz-dialog-bg"?: string | number;
/** default: #0a0a0a */
"--kz-dialog-fg"?: string | number;
/** default: rgba(10, 10, 10, 0.6) */
"--kz-dialog-muted"?: string | number;
/** default: rgba(10, 10, 10, 0.12) */
"--kz-dialog-line"?: string | number;
/** default: rgba(10, 10, 10, 0.05) */
"--kz-dialog-soft"?: string | number;
/** default: #0a0a0a */
"--kz-dialog-ink"?: string | number;
/** default: #ffffff */
"--kz-dialog-on-ink"?: string | number;
/** default: rgba(10, 10, 10, 0.3) */
"--kz-dialog-backdrop"?: string | number;
/** default: 0 30px 70px -20px rgba(10, 10, 10, 0.45) */
"--kz-dialog-shadow"?: string | number;
/** default: 440px */
"--kz-dialog-width"?: string | number;
/** default: 20px */
"--kz-dialog-radius"?: string | number;
/** default: cubic-bezier(0.22, 1, 0.36, 1) */
"--kz-dialog-ease"?: string | number;
};
/** "kodza:close", fired by the script (bubbles from the root) */
onClose?: (event: CustomEvent) => void;
className?: string;
};
export function Dialog({ variant, confirm, vars, onClose, className, style, ...rest }: DialogProps) {
const ref = useRef<ComponentRef<'div'>>(null);
const classes = ["kodza-dialog", ...([] as string[]).concat(variant ?? []).map((v) => `kodza-dialog--${v}`), 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, confirm, 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('./modal-component.js').then(() => {
if (live) document.dispatchEvent(new CustomEvent('kodza:init', { detail: { root } }));
});
return () => {
live = false;
document.dispatchEvent(new CustomEvent('kodza:destroy', { detail: { root } }));
};
}, [key]);
useEffect(() => {
const root = ref.current;
if (!root || !onClose) return;
const listener = (event: Event) => onClose(event as CustomEvent);
root.addEventListener("kodza:close", listener);
return () => root.removeEventListener("kodza:close", listener);
}, [onClose, key]);
return (
<div key={key} {...rest} className={classes} data-confirm={confirm} style={{ ...vars, ...style } as CSSProperties} ref={ref}>
<button className="kodza-dialog__trigger" type="button">Edit profile</button>
<dialog className="kodza-dialog__panel">
<form className="kodza-dialog__form" method="dialog">
<header className="kodza-dialog__head">
<h2 className="kodza-dialog__title">Edit profile</h2>
<p className="kodza-dialog__text">This is how other devs see you. Choose wisely.</p>
</header>
<div className="kodza-dialog__body">
<label className="kodza-dialog__field">
<span>Name</span>
<input type="text" name="name" defaultValue="Ada Lovelace" autoComplete="name" />
</label>
{' '}
<label className="kodza-dialog__field">
<span>Bio</span>
<input type="text" name="bio" defaultValue="Wrote the first program. Still no merge conflicts." autoComplete="off" />
</label>
</div>
<footer className="kodza-dialog__foot">
<button className="kodza-dialog__button" type="submit" value="cancel" formNoValidate>Cancel</button>
{' '}
<button className="kodza-dialog__button kodza-dialog__button--ink" type="submit" value="save">Save changes</button>
</footer>
<button className="kodza-dialog__close" type="submit" value="cancel" formNoValidate aria-label="Close">
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M4 4l8 8M12 4l-8 8" />
</svg>
</button>
</form>
</dialog>
</div>
);
}
Checked
Robots poked at it before you did. Every component goes through the same checks, and again each time it changes.