From 778b743cdb832551ed576c745728358d8bbf9d7a Mon Sep 17 00:00:00 2001
From: Shubham Padia
Date: Wed, 16 Jul 2025 20:33:15 +0700
Subject: Aside: Support custom icons (#2261)
Co-authored-by: HiDeoo <494699+HiDeoo@users.noreply.github.com>
Co-authored-by: Chris Swithinbank ---
.changeset/brave-apples-give.md | 5 ++
.changeset/five-flowers-flash.md | 5 ++
docs/src/content/docs/components/asides.mdx | 36 ++++++++++
docs/src/content/docs/guides/authoring-content.mdx | 15 +++++
packages/markdoc/index.mjs | 4 ++
packages/markdoc/package.json | 2 +-
.../__tests__/remark-rehype/asides.test.ts | 76 +++++++++++++++++++++-
.../generates-aside-caution-custom-icon.html | 1 +
...erates-aside-caution-custom-label-and-icon.html | 1 +
.../generates-aside-danger-custom-icon.html | 1 +
...nerates-aside-danger-custom-label-and-icon.html | 1 +
.../generates-aside-note-custom-icon.html | 1 +
...generates-aside-note-custom-label-and-icon.html | 1 +
...rates-aside-note-multiple-path-custom-icon.html | 1 +
.../snapshots/generates-aside-tip-custom-icon.html | 1 +
.../generates-aside-tip-custom-label-and-icon.html | 1 +
packages/starlight/integrations/asides.ts | 46 ++++++++++++-
packages/starlight/user-components/Aside.astro | 9 ++-
18 files changed, 200 insertions(+), 7 deletions(-)
create mode 100644 .changeset/brave-apples-give.md
create mode 100644 .changeset/five-flowers-flash.md
create mode 100644 packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-caution-custom-icon.html
create mode 100644 packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-caution-custom-label-and-icon.html
create mode 100644 packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-danger-custom-icon.html
create mode 100644 packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-danger-custom-label-and-icon.html
create mode 100644 packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-note-custom-icon.html
create mode 100644 packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-note-custom-label-and-icon.html
create mode 100644 packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-note-multiple-path-custom-icon.html
create mode 100644 packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-tip-custom-icon.html
create mode 100644 packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-tip-custom-label-and-icon.html
diff --git a/.changeset/brave-apples-give.md b/.changeset/brave-apples-give.md
new file mode 100644
index 00000000..4963ac42
--- /dev/null
+++ b/.changeset/brave-apples-give.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/starlight-markdoc': minor
+---
+
+Adds support for the `icon` attribute in the `aside` tag, allowing the use of any of Starlight’s built-in icons.
diff --git a/.changeset/five-flowers-flash.md b/.changeset/five-flowers-flash.md
new file mode 100644
index 00000000..6e5763bb
--- /dev/null
+++ b/.changeset/five-flowers-flash.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/starlight': minor
+---
+
+Adds support for using any of Starlight’s built-in icons in asides.
diff --git a/docs/src/content/docs/components/asides.mdx b/docs/src/content/docs/components/asides.mdx
index a28375b0..e3c0ac8c 100644
--- a/docs/src/content/docs/components/asides.mdx
+++ b/docs/src/content/docs/components/asides.mdx
@@ -132,6 +132,36 @@ A warning aside *with* a custom title.
+### Use custom icons
+
+Override the default aside icons by using the [`icon`](#icon) attribute set to the name of [one of Starlight’s built-in icons](/reference/icons/#all-icons).
+
+
+
+```mdx 'icon="starlight"'
+import { Aside } from '@astrojs/starlight/components';
+
+
+```
+
+
+
+```markdoc 'icon="starlight"'
+{% aside type="tip" icon="starlight" %}
+A warning aside *with* a custom icon.
+{% /aside %}
+```
+
+
+
+
+
+
+
## `
`);
+ await expect(res.code).toMatchFileSnapshot(
+ `./snapshots/generates-aside-${type}-custom-label-and-icon.html`
+ );
+ });
+});
+
test('ignores unknown directive variants', async () => {
const res = await renderMarkdown(`
:::unknown
diff --git a/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-caution-custom-icon.html b/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-caution-custom-icon.html
new file mode 100644
index 00000000..564ad6cd
--- /dev/null
+++ b/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-caution-custom-icon.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-caution-custom-label-and-icon.html b/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-caution-custom-label-and-icon.html
new file mode 100644
index 00000000..c6075535
--- /dev/null
+++ b/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-caution-custom-label-and-icon.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-danger-custom-icon.html b/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-danger-custom-icon.html
new file mode 100644
index 00000000..c276e379
--- /dev/null
+++ b/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-danger-custom-icon.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-danger-custom-label-and-icon.html b/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-danger-custom-label-and-icon.html
new file mode 100644
index 00000000..fc8c46ae
--- /dev/null
+++ b/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-danger-custom-label-and-icon.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-note-custom-icon.html b/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-note-custom-icon.html
new file mode 100644
index 00000000..5727e7be
--- /dev/null
+++ b/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-note-custom-icon.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-note-custom-label-and-icon.html b/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-note-custom-label-and-icon.html
new file mode 100644
index 00000000..506831fc
--- /dev/null
+++ b/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-note-custom-label-and-icon.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-note-multiple-path-custom-icon.html b/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-note-multiple-path-custom-icon.html
new file mode 100644
index 00000000..35cd1b77
--- /dev/null
+++ b/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-note-multiple-path-custom-icon.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-tip-custom-icon.html b/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-tip-custom-icon.html
new file mode 100644
index 00000000..6f6e1eb9
--- /dev/null
+++ b/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-tip-custom-icon.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-tip-custom-label-and-icon.html b/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-tip-custom-label-and-icon.html
new file mode 100644
index 00000000..b263140d
--- /dev/null
+++ b/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside-tip-custom-label-and-icon.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/starlight/integrations/asides.ts b/packages/starlight/integrations/asides.ts
index 85334d70..42e6098d 100644
--- a/packages/starlight/integrations/asides.ts
+++ b/packages/starlight/integrations/asides.ts
@@ -1,7 +1,7 @@
///
import type { AstroConfig, AstroIntegration, AstroUserConfig } from 'astro';
-import { h as _h, s as _s, type Properties } from 'hastscript';
+import { h as _h, s as _s, type Properties, type Result } from 'hastscript';
import type { Node, Paragraph as P, Parent, PhrasingContent, Root } from 'mdast';
import {
type Directives,
@@ -14,8 +14,12 @@ import { toString } from 'mdast-util-to-string';
import remarkDirective from 'remark-directive';
import type { Plugin, Transformer } from 'unified';
import { visit } from 'unist-util-visit';
-import type { HookParameters, StarlightConfig } from '../types';
+import type { HookParameters, StarlightConfig, StarlightIcon } from '../types';
import { getRemarkRehypeDocsCollectionPath, shouldTransformFile } from './remark-rehype-utils';
+import { Icons } from '../components/Icons';
+import { fromHtml } from 'hast-util-from-html';
+import type { Element } from 'hast';
+import { AstroError } from 'astro/errors';
interface AsidesOptions {
starlightConfig: Pick;
@@ -88,6 +92,20 @@ function transformUnhandledDirective(
}
}
+/** Hacky function that generates the children of an mdast SVG tree. */
+function makeSvgChildNodes(children: Result['children']): any[] {
+ const nodes: P[] = [];
+ for (const child of children) {
+ if (child.type !== 'element') continue;
+ nodes.push({
+ type: 'paragraph',
+ data: { hName: child.tagName, hProperties: child.properties },
+ children: makeSvgChildNodes(child.children),
+ });
+ }
+ return nodes;
+}
+
/**
* remark plugin that converts blocks delimited with `:::` into styled
* asides (a.k.a. “callouts”, “admonitions”, etc.). Depends on the
@@ -164,6 +182,7 @@ function remarkAsides(options: AsidesOptions): Plugin<[], Root> {
return;
}
const variant = node.name;
+ const attributes = node.attributes;
if (!isAsideVariant(variant)) return;
// remark-directive converts a container’s “label” to a paragraph added as the head of its
@@ -185,6 +204,19 @@ function remarkAsides(options: AsidesOptions): Plugin<[], Root> {
node.children.splice(0, 1);
}
+ let iconPath = iconPaths[variant];
+
+ if (attributes?.['icon']) {
+ const iconName = attributes['icon'] as StarlightIcon;
+ const icon = Icons[iconName];
+ if (!icon) throwInvalidAsideIconError(iconName);
+ // Omit the root node and return only the first child which is the SVG element.
+ const iconHastTree = fromHtml(``, { fragment: true, space: 'svg' })
+ .children[0] as Element;
+ // Render all SVG child nodes.
+ iconPath = makeSvgChildNodes(iconHastTree.children);
+ }
+
const aside = h(
'aside',
{
@@ -202,7 +234,7 @@ function remarkAsides(options: AsidesOptions): Plugin<[], Root> {
fill: 'currentColor',
class: 'starlight-aside__icon',
},
- iconPaths[variant]
+ iconPath
),
...titleNode,
]),
@@ -221,6 +253,14 @@ function remarkAsides(options: AsidesOptions): Plugin<[], Root> {
type RemarkPlugins = NonNullable['remarkPlugins']>;
+export function throwInvalidAsideIconError(icon: string) {
+ throw new AstroError(
+ 'Invalid aside icon',
+ `An aside custom icon must be set to the name of one of Starlight\’s built-in icons, but received \`${icon}\`.\n\n` +
+ 'See https://starlight.astro.build/reference/icons/#all-icons for a list of available icons.'
+ );
+}
+
export function starlightAsides(options: AsidesOptions): RemarkPlugins {
return [remarkDirective, remarkAsides(options)];
}
diff --git a/packages/starlight/user-components/Aside.astro b/packages/starlight/user-components/Aside.astro
index 785a4f20..1a1ab00d 100644
--- a/packages/starlight/user-components/Aside.astro
+++ b/packages/starlight/user-components/Aside.astro
@@ -1,6 +1,8 @@
---
import { AstroError } from 'astro/errors';
import Icon from './Icon.astro';
+import { Icons, type StarlightIcon } from '../components/Icons';
+import { throwInvalidAsideIconError } from '../integrations/asides';
const asideVariants = ['note', 'tip', 'caution', 'danger'] as const;
const icons = { note: 'information', tip: 'rocket', caution: 'warning', danger: 'error' } as const;
@@ -8,9 +10,10 @@ const icons = { note: 'information', tip: 'rocket', caution: 'warning', danger:
interface Props {
type?: (typeof asideVariants)[number];
title?: string;
+ icon?: StarlightIcon;
}
-let { type = 'note', title } = Astro.props;
+let { type = 'note', title, icon } = Astro.props;
if (!asideVariants.includes(type)) {
throw new AstroError(
@@ -20,6 +23,8 @@ if (!asideVariants.includes(type)) {
);
}
+if (icon && !Icons[icon]) throwInvalidAsideIconError(icon);
+
if (!title) {
title = Astro.locals.t(`aside.${type}`);
}
@@ -27,7 +32,7 @@ if (!title) {