Components

Base Components

Link

Text link component with primary and secondary variants.

Example

Installation

Create a link.tsx file and paste the following code into it.

import { cva, type VariantProps } from "class-variance-authority";import { ExternalLink, Plus } from "lucide-react";import Link from "next/link";import * as React from "react";import { cn } from "@/lib/utils";const linkVariants = cva(  "group inline-flex items-center text-foreground gap-1 px-1 py-1 text-sm font-medium transition-colors hover:text-link-secondary disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 [aria-disabled=true]:pointer-events-none [aria-disabled=true]:opacity-50 [aria-disabled=true]:text-link-disabled [aria-disabled=true]:hover:text-link-disabled",  {    variants: {      variant: {        primary: "no-underline",        secondary: "underline decoration-dotted underline-offset-4",      },    },    defaultVariants: {      variant: "primary",    },  },);const affordanceIconClass =  "size-4 shrink-0 text-muted-foreground transition-[opacity,color] duration-200 opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100 group-hover:text-link-secondary group-focus-visible:text-link-secondary";function LinkAffordanceIcon({ mode }: { mode: "external" | "plus" }) {  if (mode === "plus") {    return <Plus aria-hidden className={affordanceIconClass} />;  }  return <ExternalLink aria-hidden className={affordanceIconClass} />;}export type AppLinkIcon = "external" | "plus" | "none";export interface AppLinkProps  extends    React.ComponentPropsWithoutRef<typeof Link>,    VariantProps<typeof linkVariants> {  disabled?: boolean;  /** Hover/focus affordance: trailing `ExternalLink` (default), leading `Plus`, or `none` for custom icons. */  icon?: AppLinkIcon;}const AppLink = React.forwardRef<HTMLAnchorElement, AppLinkProps>(  (    {      className,      variant,      disabled,      href,      onClick,      children,      icon = "external",      ...props    },    ref,  ) => {    const isDisabled =      typeof disabled === "string" ? disabled !== "false" : Boolean(disabled);    if (isDisabled) {      return (        <span          role="link"          aria-disabled="true"          className={cn(            linkVariants({ variant }),            "cursor-not-allowed text-link-disabled opacity-60",            className,          )}          tabIndex={-1}          {...props}        >          {children}        </span>      );    }    const showAffordance = icon !== "none";    const affordanceBefore = showAffordance && icon === "plus";    const affordanceAfter = showAffordance && icon === "external";    return (      <Link        ref={ref}        className={cn(linkVariants({ variant }), className)}        href={href ?? "#"}        onClick={onClick}        {...props}      >        {affordanceBefore ? <LinkAffordanceIcon mode="plus" /> : null}        {children}        {affordanceAfter ? <LinkAffordanceIcon mode="external" /> : null}      </Link>    );  },);AppLink.displayName = "AppLink";export { AppLink, linkVariants };

Check the import paths to ensure they match your project setup.

Usage

import { AppLink } from "@/components/ui/link";
<AppLink href="/docs">Doc Link</AppLink>

Examples

Primary

Primary linkDisabled primary

Secondary

Secondary linkDisabled secondary

Affordance icon (icon)

By default, a trailing ExternalLink icon (same as in these docs) fades in on hover and focus. Use icon="plus" for a leading Plus icon, or icon="none" when you supply your own icons.

With Icon

Compose your own icons with icon="none" so the built-in affordance does not duplicate them.

API Reference

PropTypeDescription
variant"primary" | "secondary"Controls underline and color styles.
icon"external" | "plus" | "none"Hover/focus affordance: trailing external (default), leading plus, or none.
disabledbooleanDisables interaction and focus.
hrefstringDestination URL.

On this page