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 highlighter stroke that sweeps behind the link text, or a solid block that flips the text to white exactly under its edge.
With your AI
Say it to your AI. It gets the exact code below, not its best guess, and knows where it goes.
Use Kodza Highlight Link for the links in my articles.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<a class="kodza-highlight" href="#">Copy-paste and chill</a>
<a class="kodza-highlight kodza-highlight--invert" href="#">Skip the setup</a>
/* Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license */
/* Kodza — Highlight Link
A highlighter stroke sweeps in behind the text from the left and leaves through the
right. Drawn as a background, it follows the text across several lines.
The --invert variant passes a solid block instead: the text turns white exactly under
its edge (single line, made for light or dark monochrome backgrounds).
Everything is tuned with the --kz-highlight-* variables. */
.kodza-highlight {
--kz-highlight-text: currentColor;
--kz-highlight-mark: rgba(10, 10, 10, 0.12);
--kz-highlight-height: 100%;
--kz-highlight-duration: 0.55s;
--kz-highlight-ease: cubic-bezier(0.76, 0, 0.24, 1);
color: var(--kz-highlight-text);
text-decoration: none;
background-image: linear-gradient(var(--kz-highlight-mark), var(--kz-highlight-mark));
background-repeat: no-repeat;
background-position: 100% 100%;
background-size: 0% var(--kz-highlight-height);
padding: 0.02em 0.14em;
margin: 0 -0.14em;
border-radius: 0.12em;
/* Only the width moves: the anchor flips instantly, while it changes nothing on screen */
transition: background-size var(--kz-highlight-duration) var(--kz-highlight-ease);
}
.kodza-highlight:hover,
.kodza-highlight:focus-visible {
background-position: 0 100%;
background-size: 100% var(--kz-highlight-height);
}
/* Invert: a white block in "difference" mode shows as ink over a light page,
and flips the text it covers to white, pixel by pixel */
.kodza-highlight--invert {
position: relative;
display: inline-block;
background-image: none;
margin: 0;
}
.kodza-highlight--invert::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: #ffffff;
mix-blend-mode: difference;
transform: scaleX(0);
transform-origin: 100% 50%;
transition: transform var(--kz-highlight-duration) var(--kz-highlight-ease);
pointer-events: none;
}
.kodza-highlight--invert:hover::after,
.kodza-highlight--invert:focus-visible::after {
transform: scaleX(1);
transform-origin: 0 50%;
}
.kodza-highlight:focus-visible {
outline: 2px solid currentColor;
outline-offset: 4px;
}
@media (prefers-reduced-motion: reduce) {
.kodza-highlight,
.kodza-highlight--invert::after {
transition-duration: 0.01ms;
}
}
/* Theming example — a warmer marker:
.kodza-highlight {
--kz-highlight-mark: #f3e3a3;
--kz-highlight-height: 45%;
}
*/
/* kodza-guard a42e4f4deea1 — 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-highlight):not(#_) {
outline: revert;
position: revert;
outline-offset: revert;
}
:where(.kodza-highlight):not(#_):where(:not([hidden])) {
display: revert;
}
:where(.kodza-highlight):not(#_):where([hidden]) {
display: none;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-highlight:not(#_) {
border: revert;
list-style: revert;
box-sizing: revert;
background-color: 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;
}
/* 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-highlight:not(#_) {
color: var(--kz-highlight-text);
text-decoration: none;
background-image: linear-gradient(var(--kz-highlight-mark), var(--kz-highlight-mark));
padding: 0.02em 0.14em;
border-radius: 0.12em;
transition: background-size var(--kz-highlight-duration) var(--kz-highlight-ease);
}
.kodza-highlight--invert:not(#_) {
position: relative;
display: inline-block;
background-image: none;
}
.kodza-highlight:focus-visible:not(#_) {
outline: 2px solid currentColor;
outline-offset: 4px;
}
@media (prefers-reduced-motion: reduce) {
.kodza-highlight:not(#_) {
transition-duration: 0.01ms;
}
}
'use client';
// Kodza — Highlight Link, for React. From https://kodza.dev/components/hover-link-text-background-reveal/
// 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: hover-link-text-background-reveal.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 './hover-link-text-background-reveal.css';
type Variant = "invert";
export type HighlightLinkProps = Omit<ComponentPropsWithoutRef<'a'>, "children" | "variant" | "vars"> & {
/** Modifier class(es): .kodza-highlight--invert */
variant?: Variant | Variant[];
/** Settings, as CSS variables on the root */
vars?: {
/** default: currentColor */
"--kz-highlight-text"?: string | number;
/** default: rgba(10, 10, 10, 0.12) */
"--kz-highlight-mark"?: string | number;
/** default: 100% */
"--kz-highlight-height"?: string | number;
/** default: 0.55s */
"--kz-highlight-duration"?: string | number;
/** default: cubic-bezier(0.76, 0, 0.24, 1) */
"--kz-highlight-ease"?: string | number;
};
/** The text */
children?: string;
className?: string;
};
export function HighlightLink({ variant, vars, children = "Copy-paste and chill", className, style, ...rest }: HighlightLinkProps) {
const ref = useRef<ComponentRef<'a'>>(null);
const classes = ["kodza-highlight", ...([] as string[]).concat(variant ?? []).map((v) => `kodza-highlight--${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, children, style, rest]);
return (
<a href="#" key={key} {...rest} className={classes} style={{ ...vars, ...style } as CSSProperties} ref={ref}>{children}</a>
);
}
Checked
Robots poked at it before you did. Every component goes through the same checks, and again each time it changes.