Arrow Link
A link that points the way: at rest a chevron, on hover the shaft draws itself in and the head moves on, › becomes →. A back variant points the other way. Pure CSS.
View codeA link that leaves the site says so: a line of ink runs under the words to the ↗, which then springs out towards the corner, its shaft stretching after it. Pure CSS.
With your AI
Say it to your AI. It gets the exact code below, not its best guess, and knows where it goes.
Mark the links that leave my site with Kodza External Link.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<a class="kodza-external-link" href="https://developer.mozilla.org/docs/Web/CSS/:has" target="_blank" rel="noopener">MDN on :has()<span class="kodza-external-link__icon" aria-hidden="true"><svg viewBox="0 0 16 16"><path class="kodza-external-link__shaft" d="M4.5 11.5 11 5" pathLength="1" /><path class="kodza-external-link__head" d="M6 5h5v5" /></svg></span><span class="kodza-external-link__sr"> (opens in a new tab)</span></a>
<a class="kodza-external-link" href="https://github.com/" target="_blank" rel="noopener">Star it on GitHub<span class="kodza-external-link__icon" aria-hidden="true"><svg viewBox="0 0 16 16"><path class="kodza-external-link__shaft" d="M4.5 11.5 11 5" pathLength="1" /><path class="kodza-external-link__head" d="M6 5h5v5" /></svg></span><span class="kodza-external-link__sr"> (opens in a new tab)</span></a>
/* Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license */
/* Kodza — External Link
A link that leaves the site says so. On hover a line of ink runs under the words, from the
first letter to the ↗, and when it gets there the arrow springs out towards the corner,
its shaft stretching after it, as if the line had launched it. Screen readers hear
"(opens in a new tab)". Pure CSS. Everything is tuned with the --kz-external-* variables. */
.kodza-external-link {
--kz-external-text: currentColor;
--kz-external-line: color-mix(in srgb, currentColor 26%, transparent);
--kz-external-ink: currentColor;
--kz-external-thickness: 0.06em;
--kz-external-size: 0.6em;
--kz-external-ease: cubic-bezier(0.65, 0, 0.35, 1);
--kz-external-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
color: var(--kz-external-text);
text-decoration: none;
/* Two lines under the words: a faint one, and the ink that runs over it */
background:
linear-gradient(var(--kz-external-ink), var(--kz-external-ink)) 0 100% / 0% var(--kz-external-thickness) no-repeat,
linear-gradient(var(--kz-external-line), var(--kz-external-line)) 0 100% / 100% var(--kz-external-thickness) no-repeat;
padding-bottom: 0.08em;
transition: background-size 0.5s var(--kz-external-ease);
}
.kodza-external-link:hover,
.kodza-external-link:focus-visible {
background-size: 100% var(--kz-external-thickness), 100% var(--kz-external-thickness);
}
.kodza-external-link__icon {
display: inline-block;
width: var(--kz-external-size);
height: var(--kz-external-size);
margin-left: 0.22em;
vertical-align: 0.42em;
}
.kodza-external-link__icon svg {
display: block;
width: 100%;
height: 100%;
overflow: visible;
fill: none;
stroke: currentColor;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
/* The head springs to the corner once the ink arrives; the shaft stretches after it */
.kodza-external-link__head {
transition: transform 0.35s var(--kz-external-ease);
}
.kodza-external-link__shaft {
transform-origin: 4.5px 11.5px;
transition: transform 0.35s var(--kz-external-ease);
}
.kodza-external-link:hover .kodza-external-link__head,
.kodza-external-link:focus-visible .kodza-external-link__head {
transform: translate(2.5px, -2.5px);
transition: transform 0.55s var(--kz-external-spring) 0.32s;
}
.kodza-external-link:hover .kodza-external-link__shaft,
.kodza-external-link:focus-visible .kodza-external-link__shaft {
transform: scale(1.38);
transition: transform 0.55s var(--kz-external-spring) 0.32s;
}
.kodza-external-link__sr {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
.kodza-external-link:focus-visible {
outline: 2px solid currentColor;
outline-offset: 4px;
border-radius: 3px;
}
@media (prefers-reduced-motion: reduce) {
.kodza-external-link,
.kodza-external-link__head,
.kodza-external-link__shaft {
transition-duration: 0.01ms !important;
transition-delay: 0s !important;
}
}
/* kodza-guard 39d6c57a59c1 — 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-external-link):not(#_) {
border-radius: revert;
outline: revert;
outline-offset: revert;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-external-link:not(#_) {
border: revert;
list-style: revert;
box-sizing: revert;
position: revert;
padding-top: revert;
padding-right: revert;
padding-left: revert;
box-shadow: revert;
appearance: 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;
}
.kodza-external-link__icon:not(#_),
.kodza-external-link__shaft:not(#_),
.kodza-external-link__head:not(#_),
.kodza-external-link__sr:not(#_) {
box-sizing: revert;
}
.kodza-external-link__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-external-link:not(#_):where(:not([hidden])) {
display: revert;
}
.kodza-external-link: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-external-link:not(#_) {
color: var(--kz-external-text);
text-decoration: none;
background: linear-gradient(var(--kz-external-ink), var(--kz-external-ink)) 0 100% / 0% var(--kz-external-thickness) no-repeat,
linear-gradient(var(--kz-external-line), var(--kz-external-line)) 0 100% / 100% var(--kz-external-thickness) no-repeat;
padding-bottom: 0.08em;
transition: background-size 0.5s var(--kz-external-ease);
}
.kodza-external-link:hover:not(#_),
.kodza-external-link:focus-visible:not(#_) {
background-size: 100% var(--kz-external-thickness), 100% var(--kz-external-thickness);
}
.kodza-external-link__icon svg:not(#_) {
display: block;
}
.kodza-external-link:focus-visible:not(#_) {
outline: 2px solid currentColor;
outline-offset: 4px;
border-radius: 3px;
}
@media (prefers-reduced-motion: reduce) {
.kodza-external-link:not(#_) {
transition-duration: 0.01ms !important;
transition-delay: 0s !important;
}
}
'use client';
// Kodza — External Link, for React. From https://kodza.dev/components/external-link/
// 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: external-link.css, the component's CSS from Kodza, as they are.
// Yours to edit: the markup is plain JSX below.
import { useRef, type ComponentPropsWithoutRef, type ComponentRef, type CSSProperties } from 'react';
import './external-link.css';
export type ExternalLinkProps = Omit<ComponentPropsWithoutRef<'a'>, "children" | "vars"> & {
/** Settings, as CSS variables on the root */
vars?: {
/** default: currentColor */
"--kz-external-text"?: string | number;
/** default: color-mix(in srgb, currentColor 26%, transparent) */
"--kz-external-line"?: string | number;
/** default: currentColor */
"--kz-external-ink"?: string | number;
/** default: 0.06em */
"--kz-external-thickness"?: string | number;
/** default: 0.6em */
"--kz-external-size"?: string | number;
/** default: cubic-bezier(0.65, 0, 0.35, 1) */
"--kz-external-ease"?: string | number;
/** default: cubic-bezier(0.34, 1.56, 0.64, 1) */
"--kz-external-spring"?: string | number;
};
className?: string;
};
export function ExternalLink({ vars, className, style, ...rest }: ExternalLinkProps) {
const ref = useRef<ComponentRef<'a'>>(null);
const classes = ["kodza-external-link", 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]);
return (
<a href="https://developer.mozilla.org/docs/Web/CSS/:has" target="_blank" rel="noopener" key={key} {...rest} className={classes} style={{ ...vars, ...style } as CSSProperties} ref={ref}>MDN on :has()<span className="kodza-external-link__icon" aria-hidden="true"> <svg viewBox="0 0 16 16"> <path className="kodza-external-link__shaft" d="M4.5 11.5 11 5" pathLength="1" /> <path className="kodza-external-link__head" d="M6 5h5v5" /> </svg> </span><span className="kodza-external-link__sr"> (opens in a new tab)</span></a>
);
}
Checked
Robots poked at it before you did. Every component goes through the same checks, and again each time it changes.