Code Block
Code to read and copy: file name, line numbers, highlighted lines and a monochrome syntax in weight and light. Copy rolls to Copied; a terminal copies its commands only.
View codeQuestions that open on their answer, on a native <details>: the panel opens at its real height, the answer rises, the plus turns into a minus. One open at a time if you like.
With your AI
Say it to your AI. It gets the exact code below, not its best guess, and knows where it goes.
Turn my FAQ into a Kodza Accordion, one answer open at a time.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<div class="kodza-accordion" data-single>
<details class="kodza-accordion__item" open>
<summary class="kodza-accordion__trigger">
<span class="kodza-accordion__index">01</span>
<span class="kodza-accordion__question">Is Kodza really free?</span>
<span class="kodza-accordion__icon" aria-hidden="true"></span>
</summary>
<div class="kodza-accordion__panel">
<p>Yes. No account, no paywall, no “Pro” badge waiting in a corner. Copy what you like, ship it, keep the change.</p>
</div>
</details>
<details class="kodza-accordion__item">
<summary class="kodza-accordion__trigger">
<span class="kodza-accordion__index">02</span>
<span class="kodza-accordion__question">Does it work in WordPress?</span>
<span class="kodza-accordion__icon" aria-hidden="true"></span>
</summary>
<div class="kodza-accordion__panel">
<p>Paste the HTML in a Custom HTML block, the CSS in your theme and the script before the closing body tag. Every class starts with kodza-, so your theme will not even notice.</p>
</div>
</details>
<details class="kodza-accordion__item">
<summary class="kodza-accordion__trigger">
<span class="kodza-accordion__index">03</span>
<span class="kodza-accordion__question">Do I need React?</span>
<span class="kodza-accordion__icon" aria-hidden="true"></span>
</summary>
<div class="kodza-accordion__panel">
<p>No. Plain HTML, CSS and a pinch of vanilla JavaScript. If you insist, some components have a React tab.</p>
</div>
</details>
<details class="kodza-accordion__item">
<summary class="kodza-accordion__trigger">
<span class="kodza-accordion__index">04</span>
<span class="kodza-accordion__question">What about accessibility?</span>
<span class="kodza-accordion__icon" aria-hidden="true"></span>
</summary>
<div class="kodza-accordion__panel">
<p>Keyboard, focus, reduced motion and screen readers are checked before anything ships. Not perfect, never an afterthought.</p>
</div>
</details>
</div>
/* Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license */
/* Kodza — Accordion
Questions that open on their answer, built on a details element (it still opens without the script,
and the browser's find-in-page reaches closed answers). The panel opens at its real height
and the answer rises from behind its edge; the plus turns into a minus, the number goes to
ink. data-single on the root keeps one answer open at a time.
Everything is tuned with the --kz-accordion-* variables. */
.kodza-accordion {
--kz-accordion-fg: #0a0a0a;
--kz-accordion-muted: rgba(10, 10, 10, 0.62);
--kz-accordion-line: rgba(10, 10, 10, 0.12);
--kz-accordion-soft: rgba(10, 10, 10, 0.05);
--kz-accordion-ease: cubic-bezier(0.22, 1, 0.36, 1);
border-top: 1px solid var(--kz-accordion-line);
color: var(--kz-accordion-fg);
}
.kodza-accordion__item {
border-bottom: 1px solid var(--kz-accordion-line);
}
.kodza-accordion__trigger {
display: grid;
grid-template-columns: 40px minmax(0, 1fr) 32px;
align-items: center;
gap: 12px;
padding: 20px 0;
cursor: pointer;
list-style: none;
}
.kodza-accordion__trigger::-webkit-details-marker {
display: none;
}
.kodza-accordion__trigger:focus-visible {
outline: 2px solid var(--kz-accordion-fg);
outline-offset: 4px;
border-radius: 6px;
}
.kodza-accordion__index {
color: var(--kz-accordion-muted);
font-family: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;
font-size: 12px;
transition: color 0.3s ease;
}
.kodza-accordion__question {
font-size: 17px;
font-weight: 500;
line-height: 1.35;
letter-spacing: -0.015em;
}
/* The plus: two bars; the upright one lies down to make a minus */
.kodza-accordion__icon {
position: relative;
justify-self: end;
width: 32px;
height: 32px;
border-radius: 50%;
transition: background-color 0.25s ease;
}
.kodza-accordion__icon::before,
.kodza-accordion__icon::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 12px;
height: 1.5px;
margin: -0.75px 0 0 -6px;
border-radius: 1px;
background: currentColor;
transition: transform 0.45s var(--kz-accordion-ease);
}
.kodza-accordion__icon::after {
transform: rotate(90deg);
}
.kodza-accordion__trigger:hover .kodza-accordion__icon {
background: var(--kz-accordion-soft);
}
.kodza-accordion__item[open] .kodza-accordion__index {
color: var(--kz-accordion-fg);
}
.kodza-accordion__item[open] .kodza-accordion__icon::after {
transform: rotate(180deg);
}
.kodza-accordion__item[open] .kodza-accordion__icon::before {
transform: rotate(180deg);
}
/* While closing, the minus is already turning back into a plus */
.kodza-accordion__item.is-closing .kodza-accordion__index {
color: var(--kz-accordion-muted);
}
.kodza-accordion__item.is-closing .kodza-accordion__icon::after {
transform: rotate(90deg);
}
.kodza-accordion__item.is-closing .kodza-accordion__icon::before {
transform: none;
}
.kodza-accordion__panel {
overflow: hidden;
}
.kodza-accordion__panel > * {
margin: 0;
padding: 0 44px 24px 52px;
color: var(--kz-accordion-muted);
font-size: 15px;
line-height: 1.6;
text-wrap: pretty;
}
@media (max-width: 560px) {
.kodza-accordion__trigger {
grid-template-columns: 28px minmax(0, 1fr) 32px;
}
.kodza-accordion__panel > * {
padding: 0 0 22px 40px;
}
}
@media (prefers-reduced-motion: reduce) {
.kodza-accordion__index,
.kodza-accordion__icon,
.kodza-accordion__icon::before,
.kodza-accordion__icon::after {
transition-duration: 0.01ms !important;
}
}
/* Theming example — on a dark background:
.dark-section .kodza-accordion {
--kz-accordion-fg: #f2f2f0;
--kz-accordion-muted: rgba(242, 242, 240, 0.62);
--kz-accordion-line: rgba(255, 255, 255, 0.14);
--kz-accordion-soft: rgba(255, 255, 255, 0.08);
}
*/
/* kodza-guard 23606b498864 — 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-accordion__trigger):not(#_) {
border-radius: revert;
outline: revert;
outline-offset: revert;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-accordion:not(#_),
.kodza-accordion__index:not(#_),
.kodza-accordion__question:not(#_),
.kodza-accordion__icon:not(#_),
.kodza-accordion__panel:not(#_) {
box-sizing: revert;
}
.kodza-accordion__item:not(#_) {
margin: revert;
padding: 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-accordion__trigger:not(#_) {
border: revert;
margin: revert;
text-decoration: revert;
transition: revert;
box-sizing: revert;
float: revert;
position: revert;
background-color: revert;
background-image: revert;
box-shadow: 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-accordion__panel p:where(:not([class*="kodza-"]):not(.kodza-accordion__panel [class*="kodza-"] *)):not(#_) {
border: 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-accordion__item:not(#_):where(:not([hidden])),
.kodza-accordion__panel p:where(:not([class*="kodza-"]):not(.kodza-accordion__panel [class*="kodza-"] *)):not(#_):where(:not([hidden])) {
display: revert;
}
.kodza-accordion__item:not(#_):where([hidden]),
.kodza-accordion__panel p:where(:not([class*="kodza-"]):not(.kodza-accordion__panel [class*="kodza-"] *)):not(#_):where([hidden]) {
display: none;
}
.kodza-accordion__icon:not(#_)::before,
.kodza-accordion__icon:not(#_)::after {
box-sizing: revert;
}
/* The component's own rules, one step stronger: real buttons and fields, what it sets on tags themes style (links, lists, headings, images…), and the root width (a theme may narrow it, never widen it) */
.kodza-accordion__item:not(#_) {
border-bottom: 1px solid var(--kz-accordion-line);
}
.kodza-accordion__trigger:not(#_) {
display: grid;
padding: 20px 0;
list-style: none;
}
.kodza-accordion__trigger:focus-visible:not(#_) {
outline: 2px solid var(--kz-accordion-fg);
outline-offset: 4px;
border-radius: 6px;
}
.kodza-accordion__panel > *:not(#_) {
margin: 0;
padding: 0 44px 24px 52px;
color: var(--kz-accordion-muted);
font-size: 15px;
line-height: 1.6;
}
@media (max-width: 560px) {
.kodza-accordion__panel > *:not(#_) {
padding: 0 0 22px 40px;
}
}
// Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license
// Kodza — Accordion
// Animates the <details>: the panel opens at its real height while the answer rises from
// behind its edge, and closes the same way before the browser closes it. With data-single,
// opening one answer folds the others.
(() => {
const SELECTOR = '.kodza-accordion';
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const ease = 'cubic-bezier(0.22, 1, 0.36, 1)';
const fold = 'cubic-bezier(0.65, 0, 0.35, 1)';
// One instance. Everything it starts is tied to `signal`, so kodza:destroy stops it all
function mount(root, signal) {
const items = [...root.querySelectorAll('.kodza-accordion__item')];
const single = root.hasAttribute('data-single');
const open = (item) => {
const panel = item.querySelector('.kodza-accordion__panel');
item.classList.remove('is-closing');
panel.getAnimations().forEach((animation) => animation.cancel());
item.open = true;
if (reduceMotion) return;
panel.animate([{ height: '0px' }, { height: `${panel.scrollHeight}px` }], { duration: 520, easing: ease });
[...panel.children].forEach((child, i) => child.animate([
{ opacity: 0, transform: 'translateY(14px)' },
{ opacity: 1, transform: 'none' },
], { duration: 620, delay: 60 + i * 70, easing: ease, fill: 'backwards' }));
};
const close = (item) => {
const panel = item.querySelector('.kodza-accordion__panel');
if (!item.open || item.classList.contains('is-closing')) return;
if (reduceMotion) return (item.open = false);
item.classList.add('is-closing');
panel.animate([{ height: `${panel.offsetHeight}px`, opacity: 1 }, { height: '0px', opacity: 0.4 }], { duration: 420, easing: fold }).onfinish = () => {
item.classList.remove('is-closing');
item.open = false;
};
};
items.forEach((item) => {
item.querySelector('.kodza-accordion__trigger').addEventListener('click', (event) => {
event.preventDefault();
if (item.open && !item.classList.contains('is-closing')) return close(item);
if (single) items.forEach((other) => other !== item && close(other));
open(item);
}, { signal });
});
}
// Kodza lifecycle, the same in every component: `kodza:init` starts the instances inside
// event.detail.root (the whole page by default), `kodza:destroy` stops them and puts their
// markup back. For themes and apps that swap content without a page load.
const instances = new WeakMap();
const find = (scope = document) => [scope, ...scope.querySelectorAll(SELECTOR)].filter((el) => el.matches?.(SELECTOR));
const init = (scope) => find(scope).forEach((root) => {
if (root.classList.contains('is-ready')) return; // already running, maybe from another copy of this script
const controller = new AbortController();
instances.set(root, { controller, html: root.innerHTML, attrs: [...root.attributes].map((a) => [a.name, a.value]) });
mount(root, controller.signal);
root.classList.add('is-ready');
});
const destroy = (scope) => find(scope).forEach((root) => {
const instance = instances.get(root);
if (!instance) return;
instances.delete(root);
instance.controller.abort();
[...root.attributes].forEach((a) => root.removeAttribute(a.name));
instance.attrs.forEach(([name, value]) => root.setAttribute(name, value));
root.innerHTML = instance.html;
});
document.addEventListener('kodza:init', (event) => init(event.detail?.root));
document.addEventListener('kodza:destroy', (event) => destroy(event.detail?.root));
init();
// Loaded in <head>, the script runs before the markup exists: once more when it is parsed
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => init(), { once: true });
})();
'use client';
// Kodza — Accordion, for React. From https://kodza.dev/components/accordion-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: accordion-component.css and accordion-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 './accordion-component.css';
export type AccordionProps = Omit<ComponentPropsWithoutRef<'div'>, "children" | "vars" | "single"> & {
/** data-single on the root */
single?: string;
/** Settings, as CSS variables on the root */
vars?: {
/** default: #0a0a0a */
"--kz-accordion-fg"?: string | number;
/** default: rgba(10, 10, 10, 0.62) */
"--kz-accordion-muted"?: string | number;
/** default: rgba(10, 10, 10, 0.12) */
"--kz-accordion-line"?: string | number;
/** default: rgba(10, 10, 10, 0.05) */
"--kz-accordion-soft"?: string | number;
/** default: cubic-bezier(0.22, 1, 0.36, 1) */
"--kz-accordion-ease"?: string | number;
};
className?: string;
};
export function Accordion({ single, vars, className, style, ...rest }: AccordionProps) {
const ref = useRef<ComponentRef<'div'>>(null);
const classes = ["kodza-accordion", 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, single, 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('./accordion-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]);
return (
<div key={key} {...rest} className={classes} data-single={single} style={{ ...vars, ...style } as CSSProperties} ref={ref}>
<details className="kodza-accordion__item" open>
<summary className="kodza-accordion__trigger">
<span className="kodza-accordion__index">01</span>
{' '}
<span className="kodza-accordion__question">Is Kodza really free?</span>
{' '}
<span className="kodza-accordion__icon" aria-hidden="true" />
</summary>
<div className="kodza-accordion__panel">
<p>Yes. No account, no paywall, no “Pro” badge waiting in a corner. Copy what you like, ship it, keep the change.</p>
</div>
</details>
<details className="kodza-accordion__item">
<summary className="kodza-accordion__trigger">
<span className="kodza-accordion__index">02</span>
{' '}
<span className="kodza-accordion__question">Does it work in WordPress?</span>
{' '}
<span className="kodza-accordion__icon" aria-hidden="true" />
</summary>
<div className="kodza-accordion__panel">
<p>Paste the HTML in a Custom HTML block, the CSS in your theme and the script before the closing body tag. Every class starts with kodza-, so your theme will not even notice.</p>
</div>
</details>
<details className="kodza-accordion__item">
<summary className="kodza-accordion__trigger">
<span className="kodza-accordion__index">03</span>
{' '}
<span className="kodza-accordion__question">Do I need React?</span>
{' '}
<span className="kodza-accordion__icon" aria-hidden="true" />
</summary>
<div className="kodza-accordion__panel">
<p>No. Plain HTML, CSS and a pinch of vanilla JavaScript. If you insist, some components have a React tab.</p>
</div>
</details>
<details className="kodza-accordion__item">
<summary className="kodza-accordion__trigger">
<span className="kodza-accordion__index">04</span>
{' '}
<span className="kodza-accordion__question">What about accessibility?</span>
{' '}
<span className="kodza-accordion__icon" aria-hidden="true" />
</summary>
<div className="kodza-accordion__panel">
<p>Keyboard, focus, reduced motion and screen readers are checked before anything ships. Not perfect, never an afterthought.</p>
</div>
</details>
</div>
);
}
Checked
Robots poked at it before you did. Every component goes through the same checks, and again each time it changes.