Skip to content

r4ai/remark-callout

Repository files navigation

remark-callout

npm version test coverage CI Release CodeQL

NPM

A remark plugin to add obsidian style callouts to markdown.

> [!note] title here
> body here

Installation

# npm
npm install @r4ai/remark-callout

# pnpm
pnpm install @r4ai/remark-callout

# bun
bun add @r4ai/remark-callout

Usage

See Usage.

Quick Start

Vanilla JS

import remarkParse from "remark-parse";
import { unified } from "unified";
import remarkCallout from "@r4ai/remark-callout";
import remarkRehype from "remark-rehype";
import rehypeRaw from "rehype-raw";
import rehypeStringify from "rehype-stringify";

const md = `
  > [!note] title here
  > body here
`;

const html = unified()
  .use(remarkParse)
  .use(remarkCallout)
  .use(remarkRehype, { allowDangerousHtml: true })
  .use(rehypeRaw)
  .use(rehypeStringify)
  .processSync(md)
  .toString();

console.log(html);

yields:

<div data-callout data-callout-type="note">
  <div data-callout-title>title here</div>
  <div data-callout-body>
    <p>body here</p>
  </div>
</div>

Warning

To display the callout icon as HTML using options.icon or options.foldIcon, you need to set the allowDangerousHtml option to true in remark-rehype and add rehype-raw as a plugin.

Astro

  1. Install the plugin:

    npm install @r4ai/remark-callout
  2. Add @r4ai/remark-callout to remark plugins in your astro config file (e.g. astro.config.ts):

    // astro.config.ts
    import remarkCallout from "@r4ai/remark-callout";
    
    export default defineConfig({
      // ...
      markdown: {
        // ...
        remarkPlugins: [
          // ...
          remarkCallout,
        ],
      },
    });

    Note: This plugin works fine in MDX files as well. For instructions on how to use MDX with Astro, see @astrojs/mdx.

  3. Start using callouts in your markdown or mdx files:

    > [!note] title here
    > body here

    yields:

    <div data-callout data-callout-type="note">
      <div data-callout-title>title here</div>
      <div data-callout-body>
        <p>body here</p>
      </div>
    </div>

    Now you can style the callouts using CSS. Following is an example of how you can style the callouts using Tailwind CSS:

    [data-callout] {
    & {
    @apply my-6 space-y-2 rounded-lg border border-blue-600/20 bg-blue-400/20 p-4 pb-5 dark:border-blue-800/20 dark:bg-blue-600/10;
    }
    & > [data-callout-title] {
    & {
    @apply flex flex-row items-start gap-2 p-0 font-bold text-blue-500;
    }
    &:not:only-child {
    @apply mb-2;
    }
    &:empty::after {
    content: "Note";
    }
    &::before {
    @apply mt-1 block h-5 w-5 bg-current content-[""];
    mask-repeat: no-repeat;
    mask-size: cover;
    /* lucide-pencil */
    mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMiIgZD0iTTE3IDNhMi44NSAyLjgzIDAgMSAxIDQgNEw3LjUgMjAuNUwyIDIybDEuNS01LjVabS0yIDJsNCA0Ii8+PC9zdmc+");
    }
    }
    & > [data-callout-body] {
    & {
    @apply space-y-2;
    }
    & > * {
    @apply m-0;
    }
    }
    }
    details[data-callout] > summary[data-callout-title] {
    & {
    @apply cursor-pointer;
    }
    &::after {
    @apply w-full bg-right bg-no-repeat;
    content: "";
    /* lucide:chevron-right */
    background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzg4ODg4OCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjIiIGQ9Im05IDE4bDYtNmwtNi02Ii8+PC9zdmc+");
    background-size: 1.5rem;
    }
    &:not(:empty)::after {
    @apply my-auto ml-auto h-6 w-6;
    }
    }
    details[data-callout][open] > summary[data-callout-title]::after {
    /* lucide:chevron-down */
    background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzg4ODg4OCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjIiIGQ9Im02IDlsNiA2bDYtNiIvPjwvc3ZnPg==");
    }
    [data-callout][data-callout-type="info"] {
    & {
    @apply border-blue-600/20 bg-blue-400/20 dark:border-blue-800/20 dark:bg-blue-600/10;
    }
    & > [data-callout-title] {
    & {
    @apply text-blue-500;
    }
    &:empty::after {
    content: "Info";
    }
    &::before {
    /* lucide:info */
    mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzg4ODg4OCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjIiPjxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIi8+PHBhdGggZD0iTTEyIDE2di00bTAtNGguMDEiLz48L2c+PC9zdmc+");
    }
    }
    }
    [data-callout][data-callout-type="todo"] {
    & {
    @apply border-blue-600/20 bg-blue-400/20 dark:border-blue-800/20 dark:bg-blue-600/10;
    }
    & > [data-callout-title] {
    & {
    @apply text-blue-500;
    }
    &:empty::after {
    content: "ToDo";
    }
    &::before {
    /* lucide:circle-check-big */
    mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzg4ODg4OCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjIiPjxwYXRoIGQ9Ik0yMiAxMS4wOFYxMmExMCAxMCAwIDEgMS01LjkzLTkuMTQiLz48cGF0aCBkPSJtOSAxMWwzIDNMMjIgNCIvPjwvZz48L3N2Zz4=");
    }
    }
    }
    [data-callout][data-callout-type="abstract"],
    [data-callout][data-callout-type="summary"],
    [data-callout][data-callout-type="tldr"] {
    & {
    @apply border-cyan-600/20 bg-cyan-400/20 dark:border-cyan-800/20 dark:bg-cyan-600/10;
    }
    & > [data-callout-title] {
    & {
    @apply text-cyan-500;
    }
    &::before {
    /* lucide:clipboard-list */
    mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMiI+PHJlY3Qgd2lkdGg9IjgiIGhlaWdodD0iNCIgeD0iOCIgeT0iMiIgcng9IjEiIHJ5PSIxIi8+PHBhdGggZD0iTTE2IDRoMmEyIDIgMCAwIDEgMiAydjE0YTIgMiAwIDAgMS0yIDJINmEyIDIgMCAwIDEtMi0yVjZhMiAyIDAgMCAxIDItMmgybTQgN2g0bS00IDVoNG0tOC01aC4wMU04IDE2aC4wMSIvPjwvZz48L3N2Zz4=");
    }
    }
    }
    [data-callout][data-callout-type="abstract"] > [data-callout-title]:empty::after {
    content: "Abstract";
    }
    [data-callout][data-callout-type="summary"] > [data-callout-title]:empty::after {
    content: "Summary";
    }
    [data-callout][data-callout-type="tldr"] > [data-callout-title]:empty::after {
    content: "TL;DR";
    }
    [data-callout][data-callout-type="tip"],
    [data-callout][data-callout-type="hint"],
    [data-callout][data-callout-type="important"] {
    & {
    @apply border-cyan-600/20 bg-cyan-400/20 dark:border-cyan-800/20 dark:bg-cyan-600/10;
    }
    & > [data-callout-title] {
    & {
    @apply text-cyan-500;
    }
    &::before {
    /* lucide:flame */
    mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMiIgZD0iTTguNSAxNC41QTIuNSAyLjUgMCAwIDAgMTEgMTJjMC0xLjM4LS41LTItMS0zYy0xLjA3Mi0yLjE0My0uMjI0LTQuMDU0IDItNmMuNSAyLjUgMiA0LjkgNCA2LjVjMiAxLjYgMyAzLjUgMyA1LjVhNyA3IDAgMSAxLTE0IDBjMC0xLjE1My40MzMtMi4yOTQgMS0zYTIuNSAyLjUgMCAwIDAgMi41IDIuNSIvPjwvc3ZnPg==");
    }
    }
    }
    [data-callout][data-callout-type="tip"] > [data-callout-title]:empty::after {
    content: "Tip";
    }
    [data-callout][data-callout-type="hint"] > [data-callout-title]:empty::after {
    content: "Hint";
    }
    [data-callout][data-callout-type="important"] > [data-callout-title]:empty::after {
    content: "Important";
    }
    [data-callout][data-callout-type="success"],
    [data-callout][data-callout-type="check"],
    [data-callout][data-callout-type="done"] {
    & {
    @apply border-green-600/20 bg-green-400/20 dark:border-green-800/20 dark:bg-green-600/10;
    }
    & > [data-callout-title] {
    & {
    @apply text-green-500;
    }
    &::before {
    /* lucide:check */
    mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMiIgZD0iTTIwIDZMOSAxN2wtNS01Ii8+PC9zdmc+");
    }
    }
    }
    [data-callout][data-callout-type="success"] > [data-callout-title]:empty::after {
    content: "Success";
    }
    [data-callout][data-callout-type="check"] > [data-callout-title]:empty::after {
    content: "Check";
    }
    [data-callout][data-callout-type="done"] > [data-callout-title]:empty::after {
    content: "Done";
    }
    [data-callout][data-callout-type="question"],
    [data-callout][data-callout-type="help"],
    [data-callout][data-callout-type="faq"] {
    & {
    @apply border-orange-600/20 bg-orange-400/20 dark:border-orange-800/20 dark:bg-orange-600/10;
    }
    & > [data-callout-title] {
    & {
    @apply text-orange-500;
    }
    &::before {
    /* lucide:circle-help */
    mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMiI+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTAiLz48cGF0aCBkPSJNOS4wOSA5YTMgMyAwIDAgMSA1LjgzIDFjMCAyLTMgMy0zIDNtLjA4IDRoLjAxIi8+PC9nPjwvc3ZnPg==");
    }
    }
    }
    [data-callout][data-callout-type="question"] > [data-callout-title]:empty::after {
    content: "Question";
    }
    [data-callout][data-callout-type="help"] > [data-callout-title]:empty::after {
    content: "Help";
    }
    [data-callout][data-callout-type="faq"] > [data-callout-title]:empty::after {
    content: "FAQ";
    }
    [data-callout][data-callout-type="warning"],
    [data-callout][data-callout-type="caution"],
    [data-callout][data-callout-type="attention"] {
    & {
    @apply border-orange-600/20 bg-orange-400/20 dark:border-orange-800/20 dark:bg-orange-600/10;
    }
    & > [data-callout-title] {
    & {
    @apply text-orange-500;
    }
    &::before {
    /* lucide:triangle-alert */
    mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMiIgZD0ibTIxLjczIDE4bC04LTE0YTIgMiAwIDAgMC0zLjQ4IDBsLTggMTRBMiAyIDAgMCAwIDQgMjFoMTZhMiAyIDAgMCAwIDEuNzMtM00xMiA5djRtMCA0aC4wMSIvPjwvc3ZnPg==");
    }
    }
    }
    [data-callout][data-callout-type="warning"] > [data-callout-title]:empty::after {
    content: "Warning";
    }
    [data-callout][data-callout-type="caution"] > [data-callout-title]:empty::after {
    content: "Caution";
    }
    [data-callout][data-callout-type="attention"] > [data-callout-title]:empty::after {
    content: "Attention";
    }
    [data-callout][data-callout-type="failure"],
    [data-callout][data-callout-type="fail"],
    [data-callout][data-callout-type="missing"] {
    & {
    @apply border-red-600/20 bg-red-400/20 dark:border-red-800/20 dark:bg-red-600/10;
    }
    & > [data-callout-title] {
    & {
    @apply text-red-500;
    }
    &::before {
    /* lucide:check */
    mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMiIgZD0iTTIwIDZMOSAxN2wtNS01Ii8+PC9zdmc+");
    }
    }
    }
    [data-callout][data-callout-type="failure"] > [data-callout-title]:empty::after {
    content: "Failure";
    }
    [data-callout][data-callout-type="fail"] > [data-callout-title]:empty::after {
    content: "Fail";
    }
    [data-callout][data-callout-type="missing"] > [data-callout-title]:empty::after {
    content: "Missing";
    }
    [data-callout][data-callout-type="danger"],
    [data-callout][data-callout-type="error"] {
    & {
    @apply border-red-600/20 bg-red-400/20 dark:border-red-800/20 dark:bg-red-600/10;
    }
    & > [data-callout-title] {
    & {
    @apply text-red-500;
    }
    &::before {
    /* lucide:zap */
    mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXphcCI+PHBhdGggZD0iTTQgMTRhMSAxIDAgMCAxLS43OC0xLjYzbDkuOS0xMC4yYS41LjUgMCAwIDEgLjg2LjQ2bC0xLjkyIDYuMDJBMSAxIDAgMCAwIDEzIDEwaDdhMSAxIDAgMCAxIC43OCAxLjYzbC05LjkgMTAuMmEuNS41IDAgMCAxLS44Ni0uNDZsMS45Mi02LjAyQTEgMSAwIDAgMCAxMSAxNHoiLz48L3N2Zz4=");
    }
    }
    }
    [data-callout][data-callout-type="danger"] > [data-callout-title]:empty::after {
    content: "Danger";
    }
    [data-callout][data-callout-type="error"] > [data-callout-title]:empty::after {
    content: "Error";
    }
    [data-callout][data-callout-type="bug"] {
    & {
    @apply border-red-600/20 bg-red-400/20 dark:border-red-800/20 dark:bg-red-600/10;
    }
    & > [data-callout-title] {
    & {
    @apply text-red-500;
    }
    &::before {
    /* lucide:bug */
    mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMiI+PHBhdGggZD0ibTggMmwxLjg4IDEuODhtNC4yNCAwTDE2IDJNOSA3LjEzdi0xYTMuMDAzIDMuMDAzIDAgMSAxIDYgMHYxIi8+PHBhdGggZD0iTTEyIDIwYy0zLjMgMC02LTIuNy02LTZ2LTNhNCA0IDAgMCAxIDQtNGg0YTQgNCAwIDAgMSA0IDR2M2MwIDMuMy0yLjcgNi02IDZtMCAwdi05Ii8+PHBhdGggZD0iTTYuNTMgOUM0LjYgOC44IDMgNy4xIDMgNW0zIDhIMm0xIDhjMC0yLjEgMS43LTMuOSAzLjgtNE0yMC45NyA1YzAgMi4xLTEuNiAzLjgtMy41IDRNMjIgMTNoLTRtLS44IDRjMi4xLjEgMy44IDEuOSAzLjggNCIvPjwvZz48L3N2Zz4=");
    }
    }
    }
    [data-callout][data-callout-type="bug"] > [data-callout-title]:empty::after {
    content: "Bug";
    }
    [data-callout][data-callout-type="example"] {
    & {
    @apply border-purple-600/20 bg-purple-400/20 dark:border-purple-800/20 dark:bg-purple-600/10;
    }
    & > [data-callout-title] {
    & {
    @apply text-purple-500;
    }
    &::before {
    /* lucide:list */
    mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMiIgZD0iTTggNmgxM004IDEyaDEzTTggMThoMTNNMyA2aC4wMU0zIDEyaC4wMU0zIDE4aC4wMSIvPjwvc3ZnPg==");
    }
    }
    }
    [data-callout][data-callout-type="example"] > [data-callout-title]:empty::after {
    content: "Example";
    }
    [data-callout][data-callout-type="quote"],
    [data-callout][data-callout-type="cite"] {
    & {
    @apply border-zinc-600/20 bg-zinc-400/20 dark:border-zinc-800/20 dark:bg-zinc-600/15;
    }
    & > [data-callout-title] {
    & {
    @apply text-zinc-500;
    }
    &::before {
    /* lucide:quote */
    mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMiIgZD0iTTMgMjFjMyAwIDctMSA3LThWNWMwLTEuMjUtLjc1Ni0yLjAxNy0yLTJINGMtMS4yNSAwLTIgLjc1LTIgMS45NzJWMTFjMCAxLjI1Ljc1IDIgMiAyYzEgMCAxIDAgMSAxdjFjMCAxLTEgMi0yIDJzLTEgLjAwOC0xIDEuMDMxVjIwYzAgMSAwIDEgMSAxbTEyIDBjMyAwIDctMSA3LThWNWMwLTEuMjUtLjc1Ny0yLjAxNy0yLTJoLTRjLTEuMjUgMC0yIC43NS0yIDEuOTcyVjExYzAgMS4yNS43NSAyIDIgMmguNzVjMCAyLjI1LjI1IDQtMi43NSA0djNjMCAxIDAgMSAxIDEiLz48L3N2Zz4=");
    }
    }
    }
    [data-callout][data-callout-type="quote"] > [data-callout-title]:empty::after {
    content: "Quote";
    }
    [data-callout][data-callout-type="cite"] > [data-callout-title]:empty::after {
    content: "Cite";
    }

    To use the above CSS, you need to configure Astro's TailwindCSS integration to support nested syntax:

    // astro.config.ts
    import { defineConfig } from 'astro/config';
    import tailwind from '@astrojs/tailwind';
    
    export default defineConfig({
      integrations: [
        tailwind({
          // Example: Allow writing nested CSS declarations
          // alongside Tailwind's syntax
          nesting: true,
        }),
      ],
    });

    cf. https://docs.astro.build/en/guides/integrations-guide/tailwind/#nesting

    Or if you are using MDX, you can use custom components to style the callouts:

    // astro.config.ts
    import { remarkCallout } from "@r4ai/remark-callout";
    
    export default defineConfig({
      // ...
      markdown: {
        // ...
        remarkPlugins: [
          // ...
          [
            remarkCallout,
            {
              root: (callout) => ({
                tagName: "callout",
                properties: {
                  calloutType: callout.type,
                  isFoldable: String(callout.isFoldable),
                },
              }),
              title: (callout) => ({
                tagName: "callout-title",
                properties: {
                  calloutType: callout.type,
                  isFoldable: String(callout.isFoldable),
                },
              }),
            },
          ],
        ],
      },
    });
    ---
    // src/components/Callout.astro
    
    type Props = {
      calloutType: string
      isFoldable: boolean
    }
    const { calloutType, isFoldable } = Astro.props
    ---
    
    <div
      class={/* Your TailwindCSS style here */}
    >
      <slot />
    </div>
    ---
    // src/components/CalloutTitle.astro
    
    type Props = {
      callouType: string
      isFoldable: boolean
    }
    const { calloutType, isFoldable } = Astro.props
    ---
    
    <div
      class={/* Your TailwindCSS style here */}
    >
      <SomeIconComponent />
      <slot />
    </div>
    ---
    // src/pages/callout-example.astro
    
    import { Content, components } from "../content.mdx";
    import Callout from "../components/Callout.astro";
    import CalloutTitle from "../components/CalloutTitle.astro";
    ---
    
    <Content components={{ ...components, callout: Callout, "callout-title": CalloutTitle }} />

Options

See r4ai.github.io/remark-callout/docs/en/api-reference/type-aliases/options

Development

Commands

Command Description
bun install Install dependencies
bun run build Build the packages
bun run test Run tests
bun run test:coverage Run tests with coverage
bun run check Check the code
bun run check:write Check and fix the code
bun run changeset Create a changeset

Directory Structure

Directory Description
examples/nextjs Example Next.js project
packages/remark-callout The remark-callout package
packages/website The documentation website for remark-callout

Getting Started

  1. Install dependencies:

    bun install
  2. Build the packages:

    bun run build
  3. Check and fix the code:

    bun run check:write
  4. Run tests with coverage:

    bun run test:coverage
  5. Launch the documentation website:

    bun run --cwd packages/website dev