Arrow Flip Link
Text link whose round arrow leaves at the top right as a second one arrives from the bottom left.
From the Kalme template — see it in contextimport { ArrowFlipLink } from "@/components/ui/arrow-flip-link";
export default function ArrowFlipLinkDemo() {
return (
<div className="flex flex-wrap items-center justify-center gap-10">
<ArrowFlipLink href="#">All projects</ArrowFlipLink>
<ArrowFlipLink
href="#"
size={22}
className="group-hover:text-destructive"
circleClassName="group-hover:bg-destructive"
>
Read the case study
</ArrowFlipLink>
</div>
);
}Installation
$ npx shadcn@latest add https://framebrick.co/r/arrow-flip-link.json1. Install the dependencies.
npm install motion clsx tailwind-merge2. Add
cn()tolib/utils.ts(skip if shadcn already created it).import { clsx, type ClassValue } from "clsx"; import { extendTailwindMerge } from "tailwind-merge"; /* tailwind-merge must know the site's type scale (globals.css) is a font size, not a text color — otherwise cn("text-h6 text-muted-foreground") drops text-h6. */ const twMerge = extendTailwindMerge({ extend: { classGroups: { "font-size": [{ text: ["display", "h1", "h2", "h3", "h4", "h5", "h6"] }] } }, }); export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }3. Copy the component into
components/ui/arrow-flip-link.tsx."use client"; import * as React from "react"; import Link from "next/link"; import { motion, type HTMLMotionProps, type Transition } from "motion/react"; import { cn } from "@/lib/utils"; /* * Arrow Flip Link — a text link whose round arrow swaps on hover: the arrow leaves towards the top right while a * second one arrives from the bottom left (from the Kalme "Button/Secondary"). Framer's soft spring drives it: * spring, bounce 0, duration 0.4s. The flip also runs on keyboard focus. * * Colour is left to you, because the library has no brand accent: the root is a `group`, so a hover tint is * `className="group-hover:text-destructive"` on the label and `circleClassName="group-hover:bg-destructive"` * on the circle. Kalme turns both red; the motion is what this component carries. */ const soft: Transition = { type: "spring", bounce: 0, duration: 0.4 }; export type ArrowFlipLinkProps = Omit<HTMLMotionProps<"a">, "href" | "children"> & { href: string; /** Size of the round arrow in px. */ size?: number; /** Classes for the circle, e.g. a hover tint. */ circleClassName?: string; children?: React.ReactNode; }; const MotionLink = motion.create(Link); function Arrow({ size }: { size: number }) { const box = Math.round(size * 0.66); const arrow = ( <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className="size-full"> <path d="M5 11l6-6M6 5h5v5" /> </svg> ); /* Two arrows on a diagonal: the grid slides so one leaves at the top right as the other arrives at the bottom left. */ return ( <span aria-hidden className="relative block overflow-hidden" style={{ width: box, height: box }}> <motion.span className="absolute grid grid-cols-2" style={{ width: box * 2, height: box * 2 }} variants={{ rest: { x: -box, y: 0 }, hover: { x: 0, y: -box } }} transition={soft} > <span /> <span className="block" style={{ width: box, height: box }}> {arrow} </span> <span className="block" style={{ width: box, height: box }}> {arrow} </span> <span /> </motion.span> </span> ); } export function ArrowFlipLink({ href, size = 18, circleClassName, className, children, ...props }: ArrowFlipLinkProps) { const external = /^https?:\/\//.test(href); return ( <MotionLink href={href} {...(external ? { target: "_blank", rel: "noopener noreferrer" } : {})} {...props} initial="rest" animate="rest" whileHover="hover" whileFocus="hover" className={cn( "group inline-flex items-center gap-1.5 rounded-xs text-sm font-medium whitespace-nowrap text-foreground outline-none", "transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50", className, )} > {children} <span className={cn( "grid shrink-0 place-items-center rounded-full bg-primary text-primary-foreground transition-colors", circleClassName, )} style={{ width: size, height: size }} > <Arrow size={size} /> </span> </MotionLink> ); }
Usage
import { ArrowFlipLink } from "@/components/ui/arrow-flip-link";
<ArrowFlipLink href="/work">All projects</ArrowFlipLink>
// tinted like the template, through the root's group
<ArrowFlipLink href="/work" className="group-hover:text-destructive" circleClassName="group-hover:bg-destructive">
Read the case study
</ArrowFlipLink>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| href | string | — | Link target. http(s) links open in a new tab. |
| size | number | 18 | Diameter of the round arrow, in px. |
| circleClassName | string | — | Classes for the circle — this is where a hover tint goes. |
| …props | anchor props | — | Passed to the link. |
- The flip also runs on keyboard focus.
- Colour is yours: the root is a `group`, so `group-hover:` classes on the label and the circle tint it. Kalme turns both red; what this component carries is the motion.
- The arrows are decorative, so they are hidden from screen readers — the label does the talking.
AI prompt
Paste into v0, Lovable, Bolt or Claude to rebuild this component in your own project.
Create a React + Tailwind CSS + Motion (motion/react) component called ArrowFlipLink using Next.js Link. - A link: label (text-sm font-medium) and, after it, a round filled circle (default 18px, bg-primary, text-primary-foreground) holding an up-right arrow. - Inside the circle, a box the size of the arrow with overflow-hidden holds a 2x2 grid twice that size, with arrows in the top-right and bottom-left cells. At rest the grid sits at x: -box, y: 0 (showing the bottom-left arrow's cell); on hover it moves to x: 0, y: -box, so the visible arrow leaves towards the top right while the second arrives from the bottom left. Spring, bounce 0, duration 0.4s, also on keyboard focus. - Leave colour to the caller: give the root the `group` class and take a circleClassName, so a tint is `group-hover:text-…` and `group-hover:bg-…`. - External http(s) links open in a new tab with rel noopener noreferrer; the arrows are aria-hidden.











