Button link
StableA link cut from the same lit metal as Button, for calls to action that go somewhere.
ButtonLink is a real <a> with the button finish: graphite or silver, pearl, ghost, outline and inverse, in two pill sizes. It takes every <a> attribute, so href, target and download work as usual. It is a plain anchor: inside a single-page app, put the same lux-btn classes on the router’s Link so the page doesn’t reload.
Link finishes
import { ButtonLink } from '@rchursin/home/ui'
/** * A link cut from the same metal as Button. It goes somewhere, so it is an <a>: these four jump * to sections further down this page. */export default function ButtonLinkVariants() { return ( <div className="flex flex-wrap items-center justify-center gap-3"> <ButtonLink href="#usage">Get started</ButtonLink> <ButtonLink href="#examples" variant="secondary"> See examples </ButtonLink> <ButtonLink href="#api" variant="outline"> Read the API </ButtonLink> <ButtonLink href="#guidelines" variant="ghost"> Guidelines </ButtonLink> </div> )}Usage
import { ButtonLink } from '@rchursin/home/ui'<ButtonLink href="#pricing" size="lg">See pricing</ButtonLink>Examples
Sizes and inverted panels
lg is 40 pixels tall, md 36. On a panel that flips the theme, inverse is always the opposite of the page and outline takes the panel’s text colour.
import { ButtonLink } from '@rchursin/home/ui'import { Icon } from '@rchursin/product/icons/Icon'
/** * Two sizes: lg (40px) for a hero or a closing call to action, md (36px) for headers and cards. * On an inverted panel, use inverse (always the opposite of the page) and outline (takes the * panel's text colour). */export default function ButtonLinkSizes() { return ( <div className="flex w-full max-w-xl flex-col gap-6"> <div className="flex flex-wrap items-center justify-center gap-3"> <ButtonLink href="#usage" size="lg"> Get started <Icon name="arrow-right" className="size-4" /> </ButtonLink> <ButtonLink href="#usage"> Get started <Icon name="arrow-right" className="size-3.5" /> </ButtonLink> </div> <div className="flex flex-col items-center gap-4 rounded-2xl bg-inverse px-6 py-7 text-center text-on-inverse"> <p className="text-[15px] font-medium text-balance">Every example on this page is a real file.</p> <div className="flex flex-wrap items-center justify-center gap-3"> <ButtonLink href="#examples" variant="inverse"> Browse examples </ButtonLink> <ButtonLink href="#source" variant="outline"> View source </ButtonLink> </div> </div> </div> )}On a router link
The same metal on the router’s Link: it changes page without a reload. This site’s own calls to action are built this way.
import { cx } from '@rchursin/components/cx'import { Icon } from '@rchursin/product/icons/Icon'import { Link } from '@rchursin/product/lib/router'
/** * ButtonLink renders a plain <a>, so a site path reloads the page. Inside the app, put the same * metal classes on the router's Link instead: same look, no reload. These two really navigate. */const metal = 'lux-btn inline-flex h-9 items-center justify-center gap-2 rounded-full px-4 text-[13px] font-medium whitespace-nowrap'
export default function ButtonLinkRouter() { return ( <div className="flex flex-wrap items-center justify-center gap-3"> <Link to="/contact" className={cx(metal, 'lux-btn-ink')}> Get in touch <Icon name="arrow-right" className="size-3.5" /> </Link> <Link to="/projects" className={cx(metal, 'lux-btn-paper')}> See projects </Link> </div> )}API
Button link props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'primary' | 'secondary' | 'ghost' | 'inverse' | 'outline' | 'primary' | The finish. Primary is graphite in light mode and silver in dark mode; inverse is the other way round, for inverted panels. |
size | 'md' | 'lg' | 'md' | md is 36 pixels tall (headers, cards), lg is 40 (hero and closing actions). |
href | string | – | Where it goes. Always set it: an <a> without href is not focusable and is not a link. |
className | string | – | Extra classes, merged after the finish (e.g. w-full on a phone). |
children | ReactNode | – | The label, plus an optional icon. Items are spaced 8 pixels apart. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Enter | Follows the link. Space scrolls the page, as it does on any link. |
| Tab | Moves to the next control. Focus shows as a 2px ring 3px outside the pill, in the page’s text colour; on inverse and outline it switches to the panel’s text colour. |
- It renders a real <a>, so screen readers say “link” and the browser handles middle-click, ⌘ or Ctrl-click and “Open in new tab”.
- The label meets 4.5:1 against the metal in both themes; the 36 and 40 pixel heights clear the 24 pixel target minimum.
- If you open a new tab with target="_blank", say so in the label (for example with visually hidden text).
Guidelines
Do
- Use it when the press goes to another page or section; use Button when it does something in place.
- Keep one primary link per view and pair it with a quieter secondary or ghost.
- Use the router’s Link with lux-btn classes for pages inside your app.
Don’t
- Don’t use it for actions like “Save” or “Delete”.
- Don’t leave out href to make a fake button.
- Don’t write “Click here”; say where it goes.
In use
Related
- ButtonThe lit-metal button in five finishes and five sizes, with icons, a loading state and shortcut hints.
- Link & routerA small router with no dependencies: Link changes page without a reload, useRouter says where you are, in history or hash mode.
- Metal finishThe lit-metal button finish as plain classes: graphite, pearl, ghost, outline and inverse, on any element you press.
Source
The component’s own files, exactly as they run on this site.
/** * Small building blocks shared by the four home pages. * All black-and-white; every state is told apart by shape or text, never by colour alone. */import { useEffect, useRef, useState, type ComponentPropsWithoutRef, type CSSProperties, type MouseEvent, type ReactNode,} from "react"import { cx } from "../components/cx"import { useIsApple } from "../product/lib/hotkeys"import { copyText } from "../product/ui/code"import type { Verdict } from "./content"
/** Stagger index for .lux-reveal children (each step is 70ms) */export const delay = (d: number) => ({ "--d": d }) as CSSProperties