Navigation
Link card
A card that is one link, with a title, description, icon and meta line, for docs indexes and hub pages.
- Built on
<a href>::after hit area:has()- Client JavaScript
- None
- Works without React
- Yes, as HTML and CSS
- Source, gzipped
- 1.9 kB
- Built from
- Icon
Webhooks
Send events to your server.
Try its options
Loading its options.
Code
import { LinkCard } from "@/components/equip/link-card";
export default function Demo() {
return (
<LinkCard
style={{ width: 216 }}
href="#webhooks"
icon="bolt"
title="Webhooks"
description="Send events to your server."
/>
);
}Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div style="width:216px" class="eq-link-card">
<span class="eq-link-card-icon" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="m" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M13 3 5 13.5h6.5L11 21l8-10.5h-6.5z" />
</svg>
</span>
<div class="eq-link-card-body">
<h3 class="eq-link-card-title">
<a class="eq-link-card-link" href="#webhooks">Webhooks</a>
</h3>
<p class="eq-link-card-description">Send events to your server.</p>
</div>
<svg aria-hidden="true" focusable="false" class="eq-icon eq-link-card-arrow" data-size="s" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12h15" />
<path d="M13.5 6l6 6-6 6" />
</svg>
</div>Examples
Docs hub
Each title is an h3 inside its own link, so the grid can be browsed by heading and each card is one tab stop.
Quickstart
Deploy your first project from a Git repository.
Environment variables
Keep secrets out of your code and scope them per branch.
Custom domains
Point a domain you own at a deployment and issue a certificate.
Command line
Build, preview and roll back from your terminal.
Code
import { LinkCard } from "@/components/equip/link-card";
export default function Example() {
return (
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: 12, width: "100%" }}>
<LinkCard
href="#quickstart"
icon="rocket"
title="Quickstart"
description="Deploy your first project from a Git repository."
meta="5 min read"
/>
<LinkCard
href="#environment-variables"
icon="key"
title="Environment variables"
description="Keep secrets out of your code and scope them per branch."
meta="8 min read"
/>
<LinkCard
href="#custom-domains"
icon="globe"
title="Custom domains"
description="Point a domain you own at a deployment and issue a certificate."
meta="6 min read"
/>
<LinkCard
href="#cli"
icon="terminal"
title="Command line"
description="Build, preview and roll back from your terminal."
meta="12 commands"
/>
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(220px, 1fr));gap:12px;width:100%">
<div class="eq-link-card">
<span class="eq-link-card-icon" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="m" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2.5c3 2 4.5 5.5 4.5 9.5v4.5h-9V12c0-4 1.5-7.5 4.5-9.5z" />
<path d="M7.5 12.5 5 15v3.5h2.5M16.5 12.5 19 15v3.5h-2.5" />
<path d="M10.5 9a1.5 1.5 0 1 0 3 0a1.5 1.5 0 1 0 -3 0" />
<path d="M12 19.25v2" />
</svg>
</span>
<div class="eq-link-card-body">
<h3 class="eq-link-card-title">
<a class="eq-link-card-link" href="#quickstart">Quickstart</a>
</h3>
<p class="eq-link-card-description">Deploy your first project from a Git repository.</p>
<p class="eq-link-card-meta">5 min read</p>
</div>
<svg aria-hidden="true" focusable="false" class="eq-icon eq-link-card-arrow" data-size="s" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12h15" />
<path d="M13.5 6l6 6-6 6" />
</svg>
</div>
<div class="eq-link-card">
<span class="eq-link-card-icon" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="m" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 16a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" />
<path d="M10.83 13.17l8.67-8.67" />
<path d="M17 7l2.5 2.5M14 10l2 2" />
</svg>
</span>
<div class="eq-link-card-body">
<h3 class="eq-link-card-title">
<a class="eq-link-card-link" href="#environment-variables">Environment variables</a>
</h3>
<p class="eq-link-card-description">Keep secrets out of your code and scope them per branch.</p>
<p class="eq-link-card-meta">8 min read</p>
</div>
<svg aria-hidden="true" focusable="false" class="eq-icon eq-link-card-arrow" data-size="s" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12h15" />
<path d="M13.5 6l6 6-6 6" />
</svg>
</div>
<div class="eq-link-card">
<span class="eq-link-card-icon" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="m" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M3 12h18" />
<path d="M12 3c-2.5 2.5-3.75 5.5-3.75 9s1.25 6.5 3.75 9c2.5-2.5 3.75-5.5 3.75-9S14.5 5.5 12 3z" />
</svg>
</span>
<div class="eq-link-card-body">
<h3 class="eq-link-card-title">
<a class="eq-link-card-link" href="#custom-domains">Custom domains</a>
</h3>
<p class="eq-link-card-description">Point a domain you own at a deployment and issue a certificate.</p>
<p class="eq-link-card-meta">6 min read</p>
</div>
<svg aria-hidden="true" focusable="false" class="eq-icon eq-link-card-arrow" data-size="s" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12h15" />
<path d="M13.5 6l6 6-6 6" />
</svg>
</div>
<div class="eq-link-card">
<span class="eq-link-card-icon" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="m" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 4.5h14a2 2 0 0 1 2 2v11a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2z" />
<path d="M7 9.5l3 2.5-3 2.5" />
<path d="M12.5 15h4.5" />
</svg>
</span>
<div class="eq-link-card-body">
<h3 class="eq-link-card-title">
<a class="eq-link-card-link" href="#cli">Command line</a>
</h3>
<p class="eq-link-card-description">Build, preview and roll back from your terminal.</p>
<p class="eq-link-card-meta">12 commands</p>
</div>
<svg aria-hidden="true" focusable="false" class="eq-icon eq-link-card-arrow" data-size="s" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12h15" />
<path d="M13.5 6l6 6-6 6" />
</svg>
</div>
</div>Leaving the product
external opens a new tab, turns the arrow outward and adds "opens in a new tab" to the link's name.
System status (opens in a new tab)
Live uptime and incident history for every region.
Community forum (opens in a new tab)
Ask questions and share what you have built.
Code
import { LinkCard } from "@/components/equip/link-card";
export default function Example() {
return (
<div style={{ display: "grid", gap: 12, width: "100%", maxWidth: 420 }}>
<LinkCard
href="https://status.example.com"
external
icon="activity"
title="System status"
description="Live uptime and incident history for every region."
/>
<LinkCard
href="https://community.example.com"
external
icon="messages"
title="Community forum"
description="Ask questions and share what you have built."
/>
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:12px;width:100%;max-width:420px">
<div class="eq-link-card">
<span class="eq-link-card-icon" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="m" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M2.5 12h4l3-7.5 5 15 3-7.5h4" />
</svg>
</span>
<div class="eq-link-card-body">
<h3 class="eq-link-card-title">
<a target="_blank" rel="noopener noreferrer" class="eq-link-card-link" href="https://status.example.com">System status<span class="eq-link-card-hidden"> (opens in a new tab)</span></a>
</h3>
<p class="eq-link-card-description">Live uptime and incident history for every region.</p>
</div>
<svg aria-hidden="true" focusable="false" class="eq-icon eq-link-card-arrow" data-size="s" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 18 18 6" />
<path d="M9 6h9v9" />
</svg>
</div>
<div class="eq-link-card">
<span class="eq-link-card-icon" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="m" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 9h9a2 2 0 0 1 2 2v5.5a2 2 0 0 1-2 2h-.5V21L15 18.5h-5a2 2 0 0 1-2-2V11a2 2 0 0 1 2-2z" />
<path d="M15.5 5.5V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2v2.5L8 13" />
</svg>
</span>
<div class="eq-link-card-body">
<h3 class="eq-link-card-title">
<a target="_blank" rel="noopener noreferrer" class="eq-link-card-link" href="https://community.example.com">Community forum<span class="eq-link-card-hidden"> (opens in a new tab)</span></a>
</h3>
<p class="eq-link-card-description">Ask questions and share what you have built.</p>
</div>
<svg aria-hidden="true" focusable="false" class="eq-icon eq-link-card-arrow" data-size="s" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 18 18 6" />
<path d="M9 6h9v9" />
</svg>
</div>
</div>Without an icon
Leave out icon for a plain index of guides; the card is still one link from edge to edge.
Migrate from version 2
What changed in routing, caching and configuration.
Release notes
Every change since the last major version.
Code
import { LinkCard } from "@/components/equip/link-card";
export default function Example() {
return (
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", gap: 12, width: "100%" }}>
<LinkCard href="#migrate-from-v2" title="Migrate from version 2" description="What changed in routing, caching and configuration." />
<LinkCard href="#release-notes" title="Release notes" description="Every change since the last major version." meta="Updated 3 days ago" />
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:12px;width:100%">
<div class="eq-link-card">
<div class="eq-link-card-body">
<h3 class="eq-link-card-title">
<a class="eq-link-card-link" href="#migrate-from-v2">Migrate from version 2</a>
</h3>
<p class="eq-link-card-description">What changed in routing, caching and configuration.</p>
</div>
<svg aria-hidden="true" focusable="false" class="eq-icon eq-link-card-arrow" data-size="s" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12h15" />
<path d="M13.5 6l6 6-6 6" />
</svg>
</div>
<div class="eq-link-card">
<div class="eq-link-card-body">
<h3 class="eq-link-card-title">
<a class="eq-link-card-link" href="#release-notes">Release notes</a>
</h3>
<p class="eq-link-card-description">Every change since the last major version.</p>
<p class="eq-link-card-meta">Updated 3 days ago</p>
</div>
<svg aria-hidden="true" focusable="false" class="eq-icon eq-link-card-arrow" data-size="s" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12h15" />
<path d="M13.5 6l6 6-6 6" />
</svg>
</div>
</div>Props
| Prop | Type | Default | What it does |
|---|---|---|---|
href | string | none | Where the card leads. |
title | ReactNode | none | The link text and the card's accessible name. |
description | ReactNode | none | One or two lines under the title. |
icon | IconName | ReactElement | none | An Icon name such as "rocket", or your own element, shown in a tile before the text. |
meta | ReactNode | none | A small line under the description, such as a read time or a count. |
headingLevel | 2 | 3 | 4 | 5 | 6 | 3 | Heading level of the title, to fit the outline of the page. |
external | boolean | false | Opens in a new tab, turns the arrow outward and adds "(opens in a new tab)" for screen readers. |
linkProps | ComponentProps<"a"> | none | Extra attributes for the link itself, such as onClick for a client router or a download attribute. |
...rest | ComponentProps<"div"> | none | Every native div attribute, applied to the card. |
Anatomy
| root | .eq-link-card | The card. Draws the focus ring while its link has keyboard focus. |
|---|---|---|
| icon | .eq-link-card-icon | A bordered tile holding the icon, hidden from assistive tech. |
| body | .eq-link-card-body | |
| title | .eq-link-card-title | The heading. |
| link | .eq-link-card-link | The only link. Its ::after covers the whole card, so the card is the hit area. |
| description | .eq-link-card-description | |
| meta | .eq-link-card-meta | |
| arrow | .eq-link-card-arrow | Moves a little toward the reading direction on hover. |
| hidden text | .eq-link-card-hidden | Only with external. Visually hidden. |
Keyboard
| Enter | Follows the link. |
|---|
Notes
- There is one link per card and its name is the title alone, so a list of links stays short; the description is read in sequence after it.
- The link's ::after covers the card, so a click anywhere follows it; the text cannot be selected with the pointer as a result.
- Do not put other links or buttons inside a link card; they would sit under the covering link and could not be clicked.
- Renders on the server with no client JavaScript.
Source
Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.
components/equip/link-card.tsx63 lines
import type { ComponentProps, ReactElement, ReactNode } from "react";
import { Icon, type IconName } from "./icon";
import "./link-card.css";
export type LinkCardProps = Omit<ComponentProps<"div">, "title"> & {
/** Where the card leads. */
href: string;
/** The link text and the card's accessible name. Keep it short. */
title: ReactNode;
/** One or two lines under the title. */
description?: ReactNode;
/** An icon name from Icon, or your own element, shown before the text. */
icon?: IconName | ReactElement;
/** A small line under the description, such as a read time or a count. */
meta?: ReactNode;
/** The heading level of the title, so a grid of cards can be browsed by heading. */
headingLevel?: 2 | 3 | 4 | 5 | 6;
/** Opens in a new tab, swaps the arrow and says so to screen readers. */
external?: boolean;
/** Extra attributes for the link itself, such as onClick for a client router. */
linkProps?: Omit<ComponentProps<"a">, "href" | "children">;
};
export function LinkCard({
href,
title,
description,
icon,
meta,
headingLevel = 3,
external = false,
linkProps,
className,
...rest
}: LinkCardProps) {
const Heading = `h${headingLevel}` as "h3";
const linkClass = ["eq-link-card-link", linkProps?.className].filter(Boolean).join(" ");
return (
<div {...rest} className={["eq-link-card", className].filter(Boolean).join(" ")}>
{icon != null && (
<span className="eq-link-card-icon" aria-hidden="true">
{typeof icon === "string" ? <Icon name={icon} size="m" /> : icon}
</span>
)}
<div className="eq-link-card-body">
<Heading className="eq-link-card-title">
<a
{...(external ? { target: "_blank", rel: "noopener noreferrer" } : null)}
{...linkProps}
className={linkClass}
href={href}
>
{title}
{external && <span className="eq-link-card-hidden"> (opens in a new tab)</span>}
</a>
</Heading>
{description != null && <p className="eq-link-card-description">{description}</p>}
{meta != null && <p className="eq-link-card-meta">{meta}</p>}
</div>
<Icon className="eq-link-card-arrow" name={external ? "arrow-up-right" : "arrow-right"} size="s" />
</div>
);
}components/equip/link-card.css137 lines
@layer equip {
.eq-link-card {
position: relative;
box-sizing: border-box;
display: flex;
align-items: flex-start;
gap: var(--eq-space-3);
min-width: 0;
padding: var(--eq-space-4);
border: 1px solid var(--eq-line);
border-radius: var(--eq-radius-l);
background: var(--eq-surface);
color: var(--eq-ink);
font: var(--eq-weight) var(--eq-text-m) / 1.45 var(--eq-font);
transition:
background-color var(--eq-dur) var(--eq-ease),
border-color var(--eq-dur) var(--eq-ease),
scale var(--eq-dur) var(--eq-ease);
}
.eq-link-card-icon {
flex: none;
display: grid;
place-items: center;
width: var(--eq-h-m);
height: var(--eq-h-m);
border: 1px solid var(--eq-line);
border-radius: var(--eq-radius-m);
background: var(--eq-raised);
color: var(--eq-ink);
}
.eq-link-card-icon > svg {
width: 20px;
height: 20px;
}
.eq-link-card-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.eq-link-card-title {
margin: 0;
font: inherit;
font-weight: var(--eq-weight-strong);
overflow-wrap: anywhere;
}
.eq-link-card-link {
color: inherit;
text-decoration: none;
outline: none;
}
/* The one link covers the whole card, so the hit area is the card and there is one tab stop. */
.eq-link-card-link::after {
content: "";
position: absolute;
inset: -1px;
z-index: 1;
border-radius: inherit;
}
.eq-link-card-description {
margin: 0;
color: var(--eq-ink-2);
font-size: var(--eq-text-s);
}
.eq-link-card-meta {
margin: var(--eq-space-1) 0 0;
color: var(--eq-ink-3);
font-size: var(--eq-text-xs);
font-variant-numeric: tabular-nums;
}
.eq-link-card-arrow {
flex: none;
margin-top: 2px;
color: var(--eq-ink-3);
transition:
color var(--eq-dur) var(--eq-ease),
translate var(--eq-dur-move) var(--eq-spring);
}
.eq-link-card-arrow:dir(rtl) {
scale: -1 1;
}
.eq-link-card-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
@media (hover: hover) {
.eq-link-card:hover {
border-color: var(--eq-line-strong);
background: color-mix(in srgb, var(--eq-surface) 95%, var(--eq-ink));
}
.eq-link-card:hover .eq-link-card-title {
text-decoration: underline;
text-decoration-color: color-mix(in srgb, currentColor 42%, transparent);
text-underline-offset: 0.22em;
}
.eq-link-card:hover .eq-link-card-arrow {
color: var(--eq-ink);
translate: 2px 0;
}
.eq-link-card:hover .eq-link-card-arrow:dir(rtl) {
translate: -2px 0;
}
}
.eq-link-card:active {
scale: var(--eq-press);
}
/* The ring goes round the whole card, since the whole card is the link. */
.eq-link-card:has(.eq-link-card-link:focus-visible) {
outline: 2px solid var(--eq-focus);
outline-offset: 2px;
}
}