Link Button
A lightweight animated link button for navigation, secondary actions, and product pages that need subtle interaction feedback.

Overview
Link Button gives a button or link a small, useful moment of feedback: a link earns a small interaction state.
Use it where a CTA needs to feel responsive and intentional, not theatrical. A button should confirm action, state, or direction; it should not become a tiny stage play that delays the click.
The thing to watch is semantics. Keep real button and anchor elements, visible focus, instant activation, and clear loading/disabled states. Motion sits on top of the action; it does not replace it.
Install Command
npx hyperiux add link-buttonUsage Code
import LinkButton from "@/components/effects/link-button";
export default function Page() {
return (
<>
<div className="h-screen w-screen flex items-center justify-center relative">
<div className="w-fit mx-auto">
<LinkButton
btnText="Hover me"
href="#"
iconClassName="size-[2vw] mt-[0.2vw] max-sm:size-[4vw]"
className="text-[2vw] hover:text-[#ff6b00] max-sm:text-[4.5vw]"
/>
</div>
</div>
</>
);
}
Component Code
// Built using Hyperiux Vault: https://vault.hyperiux.com
"use client";
import { ArrowRight } from "lucide-react";
import { useLayoutEffect, useState } from "react";
const DEFAULT_HREF = "#";
const TABLET_BREAKPOINT = 1025;
export default function LinkButton({ btnText = "Hover me", mobileText = "Click me", textColor = "#ffffff", hoverColor = "#ff6b00", clickedColor = "#ff6b00", href = DEFAULT_HREF, className = "", linkProps = {}, icon: Icon = ArrowRight, iconClassName = "max-md:size-[6vw] max-[1025px]:size-[4vw]", showIcon = true, underlineHeight = 1.5, transitionDuration = 0.5, disableNavigation = false, onClick, ...props }) {
const [isIconRotated, setIsIconRotated] = useState(false);
const [isCompactViewport, setIsCompactViewport] = useState(false);
const [hasMeasuredViewport, setHasMeasuredViewport] = useState(false);
useLayoutEffect(() => {
const onResize = () => {
setIsCompactViewport(window.innerWidth <= TABLET_BREAKPOINT);
setHasMeasuredViewport(true);
};
onResize();
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("resize", onResize);
};
}, []);
const onLinkClick = (event) => {
setIsIconRotated((previousValue) => !previousValue);
if (disableNavigation) {
event.preventDefault();
}
onClick?.(event);
};
const { style: customStyle, ...restProps } = props;
const displayText = hasMeasuredViewport && isCompactViewport ? mobileText : btnText;
const mergedStyle = {
"--underline-height": `${underlineHeight}px`,
"--transition-duration": `${transitionDuration}s`,
...(hoverColor ? { "--link-hover-color": hoverColor } : null),
...(textColor ? { "--link-text-color": textColor } : null),
...(hasMeasuredViewport && isCompactViewport && isIconRotated ? { color: clickedColor } : null),
...customStyle,
};
const iconClassNames = `${isIconRotated ? "motion-safe:-rotate-45" : ""} ${!isCompactViewport ? "motion-safe:group-hover:-rotate-45" : ""} size-[1.1vw] max-[1025px]:size-[2vw] max-md:size-[3.5vw] transition-transform duration-[var(--transition-duration)] motion-reduce:rotate-0 motion-reduce:transition-none ${iconClassName}`;
return (<>
<a href={href} {...linkProps} {...restProps} onClick={onLinkClick} className={`group block w-fit cursor-pointer text-[1.1vw] leading-[1.2] duration-300 text-(--link-text-color) hover:text-(--link-hover-color) max-[1025px]:text-[4vw] max-md:text-[5.5vw] ${className}`} style={mergedStyle}>
<div className="flex items-center justify-start gap-2">
<span className={`btn-link-line relative inline-block w-fit after:absolute after:left-0 after:bottom-[-2%] after:h-[var(--underline-height)] after:w-full after:bg-current after:content-[''] after:transition-transform after:duration-[var(--transition-duration)] after:ease-[cubic-bezier(0.62,0.05,0.01,0.99)] motion-reduce:after:transition-none ${hasMeasuredViewport && isCompactViewport
? isIconRotated
? "after:origin-right after:scale-x-0 motion-safe:after:origin-left motion-safe:after:scale-x-100"
: "after:origin-right after:scale-x-0"
: "after:origin-right after:scale-x-0 motion-safe:group-hover:after:origin-left motion-safe:group-hover:after:scale-x-100"}`}>
{displayText}
</span>
<span className="sr-only">About {href}</span>
{showIcon && Icon && <Icon className={iconClassNames}/>}
</div>
</a>
<style jsx>{`
li :global(.btn-link-line)::after {
bottom: -20%;
}
`}</style>
</>);
}
Example Production Use Case
Use this as button-interaction implementation guidance. Verify the shipped element semantics, anchor/button mode, disabled and loading states, focus-visible behavior, keyboard activation, and reduced-motion handling before relying on exact props, defaults, imports, or installation steps.
Best Used For
- Secondary actions and navigation links that need a small designed state, not drama.
- Product and docs pages where lightweight feedback supports orientation.
- Link Button adds feedback to an action without weakening semantics, focus, or activation speed.
Not For
Not for destructive actions, unclear states, or places where animation hides the label or delays activation.
Performance Budget
Keep feedback fast, avoid layout-triggering letter movement, and preserve instant activation.
Accessibility and Mobile
Use real buttons or anchors, visible focus-visible styles, and clear loading/disabled states. Touch targets should stay large enough on mobile.
Common Mistakes
- Building Link Button with divs instead of real buttons or links.
- Hiding the label during hover.
- Forgetting loading and disabled states.
Changelog
v1.0.1
Jul 15, 2026Props
| Prop | Type | Default | Description |
|---|---|---|---|
btnText | string | Hover me | Button label text. |
textColor | color | #ffffff | Resting label and underline color. |
hoverColor | color | #ff6b00 | Hover label and underline color. |
underlineHeight | number | 1.5 | Underline thickness in pixels. |
showIcon | boolean | true | Shows or hides the arrow icon. |
transitionDuration | number | 0.5 | Animation duration in seconds. |
Frequently Asked Questions
When should I use Link Button?
Use it when a navigation or secondary link needs a small, lightweight interaction state — without delaying the action.
Should Link Button use a real button or link?
Yes. Use `<button>` for actions and `<a>` for navigation.
How should Link Button handle focus?
Use visible focus-visible styles and support keyboard activation.
How should loading states work with Link Button?
Loading should be visually clear and programmatically communicated without hiding the label.
What should reduced motion do for Link Button?
Remove long staggers, scrambles, and looping emphasis while keeping instant state feedback.
Request a Custom Button Animation
Need a custom effect? Tell us what to create.



