Drawn underlines
StableLink underlines that draw themselves in on hover and focus, or slide away from links that are underlined at rest.
lux-link has no underline at rest and draws a 1px line in from the left on hover or keyboard focus, for links that stand on their own. Add lux-link-under and the link is underlined at rest instead, with the line sliding off to the right on hover: the one to use inside running text. The line is drawn in the current text colour.
Updated View source
In text and on their own
import { Link } from '@rchursin/product/lib/router'
/** * Links in running text add .lux-link-under to .lux-link: underlined at rest (so they never * rely on colour), and the line slides away on hover. Links that stand on their own use * .lux-link alone: the underline draws itself in on hover and keyboard focus. */const nav = [ { to: '/projects', label: 'Projects' }, { to: '/about', label: 'About' }, { to: '/library', label: 'Library' }, { to: '/contact', label: 'Contact' },]
export default function DrawnLinksInText() { return ( <div className="flex w-full max-w-lg flex-col gap-8"> <p className="text-[15px]/7 text-pretty text-fg-2"> Every part of this site is in the{' '} <Link to="/library" className="lux-link lux-link-under rounded-sm text-fg focus-ring"> component library </Link> , with the code that runs it. The{' '} <Link to="/library/finishing" className="lux-link lux-link-under rounded-sm text-fg focus-ring"> finishing layer </Link>{' '} is where the metal, light and motion live. </p> <ul className="flex flex-wrap gap-x-6 gap-y-2 text-[14px] font-medium"> {nav.map((n) => ( <li key={n.to}> <Link to={n.to} className="lux-link rounded-sm pb-0.5 text-fg focus-ring"> {n.label} </Link> </li> ))} </ul> </div> )}Usage
// Once, in your app entry: Tailwind, the brand tokens, the effects and the finishing layerimport '@rchursin/product.css'<a href="/projects" className="lux-link">Projects</a>
<p>Read the <a href="/brand" className="lux-link lux-link-under">component library</a>.</p>Examples
Footer columns
Lists of links under headings, where position already says “link”.
import { Link } from '@rchursin/product/lib/router'
/** * Footer columns: each link sits on its own line under a heading, so the drawn underline is a * hover and focus detail, not the only clue that it is a link. */const columns = [ { title: 'Site', links: [ { to: '/', label: 'Home' }, { to: '/projects', label: 'Projects' }, { to: '/about', label: 'About' }, ], }, { title: 'Library', links: [ { to: '/library/foundations', label: 'Foundations' }, { to: '/library/finishing', label: 'Finishing layer' },API
CSS
| Class or variable | What it does |
|---|---|
lux-link | A 1px line in currentColor along the bottom edge, 0% wide at rest and 100% on :hover and :focus-visible, drawn over 450ms on ease-lux. |
lux-link-under | Underlined at rest: the line is 100% wide and shrinks away to the right on hover. Keyboard focus keeps it. It draws its own line, so it works alone or added to lux-link. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Tab | Focuses the link. lux-link draws its underline in; lux-link lux-link-under keeps its line. |
| Enter | Follows the link. |
- Inside a sentence, a link needs more than colour to stand out from the words around it (WCAG 1.4.1). In black and white that is the underline, so use lux-link lux-link-under there.
- lux-link alone has no underline at rest. Use it where the layout already says “link”: navigation, footers, lists of links.
- The drawn line is not a focus indicator on its own; add focus-ring for a 2px outline, as the examples do.
Guidelines
Do
- Use lux-link lux-link-under in paragraphs.
- Use lux-link in nav bars, footers and link lists.
- Add focus-ring so keyboard focus is unmistakable.
Don’t
- Don’t use lux-link alone inside running text.
- Don’t add transition-* utilities: they replace the underline’s transition.
In use
Used on this site
Related
- Link & routerA small router with no dependencies: Link changes page without a reload, useRouter says where you are, in history or hash mode.
- Site footerThe home pages’ footer: three link columns with a cropped wordmark, or one simple row.
- Easing & transitionsFour motion curves, a soft cross-fade for view changes, a reduced-motion hook and disclosures that ease open.
Source
The component’s own files, exactly as they run on this site.
/* RChursin_ finishing layer ("luxe"), ported from the RChursin_ UI kit (rchursin/styles/luxe.css). Needs the --ease-lux tokens in brand.css. Site changes would be marked "Site change:"; there are none, so `node .claude/skills/porting-ui-kit/scripts/kit.ts diff` should report this file in sync. */
/* RChursin_ · the finishing layer ("luxe")
The details that make the kit feel made, not generated: layered light on buttons, hairlines that catch the light, surfaces that lift, a whisper of film grain, and motion that is quick to answer and slow to arrive.
Rules this file follows - One light source, straight above: highlights on top edges, shades below. - Depth comes from hairlines and surface tone first; shadows stay faint. - Clicks answer in ≤200ms. Ambient arrivals take 600–1100ms. Exits are faster than entrances. - Movement only runs when the visitor allows motion; colour and opacity changes stay. - Nothing loops forever on its own. Everything sits in @layer components, so Tailwind utilities on the same element still win.*/