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 codeAn underline that slides in from the left and leaves through the right, or the reverse. Follows the text across lines, like a pen.
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 Underline Link for my navigation links.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<a class="kodza-underline" href="#">Read the docs</a>
<a class="kodza-underline kodza-underline--retract" href="#">Nobody reads 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 — Underline Link
The underline slides in from the left and leaves through the right.
The --retract variant does the opposite: visible at rest, it pulls away on hover.
It is drawn as a background, so it follows the text across several lines, like a pen.
Everything is tuned with the --kz-underline-* variables. */
.kodza-underline {
--kz-underline-text: currentColor;
--kz-underline-color: currentColor;
--kz-underline-thickness: 0.07em;
--kz-underline-offset: 0.08em;
--kz-underline-duration: 0.6s;
--kz-underline-ease: cubic-bezier(0.76, 0, 0.24, 1);
color: var(--kz-underline-text);
text-decoration: none;
background-image: linear-gradient(var(--kz-underline-color), var(--kz-underline-color));
background-repeat: no-repeat;
background-position: 100% 100%;
background-size: 0% var(--kz-underline-thickness);
padding-bottom: var(--kz-underline-offset);
/* Only the width moves: the anchor flips instantly, while it changes nothing on screen */
transition: background-size var(--kz-underline-duration) var(--kz-underline-ease);
}
.kodza-underline:hover,
.kodza-underline:focus-visible {
background-position: 0 100%;
background-size: 100% var(--kz-underline-thickness);
}
.kodza-underline--retract {
background-position: 0 100%;
background-size: 100% var(--kz-underline-thickness);
}
.kodza-underline--retract:hover,
.kodza-underline--retract:focus-visible {
background-position: 100% 100%;
background-size: 0% var(--kz-underline-thickness);
}
.kodza-underline:focus-visible {
outline: 2px solid currentColor;
outline-offset: 4px;
border-radius: 2px;
}
@media (prefers-reduced-motion: reduce) {
.kodza-underline {
transition-duration: 0.01ms;
}
}
/* kodza-guard 8d0c30debe74 — 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-underline):not(#_) {
border-radius: revert;
outline: revert;
outline-offset: revert;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-underline:not(#_) {
border: revert;
list-style: revert;
box-sizing: revert;
position: revert;
padding-top: revert;
padding-right: revert;
padding-left: 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;
}
.kodza-underline:not(#_):where(:not([hidden])) {
display: revert;
}
.kodza-underline: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-underline:not(#_) {
color: var(--kz-underline-text);
text-decoration: none;
background-image: linear-gradient(var(--kz-underline-color), var(--kz-underline-color));
padding-bottom: var(--kz-underline-offset);
transition: background-size var(--kz-underline-duration) var(--kz-underline-ease);
}
.kodza-underline:focus-visible:not(#_) {
outline: 2px solid currentColor;
outline-offset: 4px;
border-radius: 2px;
}
@media (prefers-reduced-motion: reduce) {
.kodza-underline:not(#_) {
transition-duration: 0.01ms;
}
}
'use client';
// Kodza — Underline Link, for React. From https://kodza.dev/components/hover-link-text-underline/
// 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-underline.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-underline.css';
type Variant = "retract";
export type UnderlineLinkProps = Omit<ComponentPropsWithoutRef<'a'>, "children" | "variant" | "vars"> & {
/** Modifier class(es): .kodza-underline--retract */
variant?: Variant | Variant[];
/** Settings, as CSS variables on the root */
vars?: {
/** default: currentColor */
"--kz-underline-text"?: string | number;
/** default: currentColor */
"--kz-underline-color"?: string | number;
/** default: 0.07em */
"--kz-underline-thickness"?: string | number;
/** default: 0.08em */
"--kz-underline-offset"?: string | number;
/** default: 0.6s */
"--kz-underline-duration"?: string | number;
/** default: cubic-bezier(0.76, 0, 0.24, 1) */
"--kz-underline-ease"?: string | number;
};
/** The text */
children?: string;
className?: string;
};
export function UnderlineLink({ variant, vars, children = "Read the docs", className, style, ...rest }: UnderlineLinkProps) {
const ref = useRef<ComponentRef<'a'>>(null);
const classes = ["kodza-underline", ...([] as string[]).concat(variant ?? []).map((v) => `kodza-underline--${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.