# Link (Caseroom component)

Inline in text, standalone with an arrow, or external with an arrow out and a new-tab note for screen readers. Black and white, dark mode, shadcn-compatible; React, Astro and plain HTML.

3.7% of the 2,062 sites in The UI Index have a link (77 sites).

## Install

- React: `npx shadcn@latest add https://www.themes.dev/caseroom/r/link.json`
- Astro: `npx shadcn@latest add https://www.themes.dev/caseroom/r/astro/link.json`
- HTML: https://www.themes.dev/caseroom/r/html/link.html (with https://www.themes.dev/caseroom/r/html/caseroom.css)

Page: https://www.themes.dev/caseroom/components/link/ · Rules for agents: https://www.themes.dev/caseroom/AGENT.md

## Code

### link.astro

```astro
---
import type { HTMLAttributes } from 'astro/types';
import { cn } from '@/lib/utils';
import { ARROW_PATH, EXTERNAL_PATH, linkVariants, type LinkVariants } from './link.variants';

type Props = HTMLAttributes<'a'> & LinkVariants & { href: string; newTab?: boolean };
const { variant, href, newTab, class: className, ...rest } = Astro.props;
const icon = variant === 'standalone' ? ARROW_PATH : variant === 'external' ? EXTERNAL_PATH : null;
---

<a href={href} target={newTab ? '_blank' : undefined} rel={newTab ? 'noopener' : undefined} class={cn(linkVariants({ variant }), className)} {...rest}><slot />{icon && <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d={icon} /></svg>}{newTab && <span class="sr-only"> (opens in a new tab)</span>}</a>
```

### link.tsx

```tsx
import type { AnchorHTMLAttributes } from 'react';
import { cn } from '@/lib/utils';
import { ARROW_PATH, EXTERNAL_PATH, linkVariants, type LinkVariants } from './link.variants';

export type LinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & LinkVariants & { href: string; newTab?: boolean };

/** A link: inline in text, standalone with an arrow, or external with an arrow out. */
export function Link({ variant, href, newTab, className, children, ...props }: LinkProps) {
  const icon = variant === 'standalone' ? ARROW_PATH : variant === 'external' ? EXTERNAL_PATH : null;
  return (
    <a href={href} target={newTab ? '_blank' : undefined} rel={newTab ? 'noopener' : undefined} className={cn(linkVariants({ variant }), className)} {...props}>
      {children}
      {icon && <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d={icon} /></svg>}
      {newTab && <span className="sr-only"> (opens in a new tab)</span>}
    </a>
  );
}
export { linkVariants };
```

### link.variants.ts

```tsx
import { cva, type VariantProps } from 'class-variance-authority';

/** Links. inline: in running text, underlined (the underline darkens on hover and thickens when pressed).
 *  standalone: on its own line, with an arrow that steps forward on hover. external: opens elsewhere, marked with
 *  an arrow out and "(opens in a new tab)" for screen readers when it does. */
export const linkVariants = cva(
  'rounded-sm font-medium text-foreground underline-offset-4 transition-[color,text-decoration-color,background-color] active:bg-accent [&_svg]:size-3.5 [&_svg]:shrink-0 [&_svg]:transition-transform',
  {
    variants: {
      variant: {
        inline: 'underline decoration-foreground/30 hover:decoration-foreground active:decoration-2',
        standalone: 'group inline-flex min-h-6 items-center gap-1.5 hover:underline active:decoration-2 hover:[&_svg]:translate-x-0.5',
        external: 'inline-flex min-h-6 items-center gap-1 underline decoration-foreground/30 hover:decoration-foreground active:decoration-2 hover:[&_svg]:-translate-y-px hover:[&_svg]:translate-x-px',
      },
    },
    defaultVariants: { variant: 'inline' },
  },
);
export type LinkVariants = VariantProps<typeof linkVariants>;
export const ARROW_PATH = 'M3 8h10M9 4l4 4-4 4';
export const EXTERNAL_PATH = 'M5 11l6-6M6 5h5v5';
```
