shikidown

Introduction

An Angular Markdown renderer built on Shiki and markdown-it, with Angular components embedded by selector and incremental block rendering.

shikidown renders Markdown in an Angular application. It is a thin, opinionated layer over markdown-it and Shiki that adds the two things a Markdown renderer usually leaves to you: putting live Angular components inside the document, and re-rendering it cheaply when the source changes.

What it does

Shiki v4 highlighting

IDE-quality syntax colours. A dark and a light theme are emitted at once and switched through CSS variables, so toggling the theme costs no re-highlighting.

Angular components by selector

Register a component, then write its selector in the Markdown. Each one is wrapped in a standard Custom Element through @angular/elements.

Incremental block rendering

Only the blocks whose source text changed are re-parsed. Unchanged DOM nodes are kept, so embedded component state survives every keystroke.

Mermaid diagrams

Fenced mermaid blocks are recognised natively. Rendering lives behind a separate entry point, so the mermaid package never enters your bundle unless you ask for it.

Three ways in

The library exposes one component, one pipe and one service. Which one you reach for depends on what you are building.

Use it whenIncremental rendering
MarkdownComponentYou are displaying a document. This is the default choice.Yes
MarkdownPipeYou need the HTML inside a template you already control.No
MarkdownServiceYou need the HTML outside a template — pre-rendering, a custom pipe, a report generator.n/a

A complete example

counter.ts
import { ChangeDetectionStrategy, Component, input, linkedSignal } from '@angular/core';

@Component({
  selector: 'my-counter',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div>Count: {{ count() }}</div>
    <button (click)="increment()">+1</button>
  `,
})
export class CounterComponent {
  readonly initialCount = input(0);
  // linkedSignal seeds from the input and stays writable — the counter resets
  // if the attribute changes, which a plain signal() in ngOnInit would miss.
  readonly count = linkedSignal(() => this.initialCount());

  increment(): void {
    this.count.update((n) => n + 1);
  }
}
app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideMarkdown } from 'shikidown';
import { CounterComponent } from './counter';

export const appConfig: ApplicationConfig = {
  providers: [
    provideMarkdown({
      theme: { dark: 'github-dark', light: 'catppuccin-latte' },
      components: { 'my-counter': CounterComponent },
    }),
  ],
};
docs-page.ts
import { Component } from '@angular/core';
import { MarkdownComponent } from 'shikidown';

@Component({
  imports: [MarkdownComponent],
  template: `<shikidown [content]="md" class="prose dark:prose-invert max-w-none" />`,
})
export class DocsPage {
  readonly md = `# Hello\n\n<my-counter initial-count="5"></my-counter>`;
}

That is the whole setup. The counter in the Markdown is a real Angular component, with its own state and change detection.

Where to go next

Requirements

shikidown targets Angular 22 and above. @angular/*, markdown-it and shiki are peer dependencies you already declare, so they stay out of your dependency graph twice over. The library does pull in a handful of @shikijs/* packages directly (core, engine-oniguruma, langs, themes, types) — these are what let it import individual language grammars and themes instead of Shiki's full bundle, see Custom languages.

The library ships no styles. Typography is yours to bring; see Styling and dark mode for the recommended setup.

On this page