Footer Contact
A freelance or studio footer: the email address across the whole width, copied on a click while the line under it rolls to Copied, the local time to the minute, the networks in big rows.
View codeA site footer: the logo and a line, up to five columns of links under mono titles, a bottom row with the year, legal pages and Back to top; under a link, a line comes in from the left.
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 Footer Columns to my page.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<footer class="kodza-footer-columns">
<div class="kodza-footer-columns__inner">
<div class="kodza-footer-columns__main">
<div class="kodza-footer-columns__brand">
<a class="kodza-footer-columns__logo" href="#" aria-label="Harbor, home"><svg viewBox="0 0 24 22" width="22" height="20" aria-hidden="true"><path d="M2 11a10 10 0 0 1 20 0z"/><path d="M2.46 14h19.08a10 10 0 0 1-19.08 0z"/></svg><span>harbor</span></a>
<p class="kodza-footer-columns__pitch">Deploys that don't need a meeting.</p>
</div>
<nav class="kodza-footer-columns__nav" aria-label="Footer">
<div class="kodza-footer-columns__col">
<p class="kodza-footer-columns__title">Product</p>
<ul class="kodza-footer-columns__links">
<li><a class="kodza-footer-columns__link" href="#">Deploys</a></li>
<li><a class="kodza-footer-columns__link" href="#">Previews</a></li>
<li><a class="kodza-footer-columns__link" href="#">Edge</a></li>
<li><a class="kodza-footer-columns__link" href="#">Pricing</a></li>
</ul>
</div>
<div class="kodza-footer-columns__col">
<p class="kodza-footer-columns__title">Solutions</p>
<ul class="kodza-footer-columns__links">
<li><a class="kodza-footer-columns__link" href="#">Agencies</a></li>
<li><a class="kodza-footer-columns__link" href="#">Startups</a></li>
<li><a class="kodza-footer-columns__link" href="#">Teams</a></li>
</ul>
</div>
<div class="kodza-footer-columns__col">
<p class="kodza-footer-columns__title">Resources</p>
<ul class="kodza-footer-columns__links">
<li><a class="kodza-footer-columns__link" href="#">Docs</a></li>
<li><a class="kodza-footer-columns__link" href="#">Changelog</a></li>
<li><a class="kodza-footer-columns__link" href="#">Guides</a></li>
<li><a class="kodza-footer-columns__link" href="#">Status</a></li>
</ul>
</div>
<div class="kodza-footer-columns__col">
<p class="kodza-footer-columns__title">Company</p>
<ul class="kodza-footer-columns__links">
<li><a class="kodza-footer-columns__link" href="#">About</a></li>
<li><a class="kodza-footer-columns__link" href="#">Blog</a></li>
<li><a class="kodza-footer-columns__link" href="#">Careers</a></li>
<li><a class="kodza-footer-columns__link" href="#">Contact</a></li>
</ul>
</div>
</nav>
</div>
<div class="kodza-footer-columns__base">
<div class="kodza-footer-columns__legal">
<p class="kodza-footer-columns__copy">© <span class="kodza-footer-columns__year">2026</span> Harbor</p>
<ul class="kodza-footer-columns__row">
<li><a class="kodza-footer-columns__link" href="#">Privacy</a></li>
<li><a class="kodza-footer-columns__link" href="#">Terms</a></li>
</ul>
</div>
<div class="kodza-footer-columns__end">
<ul class="kodza-footer-columns__row">
<li><a class="kodza-footer-columns__link" href="#">GitHub</a></li>
<li><a class="kodza-footer-columns__link" href="#">X</a></li>
</ul>
<a class="kodza-footer-columns__top" href="#top">Back to top<span class="kodza-footer-columns__arrow" aria-hidden="true"><svg viewBox="0 0 12 12" width="12" height="12"><path d="M6 10.5v-9M2 5.5l4-4 4 4"/></svg><svg viewBox="0 0 12 12" width="12" height="12"><path d="M6 10.5v-9M2 5.5l4-4 4 4"/></svg></span></a>
</div>
</div>
</div>
</footer>
/* Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license */
/* Kodza — Footer Columns
The footer you paste everywhere: the logo and a line, up to five columns of links under mono
titles, then a bottom row with the year, the legal pages, the networks and Back to top. On
hover, a line comes in under a link from the left and leaves by the right, and the arrow of
Back to top rolls up. On phones the columns go two by two: nothing is hidden.
Everything is tuned with the --kz-footer-columns-* variables. */
.kodza-footer-columns {
--kz-footer-columns-bg: #ffffff;
--kz-footer-columns-fg: #0a0a0a;
--kz-footer-columns-muted: rgba(10, 10, 10, 0.62);
--kz-footer-columns-line: rgba(10, 10, 10, 0.1);
--kz-footer-columns-soft: rgba(10, 10, 10, 0.05); /* Back to top, under the pointer */
--kz-footer-columns-width: 1200px; /* the content, centred */
--kz-footer-columns-pad: 24px;
--kz-footer-columns-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;
--kz-footer-columns-ease: cubic-bezier(0.22, 1, 0.36, 1);
display: block;
box-sizing: border-box;
width: 100%;
margin: 0;
padding: 0;
border-top: 1px solid var(--kz-footer-columns-line);
background: var(--kz-footer-columns-bg);
color: var(--kz-footer-columns-fg);
font-size: 15px;
line-height: 1.5;
text-align: left;
container: kodza-footer-columns / inline-size;
}
/* Centred by its margins (no margin: auto, which some browsers read late) */
.kodza-footer-columns__inner {
box-sizing: border-box;
margin: 0 max(0px, calc((100% - var(--kz-footer-columns-width)) / 2));
padding-top: clamp(56px, 6vw, 80px);
padding-bottom: 28px;
padding-left: max(var(--kz-footer-columns-pad), env(safe-area-inset-left, 0px));
padding-right: max(var(--kz-footer-columns-pad), env(safe-area-inset-right, 0px));
}
.kodza-footer-columns__main {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
gap: 48px clamp(32px, 5vw, 80px);
}
.kodza-footer-columns__brand {
min-width: 0;
}
/* Pulled up so the name sits on the line of the column titles */
.kodza-footer-columns__logo {
display: inline-flex;
align-items: center;
gap: 8px;
height: 40px;
margin: -11px 0 0;
border-radius: 6px;
color: inherit;
font-size: 18px;
font-weight: 600;
line-height: 1;
letter-spacing: -0.04em;
text-decoration: none;
}
.kodza-footer-columns__logo svg {
display: block;
flex: none;
fill: currentColor;
}
.kodza-footer-columns__pitch {
max-width: 24em;
margin: 10px 0 0;
font-size: 15px;
line-height: 1.55;
color: var(--kz-footer-columns-muted);
text-wrap: pretty;
}
/* As many columns as you write, up to five on a row */
.kodza-footer-columns__nav {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(112px, 100%), 1fr));
gap: 40px 24px;
min-width: 0;
}
.kodza-footer-columns__col {
min-width: 0;
}
.kodza-footer-columns__title,
.kodza-footer-columns__copy {
margin: 0;
font-family: var(--kz-footer-columns-mono);
font-size: 12px;
font-weight: 400;
letter-spacing: 0.02em;
line-height: 1.5;
color: var(--kz-footer-columns-muted);
}
.kodza-footer-columns__title {
margin-bottom: 16px;
}
.kodza-footer-columns__links,
.kodza-footer-columns__row {
margin: 0;
padding: 0;
list-style: none;
}
.kodza-footer-columns__links {
display: grid;
justify-items: start;
gap: 10px;
}
.kodza-footer-columns__links li,
.kodza-footer-columns__row li {
margin: 0;
padding: 0;
}
/* Links: the line grows from the left, and shrinks away to the right */
.kodza-footer-columns__link {
color: inherit;
font-size: 14px;
line-height: 1.5;
text-decoration: none;
background: linear-gradient(currentColor, currentColor) no-repeat 100% 100% / 0 1px;
transition: background-size 0.4s var(--kz-footer-columns-ease), color 0.2s ease;
}
@media (hover: hover) {
.kodza-footer-columns__link:hover {
background-position: 0 100%;
background-size: 100% 1px;
}
}
.kodza-footer-columns__base {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 16px 32px;
margin-top: clamp(56px, 7vw, 96px);
padding-top: 20px;
border-top: 1px solid var(--kz-footer-columns-line);
}
.kodza-footer-columns__legal,
.kodza-footer-columns__end {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 24px;
}
.kodza-footer-columns__row {
display: flex;
flex-wrap: wrap;
gap: 4px 20px;
}
.kodza-footer-columns__row .kodza-footer-columns__link {
font-size: 13px;
color: var(--kz-footer-columns-muted);
}
@media (hover: hover) {
.kodza-footer-columns__row .kodza-footer-columns__link:hover {
color: var(--kz-footer-columns-fg);
}
}
/* Back to top: a pill, and an arrow that rolls up (the next one waits under it) */
.kodza-footer-columns__top {
display: inline-flex;
flex: none;
align-items: center;
gap: 8px;
height: 32px;
padding: 0 12px 0 14px;
border-radius: 999px;
box-shadow: inset 0 0 0 1px var(--kz-footer-columns-line);
color: inherit;
font-size: 13px;
font-weight: 500;
line-height: 1;
text-decoration: none;
transition: background-color 0.2s ease;
}
.kodza-footer-columns__arrow {
display: flex;
flex: none;
flex-direction: column;
width: 12px;
height: 12px;
overflow: clip;
}
.kodza-footer-columns__arrow svg {
display: block;
flex: none;
fill: none;
stroke: currentColor;
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform 0.45s var(--kz-footer-columns-ease);
}
@media (hover: hover) {
.kodza-footer-columns__top:hover {
background-color: var(--kz-footer-columns-soft);
}
.kodza-footer-columns__top:hover .kodza-footer-columns__arrow svg {
transform: translateY(-100%);
}
}
.kodza-footer-columns__logo:focus-visible,
.kodza-footer-columns__link:focus-visible,
.kodza-footer-columns__top:focus-visible {
outline: 2px solid var(--kz-footer-columns-fg);
outline-offset: 3px;
}
.kodza-footer-columns__link:focus-visible {
border-radius: 2px;
}
@media (prefers-reduced-motion: reduce) {
.kodza-footer-columns__link,
.kodza-footer-columns__arrow svg {
transition: none;
}
}
/* The brand goes on top of the columns */
@container kodza-footer-columns (max-width: 960px) {
.kodza-footer-columns__main {
grid-template-columns: minmax(0, 1fr);
}
}
/* Phones: the columns two by two, the bottom row stacked */
@container kodza-footer-columns (max-width: 600px) {
.kodza-footer-columns__nav {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 36px 16px;
}
.kodza-footer-columns__base {
flex-direction: column;
align-items: flex-start;
}
}
/* kodza-guard a5e492b73fa0 — 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-footer-columns__logo):not(#_) {
outline: revert;
outline-offset: revert;
}
:where(.kodza-footer-columns__link):not(#_) {
border-radius: revert;
outline: revert;
outline-offset: revert;
}
:where(.kodza-footer-columns__top):not(#_) {
outline: revert;
background-color: revert;
outline-offset: revert;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-footer-columns:not(#_) {
border-radius: revert;
outline: revert;
text-decoration: revert;
transition: revert;
list-style: revert;
position: 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-height: revert;
font-family: revert;
font-weight: revert;
font-style: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
}
.kodza-footer-columns__main:not(#_),
.kodza-footer-columns__brand:not(#_),
.kodza-footer-columns__col:not(#_),
.kodza-footer-columns__base:not(#_),
.kodza-footer-columns__legal:not(#_),
.kodza-footer-columns__year:not(#_),
.kodza-footer-columns__end:not(#_),
.kodza-footer-columns__arrow:not(#_),
.kodza-footer-columns__logo span:where(:not([class*="kodza-"])):not(#_) {
box-sizing: revert;
}
.kodza-footer-columns__logo:not(#_) {
border: revert;
padding: revert;
transition: revert;
list-style: 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;
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-footer-columns__pitch: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-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-footer-columns__nav:not(#_),
.kodza-footer-columns__logo 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-footer-columns__title:not(#_),
.kodza-footer-columns__copy: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-style: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
}
.kodza-footer-columns__links:not(#_),
.kodza-footer-columns__row: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-footer-columns__link:not(#_) {
border: revert;
margin: revert;
padding: 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-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-footer-columns__top:not(#_) {
border: revert;
margin: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: revert;
background-image: revert;
appearance: revert;
max-width: revert;
max-height: revert;
font-family: 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-footer-columns__links li:where(:not([class*="kodza-"]):not(.kodza-footer-columns__links [class*="kodza-"] *)):not(#_),
.kodza-footer-columns__row li:where(:not([class*="kodza-"]):not(.kodza-footer-columns__row [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;
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-footer-columns__arrow 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;
}
.kodza-footer-columns__pitch:not(#_):where(:not([hidden])),
.kodza-footer-columns__title:not(#_):where(:not([hidden])),
.kodza-footer-columns__link:not(#_):where(:not([hidden])),
.kodza-footer-columns__copy:not(#_):where(:not([hidden])),
.kodza-footer-columns__links li:where(:not([class*="kodza-"]):not(.kodza-footer-columns__links [class*="kodza-"] *)):not(#_):where(:not([hidden])),
.kodza-footer-columns__row li:where(:not([class*="kodza-"]):not(.kodza-footer-columns__row [class*="kodza-"] *)):not(#_):where(:not([hidden])) {
display: revert;
}
.kodza-footer-columns__pitch:not(#_):where([hidden]),
.kodza-footer-columns__title:not(#_):where([hidden]),
.kodza-footer-columns__link:not(#_):where([hidden]),
.kodza-footer-columns__copy:not(#_):where([hidden]),
.kodza-footer-columns__links li:where(:not([class*="kodza-"]):not(.kodza-footer-columns__links [class*="kodza-"] *)):not(#_):where([hidden]),
.kodza-footer-columns__row li:where(:not([class*="kodza-"]):not(.kodza-footer-columns__row [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-footer-columns:not(#_) {
display: block;
box-sizing: border-box;
padding: 0;
border-top: 1px solid var(--kz-footer-columns-line);
background: var(--kz-footer-columns-bg);
color: var(--kz-footer-columns-fg);
font-size: 15px;
line-height: 1.5;
text-align: left;
}
.kodza-footer-columns__logo:not(#_) {
display: inline-flex;
margin: -11px 0 0;
border-radius: 6px;
color: inherit;
font-size: 18px;
font-weight: 600;
line-height: 1;
letter-spacing: -0.04em;
text-decoration: none;
}
.kodza-footer-columns__logo svg:not(#_) {
display: block;
}
.kodza-footer-columns__pitch:not(#_) {
max-width: 24em;
margin: 10px 0 0;
font-size: 15px;
line-height: 1.55;
color: var(--kz-footer-columns-muted);
}
.kodza-footer-columns__nav:not(#_) {
display: grid;
}
.kodza-footer-columns__title:not(#_),
.kodza-footer-columns__copy:not(#_) {
margin: 0;
font-family: var(--kz-footer-columns-mono);
font-size: 12px;
font-weight: 400;
letter-spacing: 0.02em;
line-height: 1.5;
color: var(--kz-footer-columns-muted);
}
.kodza-footer-columns__title:not(#_) {
margin-bottom: 16px;
}
.kodza-footer-columns__links:not(#_),
.kodza-footer-columns__row:not(#_) {
margin: 0;
padding: 0;
list-style: none;
}
.kodza-footer-columns__links:not(#_) {
display: grid;
}
.kodza-footer-columns__links li:not(#_),
.kodza-footer-columns__row li:not(#_) {
margin: 0;
padding: 0;
}
.kodza-footer-columns__link:not(#_) {
color: inherit;
font-size: 14px;
line-height: 1.5;
text-decoration: none;
background: linear-gradient(currentColor, currentColor) no-repeat 100% 100% / 0 1px;
transition: background-size 0.4s var(--kz-footer-columns-ease), color 0.2s ease;
}
@media (hover: hover) {
.kodza-footer-columns__link:hover:not(#_) {
background-position: 0 100%;
background-size: 100% 1px;
}
}
.kodza-footer-columns__row:not(#_) {
display: flex;
}
.kodza-footer-columns__row .kodza-footer-columns__link:not(#_) {
font-size: 13px;
color: var(--kz-footer-columns-muted);
}
@media (hover: hover) {
.kodza-footer-columns__row .kodza-footer-columns__link:hover:not(#_) {
color: var(--kz-footer-columns-fg);
}
}
.kodza-footer-columns__top:not(#_) {
display: inline-flex;
padding: 0 12px 0 14px;
border-radius: 999px;
box-shadow: inset 0 0 0 1px var(--kz-footer-columns-line);
color: inherit;
font-size: 13px;
font-weight: 500;
line-height: 1;
text-decoration: none;
transition: background-color 0.2s ease;
}
.kodza-footer-columns__arrow svg:not(#_) {
display: block;
transition: transform 0.45s var(--kz-footer-columns-ease);
}
@media (hover: hover) {
.kodza-footer-columns__top:hover:not(#_) {
background-color: var(--kz-footer-columns-soft);
}
}
.kodza-footer-columns__logo:focus-visible:not(#_),
.kodza-footer-columns__link:focus-visible:not(#_),
.kodza-footer-columns__top:focus-visible:not(#_) {
outline: 2px solid var(--kz-footer-columns-fg);
outline-offset: 3px;
}
.kodza-footer-columns__link:focus-visible:not(#_) {
border-radius: 2px;
}
@media (prefers-reduced-motion: reduce) {
.kodza-footer-columns__link:not(#_),
.kodza-footer-columns__arrow svg:not(#_) {
transition: none;
}
}
// Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license
// Kodza — Footer Columns
// The script writes the year of the ©, and Back to top goes up smoothly (at once when motion is
// reduced). From the keyboard the focus goes up with it: the next Tab starts at the top of the
// page instead of coming back down to the footer. Without the script, #top still goes up.
(() => {
const SELECTOR = '.kodza-footer-columns';
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), summary, [tabindex]:not([tabindex="-1"])';
// One instance, with its own state: two copies on a page never interfere. Everything it
// starts is tied to `signal`, so kodza:destroy stops it all.
function mount(root, signal) {
root.querySelectorAll('.kodza-footer-columns__year').forEach((year) => {
year.textContent = String(new Date().getFullYear());
});
root.querySelectorAll('.kodza-footer-columns__top').forEach((top) => {
top.addEventListener('click', (event) => {
event.preventDefault();
window.scrollTo({ top: 0, behavior: reduceMotion ? 'auto' : 'smooth' });
if (event.detail !== 0) return; // a click with the mouse leaves the focus where it is
// From the keyboard: the first thing on the page you can reach takes the focus
const first = [...document.querySelectorAll(FOCUSABLE)].find((el) => !root.contains(el) && el.getClientRects().length && el.checkVisibility?.({ visibilityProperty: true }) !== false);
first?.focus({ preventScroll: true });
}, { 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. 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 — Footer Columns, for React. From https://kodza.dev/components/footer-columns/
// 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: footer-columns.css and footer-columns.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.
// Built from your data (tabs, rows, options)? Put what the markup is made of in the key, as plain
// values (ids, text, numbers): a change starts the script again on fresh markup. What the visitor
// changes (the open tab, the sort, what they type) is the script's: keep it out of the key, follow
// it with onClick or onInput on the root, and change it from your code as a visitor would (click
// the tab; set the field's value, then dispatch a bubbling input event).
import { useRef, useEffect, type ComponentPropsWithoutRef, type ComponentRef, type CSSProperties } from 'react';
import './footer-columns.css';
export type FooterColumnsProps = Omit<ComponentPropsWithoutRef<'footer'>, "children" | "vars"> & {
/** Settings, as CSS variables on the root */
vars?: {
/** default: #ffffff */
"--kz-footer-columns-bg"?: string | number;
/** default: #0a0a0a */
"--kz-footer-columns-fg"?: string | number;
/** default: rgba(10, 10, 10, 0.62) */
"--kz-footer-columns-muted"?: string | number;
/** default: rgba(10, 10, 10, 0.1) */
"--kz-footer-columns-line"?: string | number;
/** Back to top, under the pointer — default: rgba(10, 10, 10, 0.05) */
"--kz-footer-columns-soft"?: string | number;
/** the content, centred — default: 1200px */
"--kz-footer-columns-width"?: string | number;
/** default: 24px */
"--kz-footer-columns-pad"?: string | number;
/** default: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace */
"--kz-footer-columns-mono"?: string | number;
/** default: cubic-bezier(0.22, 1, 0.36, 1) */
"--kz-footer-columns-ease"?: string | number;
};
className?: string;
};
export function FooterColumns({ vars, className, style, ...rest }: FooterColumnsProps) {
const ref = useRef<ComponentRef<'footer'>>(null);
const classes = ["kodza-footer-columns", 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('./footer-columns.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 (
<footer key={key} {...rest} className={classes} style={{ ...vars, ...style } as CSSProperties} ref={ref}>
<div className="kodza-footer-columns__inner">
<div className="kodza-footer-columns__main">
<div className="kodza-footer-columns__brand">
<a className="kodza-footer-columns__logo" href="#" aria-label="Harbor, home">
<svg viewBox="0 0 24 22" width="22" height="20" aria-hidden="true">
<path d="M2 11a10 10 0 0 1 20 0z" />
<path d="M2.46 14h19.08a10 10 0 0 1-19.08 0z" />
</svg>
<span>harbor</span>
</a>
<p className="kodza-footer-columns__pitch">Deploys that don't need a meeting.</p>
</div>
<nav className="kodza-footer-columns__nav" aria-label="Footer">
<div className="kodza-footer-columns__col">
<p className="kodza-footer-columns__title">Product</p>
<ul className="kodza-footer-columns__links">
<li>
<a className="kodza-footer-columns__link" href="#">Deploys</a>
</li>
<li>
<a className="kodza-footer-columns__link" href="#">Previews</a>
</li>
<li>
<a className="kodza-footer-columns__link" href="#">Edge</a>
</li>
<li>
<a className="kodza-footer-columns__link" href="#">Pricing</a>
</li>
</ul>
</div>
<div className="kodza-footer-columns__col">
<p className="kodza-footer-columns__title">Solutions</p>
<ul className="kodza-footer-columns__links">
<li>
<a className="kodza-footer-columns__link" href="#">Agencies</a>
</li>
<li>
<a className="kodza-footer-columns__link" href="#">Startups</a>
</li>
<li>
<a className="kodza-footer-columns__link" href="#">Teams</a>
</li>
</ul>
</div>
<div className="kodza-footer-columns__col">
<p className="kodza-footer-columns__title">Resources</p>
<ul className="kodza-footer-columns__links">
<li>
<a className="kodza-footer-columns__link" href="#">Docs</a>
</li>
<li>
<a className="kodza-footer-columns__link" href="#">Changelog</a>
</li>
<li>
<a className="kodza-footer-columns__link" href="#">Guides</a>
</li>
<li>
<a className="kodza-footer-columns__link" href="#">Status</a>
</li>
</ul>
</div>
<div className="kodza-footer-columns__col">
<p className="kodza-footer-columns__title">Company</p>
<ul className="kodza-footer-columns__links">
<li>
<a className="kodza-footer-columns__link" href="#">About</a>
</li>
<li>
<a className="kodza-footer-columns__link" href="#">Blog</a>
</li>
<li>
<a className="kodza-footer-columns__link" href="#">Careers</a>
</li>
<li>
<a className="kodza-footer-columns__link" href="#">Contact</a>
</li>
</ul>
</div>
</nav>
</div>
<div className="kodza-footer-columns__base">
<div className="kodza-footer-columns__legal">
<p className="kodza-footer-columns__copy">© <span className="kodza-footer-columns__year">2026</span> Harbor</p>
<ul className="kodza-footer-columns__row">
<li>
<a className="kodza-footer-columns__link" href="#">Privacy</a>
</li>
<li>
<a className="kodza-footer-columns__link" href="#">Terms</a>
</li>
</ul>
</div>
<div className="kodza-footer-columns__end">
<ul className="kodza-footer-columns__row">
<li>
<a className="kodza-footer-columns__link" href="#">GitHub</a>
</li>
<li>
<a className="kodza-footer-columns__link" href="#">X</a>
</li>
</ul>
<a className="kodza-footer-columns__top" href="#top">Back to top<span className="kodza-footer-columns__arrow" aria-hidden="true"> <svg viewBox="0 0 12 12" width="12" height="12"> <path d="M6 10.5v-9M2 5.5l4-4 4 4" /> </svg> <svg viewBox="0 0 12 12" width="12" height="12"> <path d="M6 10.5v-9M2 5.5l4-4 4 4" /> </svg> </span></a>
</div>
</div>
</div>
</footer>
);
}
Checked
Robots poked at it before you did. Every component goes through the same checks, and again each time it changes.