How to add Obsidian-style callouts to Astro
Eiko WagenknechtThis is how callouts appear in Obsidian. If you aim to replicate this style and functionality on your Astro site, this article is for you.
Table of Contents
Callout Syntaxes for Markdown
Callouts are defined in GitHub Flavored Markdown and Obsidian Flavored Markdown. However, they are not compatible with each other, meaning you cannot interchange the syntax between the two formats.
This articles focuses on the Obsidian callouts. For those interested in GitHub callouts (or “alerts”), please refer to my guide: Use GitHub Alerts (Admonitions, Callouts) in Astro.
Obsidian callouts are - like GitHub alerts - defined by starting a blockquote with > [!callouttype]
. This can optionally be followed by a -
or +
sign to make the callout foldable and, also optionally, a custom title. A full callout looks like this:
> [!hint]+ Did you know?
> You can fold this by clicking on it.
The type can be one of the following:
- note
- tip (aliases hint and important)
- warning (aliases caution and attention)
- abstract (aliases summary and tldr)
- info
- todo
- success (aliases check and done)
- question (aliases help and faq)
- failure (aliases fail, missing)
- danger (alias error)
- bug
- example
- quote (alias cite)
All other types will default to note.
While searching for an Obsidian callout parser for remark, I wanted support for:
- All Obsidian callout types, including aliases
- Custom titles
- Foldable callouts
- Styling with CSS
- Custom icons
- Elements in callout titles:
code
, emphasis, headings, math, etc. - Nested callouts
- Handling edge cases the same way as Obsidian
There are multiple options available, but none supports all the features:
- gz-remark-callout
- Supports: Custom titles, Obsidian elements in callout titles, styling with CSS, custom icons
- Doesn’t support: Foldable callouts (this is not in scope, according to the maintainer)
- Unfortunately, for me it only crashes
- remark-obsidian-callout
- Supports: Foldable callouts, custom titles, styling with CSS, custom icons
- Doesn’t support: Obsidian elements in callout titles
- remark-obsidian
- This plugin supports multiple Obsidian features, including callouts.
- Supports: Custom titles, styling with css
- Doesn’t support: Foldable callouts, many callout types, custom icons
- Also, this has a GPL-3.0 license, which might make it unusable for some projects.
- @r4ai/remark-callout
- Supports: Custom titles, foldable callouts**, styling with CSS, custom icons, inline elements in callout titles, icons**
- Doesn’t support: Block elements (like headings) in callout titles
- ** This has been recently added or significantly improved
- remark-callouts, moved here
- This hasn’t been updated in 2 years.
- Supports:
- Doesn’t support: Custom icons, foldable callouts
- Custom remark transformer in Quartz
- This seems to be loosely based on remark-obsidian-callout, but adds some additional features.
- Supports: Custom titles, foldable callouts, all callout types, nested callouts, styling with CSS (limited*)
- Doesn’t support: Headings and LaTeX in callout titles, custom icons
- *: Needs
!important
overrides
All options have some limitations. Therefore, I chose the one that best fits the required features (@r4ai/remark-callout). I added its second GitHub star and initiated a conversation with the maintainer to improve it.
Over the past few days, we collaboratively added many of the missing features with v0.5.0
, including:
- Regex fixes to make it behave more like Obsidian
- Data attribute formatting, also more like Obsidian
- Titles in callouts now added by default
- Support for icons
There are still some minor issues, but the plugin is in a good state, and I’m going to use it on this site. Some of the remaining issues include minor bugs and edge cases that need to be addressed.
To add the plugin to Astro and configure it to behave like Obsidian, you need to apply some custom configuration. This involves modifying the Astro settings to support the specific features of Obsidian callouts.
Installation and Configuration
Install the plugin with your favorite package manager, e.g. pnpm add @r4ai/remark-callout
.
Add the plugin to your astro.config.mjs
file:
import { remarkCallout } from "@r4ai/remark-callout";
function getCalloutIcon(type) {
switch (type) {
case "abstract":
case "summary":
case "tldr":
return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-clipboard-list"><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><path d="M12 11h4"></path><path d="M12 16h4"></path><path d="M8 11h.01"></path><path d="M8 16h.01"></path></svg>';
case "info":
return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-info"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg>';
case "todo":
return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-check-circle-2"><circle cx="12" cy="12" r="10"></circle><path d="m9 12 2 2 4-4"></path></svg>';
case "tip":
case "hint":
case "important":
return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-flame"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"></path></svg>';
case "success":
case "check":
case "done":
return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-check"><path d="M20 6 9 17l-5-5"></path></svg>';
case "question":
case "help":
case "faq":
return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><path d="M12 17h.01"></path></svg>';
case "warning":
case "caution":
case "attention":
return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-alert-triangle"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>';
case "failure":
case "fail":
case "missing":
return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-x"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>';
case "danger":
case "error":
return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-zap"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>';
case "bug":
return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-bug"><path d="m8 2 1.88 1.88"></path><path d="M14.12 3.88 16 2"></path><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"></path><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"></path><path d="M12 20v-9"></path><path d="M6.53 9C4.6 8.8 3 7.1 3 5"></path><path d="M6 13H2"></path><path d="M3 21c0-2.1 1.7-3.9 3.8-4"></path><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"></path><path d="M22 13h-4"></path><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"></path></svg>';
case "example":
return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-list"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>';
case "quote":
case "cite":
return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-quote"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"></path><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"></path></svg>';
case "note":
return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-pencil"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"></path><path d="m15 5 4 4"></path></svg>';
default: // Note icon as fallback as well
return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-pencil"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"></path><path d="m15 5 4 4"></path></svg>';
}
}
export default defineConfig({
markdown: {
remarkPlugins: [
[
remarkCallout,
{
root: (callout) => ({
tagName: "div",
properties: {
dataCalloutFold: callout.isFoldable
? callout.defaultFolded
? "-"
: "+"
: false,
dataCallout: callout.type,
dataCalloutDefaultFolded:
callout.defaultFolded == null
? undefined
: String(callout.defaultFolded),
class: callout.isFoldable
? callout.defaultFolded
? "callout is-collapsible is-collapsed"
: "callout is-collapsible"
: "callout",
},
}),
title: (callout) => ({
tagName: "div",
properties: {
class: "callout-title",
},
}),
body: () => ({
tagName: "div",
properties: {
dataCalloutTitle: true,
class: "callout-content",
},
}),
icon: (callout) => ({
tagName: "div",
properties: {
className: "callout-icon",
},
children: getCalloutIcon(callout.type),
}),
foldIcon: (callout) =>
callout.isFoldable
? {
tagName: "div",
properties: {
className: "callout-fold",
},
children:
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-chevron-down"><path d="m6 9 6 6 6-6"></path></svg>',
}
: undefined,
},
],
],
},
});
Add CSS to style it as you like (I use Tailwind):
.callout {
@apply border;
@apply rounded-md;
@apply px-0;
@apply py-4;
@apply bg-zinc-800;
}
.callout-title {
@apply flex;
@apply flex-row;
@apply gap-2;
@apply items-center;
@apply font-semibold;
@apply mb-2;
}
/* Partially undo Tailwind prose */
.callout-content p:last-of-type {
@apply font-normal;
@apply mb-0;
}
.callout-icon svg {
@apply w-5;
@apply h-5;
}
.callout.is-collapsed .callout-fold svg {
@apply -rotate-90;
}
.callout.is-collapsed .callout-content {
@apply hidden;
}
/* Default callout, also applies to "note" */
.callout[data-callout],
.callout[data-callout="note" i],
.callout[data-callout="info" i],
.callout[data-callout="todo" i] {
@apply text-text-inverted;
@apply border-s-blue-500;
@apply border-s-8;
@apply my-6;
@apply ps-4;
}
.callout[data-callout] > .callout-title,
.callout[data-callout="note" i] > .callout-title,
.callout[data-callout="info" i] > .callout-title,
.callout[data-callout="todo" i] > .callout-title {
@apply text-blue-400;
}
.callout[data-callout="abstract" i],
.callout[data-callout="summary" i],
.callout[data-callout="tldr" i],
.callout[data-callout="tip" i],
.callout[data-callout="hint" i],
.callout[data-callout="important" i] {
@apply border-s-teal-500;
}
.callout[data-callout="abstract" i] > .callout-title,
.callout[data-callout="summary" i] > .callout-title,
.callout[data-callout="tldr" i] > .callout-title,
.callout[data-callout="tip" i] > .callout-title,
.callout[data-callout="hint" i] > .callout-title,
.callout[data-callout="important" i] > .callout-title {
@apply text-teal-400;
}
.callout[data-callout="success" i],
.callout[data-callout="check" i],
.callout[data-callout="done" i] {
@apply border-s-green-500;
}
.callout[data-callout="success" i] > .callout-title,
.callout[data-callout="check" i] > .callout-title,
.callout[data-callout="done" i] > .callout-title {
@apply text-green-400;
}
.callout[data-callout="question" i],
.callout[data-callout="help" i],
.callout[data-callout="faq" i],
.callout[data-callout="warning" i],
.callout[data-callout="caution" i],
.callout[data-callout="attention" i] {
@apply border-s-yellow-500;
}
.callout[data-callout="question" i] > .callout-title,
.callout[data-callout="help" i] > .callout-title,
.callout[data-callout="faq" i] > .callout-title,
.callout[data-callout="warning" i] > .callout-title,
.callout[data-callout="caution" i] > .callout-title,
.callout[data-callout="attention" i] > .callout-title {
@apply text-yellow-400;
}
.callout[data-callout="failure" i],
.callout[data-callout="fail" i],
.callout[data-callout="missing" i],
.callout[data-callout="danger" i],
.callout[data-callout="error" i],
.callout[data-callout="bug" i] {
@apply border-s-red-500;
}
.callout[data-callout="failure" i] > .callout-title,
.callout[data-callout="fail" i] > .callout-title,
.callout[data-callout="missing" i] > .callout-title,
.callout[data-callout="danger" i] > .callout-title,
.callout[data-callout="error" i] > .callout-title,
.callout[data-callout="bug" i] > .callout-title {
@apply text-red-400;
}
.callout[data-callout="example" i] {
@apply border-s-purple-500;
}
.callout[data-callout="example" i] > .callout-title {
@apply text-purple-400;
}
.callout[data-callout="quote" i],
.callout[data-callout="cite" i] {
@apply border-s-zinc-500;
}
.callout[data-callout="quote" i] > .callout-title,
.callout[data-callout="cite" i] > .callout-title {
@apply text-zinc-400;
}
Add custom JavaScript to your base layout to make the callouts foldable:
<!-- Collapsible element toggle -->
<script>
let collapsibleElements = Array.from(
document.querySelectorAll(".is-collapsible"),
);
collapsibleElements.forEach((element) => {
element.addEventListener("click", () => {
element.classList.toggle("is-collapsed");
});
});
</script>
Callouts should look like this now:
You can fold this by clicking on it.
I deliberately tweaked the CSS to suit this site’s design preferences, making it look slightly different from Obsidian. The “Original” look can be achieved with a few adjustments to the CSS.
No Comments? No Problem.
This blog doesn't support comments, but your thoughts and questions are always welcome. Reach out through the contact details in the footer below.