<link-surface> github npm jayfreestone.com ↗
link-surface progressively expands a real anchor’s
plain-click target across otherwise inert content. This lets whole cards become clickable areas, while allowing for other links and interactive elements to remain unaffected.
Mark one real fallback anchor as the primary link. Interactive elements are left
alone automatically. Optionally use data-link-surface-ignore to
explicitly exclude content and everything inside it.
<link-surface>
<h2>
<a data-primary-link href="/articles/card-design-woes">
Cards
</a>
</h2>
<p>Ten common pitfalls to avoid when designing cards.</p>
<button type="button">Save — does not open the article</button>
<p data-link-surface-ignore>
This text is explicitly excluded from the link target.
</p>
</link-surface>
A plain click anywhere on inert card content is proxied to the headline anchor (watch the log at the bottom of the page). Secondary links, buttons, and marked widgets keep their own behavior. Selecting text and releasing never navigates.
Ten common pitfalls to avoid when designing card components, and why wrapping everything in one big anchor is the worst of them.
Try selecting this sentence and letting go. A surface never hijacks a text selection or a modified click.
No selector can detect every interactive custom element, so this tag
rail is marked data-link-surface-ignore. Clicks on it don't activate the link.
This surface has no a[data-primary-link], so it never
becomes ready: it fails the :has(a[data-primary-link][href])
style rule, so no accent, no pointer cursor, no proxying. The element
degrades to plain markup.
Demo links point at fragment URLs and are intercepted here purely so the page doesn’t jump.