External Link
A 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.
View codeA 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.
With your AI
Say it to your AI. It gets the exact code below, not its best guess, and knows where it goes.
Use the Kodza Arrow Link for the Read more links on my blog.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<a class="kodza-arrow-link" href="#">Read the changelog<span class="kodza-arrow-link__arrow" aria-hidden="true"><svg viewBox="0 0 16 16"><path class="kodza-arrow-link__shaft" d="M2.5 8h10" pathLength="1" /><path class="kodza-arrow-link__head" d="M8.5 3.5 13 8l-4.5 4.5" /></svg></span></a>
<a class="kodza-arrow-link kodza-arrow-link--back" href="#"><span class="kodza-arrow-link__arrow" aria-hidden="true"><svg viewBox="0 0 16 16"><path class="kodza-arrow-link__shaft" d="M13.5 8h-10" pathLength="1" /><path class="kodza-arrow-link__head" d="M7.5 3.5 3 8l4.5 4.5" /></svg></span>Back to the docs</a>
/* Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license */
/* Kodza — Arrow Link
A link that points the way. At rest a chevron sits after the words; on hover the arrow's
shaft draws itself in and the head moves on, so › becomes →, and the words lean forward a
hair. --back points the other way. Pure CSS.
Everything is tuned with the --kz-arrow-link-* variables. */
.kodza-arrow-link {
--kz-arrow-link-size: 0.82em;
--kz-arrow-link-gap: 0.3em;
--kz-arrow-link-duration: 0.45s;
--kz-arrow-link-ease: cubic-bezier(0.22, 1, 0.36, 1);
display: inline-flex;
align-items: center;
gap: var(--kz-arrow-link-gap);
color: inherit;
text-decoration: none;
}
.kodza-arrow-link__arrow {
display: inline-block;
flex: none;
width: var(--kz-arrow-link-size);
height: var(--kz-arrow-link-size);
overflow: visible;
}
.kodza-arrow-link__arrow svg {
display: block;
width: 100%;
height: 100%;
overflow: visible;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
stroke-linejoin: round;
}
/* At rest: the head alone, a little back; the shaft not drawn yet */
.kodza-arrow-link__head {
transform: translateX(-3.5px);
transition: transform var(--kz-arrow-link-duration) var(--kz-arrow-link-ease);
}
.kodza-arrow-link__shaft {
stroke-dasharray: 1;
stroke-dashoffset: 1;
transition: stroke-dashoffset var(--kz-arrow-link-duration) var(--kz-arrow-link-ease);
}
.kodza-arrow-link:hover .kodza-arrow-link__head,
.kodza-arrow-link:focus-visible .kodza-arrow-link__head {
transform: translateX(1.5px);
}
.kodza-arrow-link:hover .kodza-arrow-link__shaft,
.kodza-arrow-link:focus-visible .kodza-arrow-link__shaft {
stroke-dashoffset: 0;
}
/* Back: the same, facing the other way */
.kodza-arrow-link--back .kodza-arrow-link__head {
transform: translateX(3.5px);
}
.kodza-arrow-link--back:hover .kodza-arrow-link__head,
.kodza-arrow-link--back:focus-visible .kodza-arrow-link__head {
transform: translateX(-1.5px);
}
.kodza-arrow-link:focus-visible {
outline: 2px solid currentColor;
outline-offset: 4px;
border-radius: 3px;
}
@media (prefers-reduced-motion: reduce) {
.kodza-arrow-link__head,
.kodza-arrow-link__shaft {
transition-duration: 0.01ms !important;
}
}
/* kodza-guard 2ed04d65dfc7 — 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-arrow-link):not(#_) {
border-radius: revert;
outline: revert;
outline-offset: revert;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-arrow-link:not(#_) {
border: revert;
padding: revert;
transition: revert;
list-style: revert;
box-sizing: revert;
position: revert;
background-color: revert;
background-image: 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-arrow-link__arrow:not(#_),
.kodza-arrow-link__shaft:not(#_),
.kodza-arrow-link__head:not(#_) {
box-sizing: revert;
}
.kodza-arrow-link__arrow 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;
}
/* 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-arrow-link:not(#_) {
display: inline-flex;
color: inherit;
text-decoration: none;
}
.kodza-arrow-link__arrow svg:not(#_) {
display: block;
}
.kodza-arrow-link:focus-visible:not(#_) {
outline: 2px solid currentColor;
outline-offset: 4px;
border-radius: 3px;
}
'use client';
// Kodza — Arrow Link, for React. From https://kodza.dev/components/arrow-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: arrow-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 './arrow-link.css';
type Variant = "back";
export type ArrowLinkProps = Omit<ComponentPropsWithoutRef<'a'>, "children" | "variant" | "vars"> & {
/** Modifier class(es): .kodza-arrow-link--back */
variant?: Variant | Variant[];
/** Settings, as CSS variables on the root */
vars?: {
/** default: 0.82em */
"--kz-arrow-link-size"?: string | number;
/** default: 0.3em */
"--kz-arrow-link-gap"?: string | number;
/** default: 0.45s */
"--kz-arrow-link-duration"?: string | number;
/** default: cubic-bezier(0.22, 1, 0.36, 1) */
"--kz-arrow-link-ease"?: string | number;
};
className?: string;
};
export function ArrowLink({ variant, vars, className, style, ...rest }: ArrowLinkProps) {
const ref = useRef<ComponentRef<'a'>>(null);
const classes = ["kodza-arrow-link", ...([] as string[]).concat(variant ?? []).map((v) => `kodza-arrow-link--${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, vars, style, rest]);
return (
<a href="#" key={key} {...rest} className={classes} style={{ ...vars, ...style } as CSSProperties} ref={ref}>Read the changelog<span className="kodza-arrow-link__arrow" aria-hidden="true"> <svg viewBox="0 0 16 16"> <path className="kodza-arrow-link__shaft" d="M2.5 8h10" pathLength="1" /> <path className="kodza-arrow-link__head" d="M8.5 3.5 13 8l-4.5 4.5" /> </svg> </span></a>
);
}
Checked
Robots poked at it before you did. Every component goes through the same checks, and again each time it changes.