Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/remove-blue-link.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@cloudflare/kumo": major
---

**Breaking:** `Link` now ships a single visual style — an underlined anchor that inherits color from the surrounding text (renders as the default foreground, i.e. black on light mode). The `"inline"`, `"current"`, and `"plain"` variants have been removed and consolidated into a single `"default"` variant.

Migration:

- `<Link variant="inline">` → `<Link>` (default). The result is no longer blue; if you need the brand-blue treatment, wrap the link and add `className="text-kumo-link"` yourself.
- `<Link variant="current">` → `<Link>` (default). Behavior is unchanged; the default now always inherits `currentColor`.
- `<Link variant="plain">` → `<Link className="no-underline">` (or drop `Link` and use a plain `<a>` styled by your app). Kumo no longer ships an un-underlined link variant.

The `KUMO_LINK_VARIANTS` export still exists and continues to satisfy the Kumo variant standard; it now contains a single `default` entry.
13 changes: 7 additions & 6 deletions packages/kumo-docs-astro/src/components/demos/HomeGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -578,12 +578,13 @@ export function HomeGrid() {
id: "link",
Component: (
<div className="flex flex-col gap-2 text-sm">
<Link href="#">Default link</Link>
<Link href="#" variant="current">
Current color link
</Link>
<Link href="#" variant="plain">
Plain link
<Link href="#">Inline link</Link>
<Link
href="https://cloudflare.com"
target="_blank"
rel="noopener noreferrer"
>
External link <Link.ExternalIcon />
</Link>
</div>
),
Expand Down
27 changes: 12 additions & 15 deletions packages/kumo-docs-astro/src/components/demos/LinkDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { Link } from "@cloudflare/kumo";

export function LinkBasicDemo() {
return (
<div className="grid gap-x-6 gap-y-4 text-base md:grid-cols-3">
<Link href="#">Default inline link</Link>
<Link href="#" variant="current">
Current color link
</Link>
<Link href="#" variant="plain">
Plain inline link
<div className="flex flex-wrap items-center gap-x-6 gap-y-4 text-base">
<Link href="#">Underlined inline link</Link>
<Link
href="https://cloudflare.com"
target="_blank"
rel="noopener noreferrer"
>
External link <Link.ExternalIcon />
</Link>
</div>
);
Expand Down Expand Up @@ -37,14 +38,11 @@ export function LinkExternalDemo() {
);
}

export function LinkCurrentVariantDemo() {
export function LinkColorInheritanceDemo() {
return (
<p className="text-base text-kumo-danger">
This error message contains a{" "}
<Link href="#" variant="current">
link
</Link>{" "}
that inherits the red color from its parent.
This error message contains a <Link href="#">link</Link> that inherits
the red color from its parent.
</p>
);
}
Expand All @@ -62,7 +60,7 @@ const CustomRouterLink = ({
export function LinkRenderDemo() {
return (
<div className="flex flex-col gap-x-6 gap-y-4 text-base md:flex-row">
<Link render={<CustomRouterLink href="/dashboard" />} variant="inline">
<Link render={<CustomRouterLink href="/dashboard" />}>
Dashboard (via render)
</Link>
<Link
Expand All @@ -73,7 +71,6 @@ export function LinkRenderDemo() {
rel="noopener noreferrer"
/>
}
variant="inline"
>
Cloudflare Docs <Link.ExternalIcon />
</Link>
Expand Down
81 changes: 22 additions & 59 deletions packages/kumo-docs-astro/src/pages/components/link.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: ~/layouts/MdxDocLayout.astro
title: "Link"
description: "A styled anchor component for inline text links with multiple variants and composition support."
description: "A styled anchor component for inline text links with composition support."
sourceFile: "components/link"
---

Expand All @@ -11,7 +11,7 @@ import {
LinkBasicDemo,
LinkInParagraphDemo,
LinkExternalDemo,
LinkCurrentVariantDemo,
LinkColorInheritanceDemo,
LinkRenderDemo,
} from "~/components/demos/LinkDemo";

Expand Down Expand Up @@ -52,7 +52,9 @@ import { Link } from "@cloudflare/kumo/components/link";
### Basic Link

<p>
The default Link component renders an underlined anchor with primary color styling.
Link renders an underlined anchor that inherits its color from the
surrounding text. There is a single visual style — Kumo intentionally does
not ship a distinct "brand blue" link color.
</p>

```tsx
Expand Down Expand Up @@ -146,9 +148,7 @@ export default function Example() {
return (
<>
{/* Force a specific router link (bypasses LinkProvider) */}
<Link render={<RouterLink to="/dashboard" />} variant="inline">
Dashboard
</Link>
<Link render={<RouterLink to="/dashboard" />}>Dashboard</Link>

{/* Force a plain anchor (bypasses LinkProvider) */}
<Link render={<a />} href="https://example.com" target="_blank" rel="noopener noreferrer">
Expand Down Expand Up @@ -184,14 +184,14 @@ export default function Example() {
<LinkExternalDemo client:visible />
</ComponentExample>

### Current Variant (Color Inheritance)
### Color Inheritance

<p>
The `current` variant inherits color from its parent, useful for links within
colored contexts like alerts.
Link inherits color from its parent, which makes it work naturally inside
colored contexts such as alerts and error messages.
</p>
<ComponentExample demo="LinkCurrentVariantDemo">
<LinkCurrentVariantDemo client:visible />
<ComponentExample demo="LinkColorInheritanceDemo">
<LinkColorInheritanceDemo client:visible />
</ComponentExample>

### Composition with render prop
Expand Down Expand Up @@ -228,11 +228,12 @@ export default function Example() {
<tbody>
<tr class="border-b border-kumo-hairline">
<td class="px-4 py-3 font-mono text-xs">variant</td>
<td class="px-4 py-3 font-mono text-xs">
"inline" | "current" | "plain"
<td class="px-4 py-3 font-mono text-xs">"default"</td>
<td class="px-4 py-3 font-mono text-xs">"default"</td>
<td class="px-4 py-3 text-xs">
Visual style. Currently a single underlined variant that inherits
color from the surrounding text.
</td>
<td class="px-4 py-3 font-mono text-xs">"inline"</td>
<td class="px-4 py-3 text-xs">Visual style variant</td>
</tr>
<tr class="border-b border-kumo-hairline">
<td class="px-4 py-3 font-mono text-xs">render</td>
Expand Down Expand Up @@ -277,41 +278,6 @@ export default function Example() {
</table>
</div>

### Variants

<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-kumo-hairline">
<th class="px-4 py-3 text-left font-semibold">Variant</th>
<th class="px-4 py-3 text-left font-semibold">Description</th>
<th class="px-4 py-3 text-left font-semibold">Use Case</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-kumo-hairline">
<td class="px-4 py-3 font-mono text-xs">inline</td>
<td class="px-4 py-3 text-xs">Primary color with underline</td>
<td class="px-4 py-3 text-xs">Default for inline text links</td>
</tr>
<tr class="border-b border-kumo-hairline">
<td class="px-4 py-3 font-mono text-xs">current</td>
<td class="px-4 py-3 text-xs">
Inherits parent text color with underline
</td>
<td class="px-4 py-3 text-xs">
Links within colored contexts (alerts, errors)
</td>
</tr>
<tr class="border-b border-kumo-hairline">
<td class="px-4 py-3 font-mono text-xs">plain</td>
<td class="px-4 py-3 text-xs">Primary color without underline</td>
<td class="px-4 py-3 text-xs">Navigation links, menus, footers</td>
</tr>
</tbody>
</table>
</div>

### Link.ExternalIcon

<p>
Expand All @@ -336,19 +302,16 @@ export default function Example() {
<div class="space-y-4 text-sm">
<div>

### When to Use Each Variant
### Visual Style

<ul class="ml-4 list-disc space-y-1">
<li>
<strong>inline</strong>: Default choice for links within body text
</li>
<li>
<strong>current</strong>: Links inside alerts, banners, or other
colored containers
Links are underlined and inherit their color from the surrounding
text — there is no separate "brand blue" link color
</li>
<li>
<strong>plain</strong>: Navigation menus, footers, or where underlines
are distracting
Inside colored contexts (alerts, errors, callouts), the link
automatically picks up the parent color
</li>
</ul>
</div>
Expand Down Expand Up @@ -402,7 +365,7 @@ export default function Example() {
The external icon has `aria-hidden="true"` - add descriptive text for
screen readers
</li>
<li>Ensure sufficient color contrast for all variants</li>
<li>Ensure sufficient color contrast with the surrounding text</li>
<li>Use descriptive link text (avoid "click here")</li>
</ul>
</div>
Expand Down
60 changes: 30 additions & 30 deletions packages/kumo-figma/src/generators/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
* Generates a single Link ComponentSet in Figma with variant property.
* Reads variant definitions from component-registry.json (the source of truth).
*
* Link is a text-only component with three variants:
* - inline: Default underlined link (text-primary) with semi-transparent underline
* - current: Inherits color from parent (text-current) with semi-transparent underline
* - plain: No underline decoration (text-primary)
* Link is a text-only component with a single visual style:
* - default: Underlined link that inherits color from parent text
* (text-current) with semi-transparent underline
*
* Underline opacity (from .link-current CSS class in kumo-binding.css):
* - Light mode: 35% opacity
Expand Down Expand Up @@ -90,11 +89,11 @@ export function getLinkParsedVariantStyles(variant: string) {
variant,
classes,
description: variantProp.descriptions[variant] || "",
// Link uses text-primary or text-current classes
hasUnderline: classes.includes("underline"),
// Check if variant uses link-current class (has semi-transparent underline)
hasLinkCurrentClass: classes.includes("link-current"),
textColor: classes.includes("text-current") ? "current" : "primary",
// Link always renders in the surrounding text color (text-current)
textColor: classes.includes("text-current") ? "current" : "default",
};
}

Expand Down Expand Up @@ -128,11 +127,12 @@ export function getAllLinkVariantData() {
/**
* Create a single Link component with the specified variant
*
* For variants with underlines (inline, current), we create a custom underline
* as a separate line element to support the semi-transparent opacity that
* matches the CSS implementation (.link-current class).
* Link renders an underlined text node that inherits its color from the
* surrounding text. The underline is created as a separate line element so
* we can preserve the semi-transparent opacity from the CSS `.link-current`
* class.
*
* @param variant - The variant name (inline, current, plain)
* @param variant - The variant name (currently only "default")
* @param mode - Color mode for underline opacity (light = 35%, dark = 65%)
*/
async function createLinkComponent(
Expand Down Expand Up @@ -161,15 +161,8 @@ async function createLinkComponent(
const textNode = await createTextNode("Link text", FONT_SIZE.base, 400);
textNode.name = "Label";

// Get the appropriate text color variable
let textColorVar;
if (variantData.textColor === "current") {
// For "current" variant, use default text color in Figma demo
textColorVar = getVariableByName(VAR_NAMES.text.default);
} else {
// For inline and plain, use link/brand color
textColorVar = getVariableByName(VAR_NAMES.text.link);
}
// Link inherits parent color; use the default text variable for the Figma preview.
const textColorVar = getVariableByName(VAR_NAMES.text.default);

// Apply text color
if (textColorVar) {
Expand Down Expand Up @@ -234,6 +227,10 @@ async function createLinkComponent(
* Creates a single "Link" ComponentSet with variants derived from
* component-registry.json. Creates both light and dark mode sections.
*
* `figma.combineAsVariants` requires at least two components. Link currently
* ships a single variant, so when only one variant exists we fall back to
* placing the bare component in each section without combining.
*
* @param page - Target page for components
* @param startY - Y position to start placing the section
* @returns The Y position after this section (for next section placement)
Expand Down Expand Up @@ -275,15 +272,18 @@ export async function generateLinkComponents(
components.push(component);
}

// Combine all variants into a single ComponentSet
const componentSet = figma.combineAsVariants(components, page);
componentSet.name = "Link";
componentSet.description =
"Link component with variant styles for navigation";
// Combine variants into a single ComponentSet. `combineAsVariants` requires
// 2+ components; with a single variant we keep the bare component.
const primary: ComponentNode | ComponentSetNode =
components.length > 1
? figma.combineAsVariants(components, page)
: components[0];
primary.name = "Link";
primary.description = "Link component with variant styles for navigation";

// Calculate content dimensions (add label column width)
const contentWidth = componentSet.width + labelColumnWidth;
const contentHeight = componentSet.height;
const contentWidth = primary.width + labelColumnWidth;
const contentHeight = primary.height;

// Content Y offset to make room for title inside frame
const contentYOffset = SECTION_TITLE.height;
Expand All @@ -302,10 +302,10 @@ export async function generateLinkComponents(
contentHeight + SECTION_PADDING * 2 + contentYOffset,
);

// Move ComponentSet into light section frame
lightSection.frame.appendChild(componentSet);
componentSet.x = SECTION_PADDING + labelColumnWidth;
componentSet.y = SECTION_PADDING + contentYOffset;
// Move primary node into light section frame
lightSection.frame.appendChild(primary);
primary.x = SECTION_PADDING + labelColumnWidth;
primary.y = SECTION_PADDING + contentYOffset;

// Add row labels to light section
for (const label of rowLabels) {
Expand Down
Loading