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 when | Incremental rendering | |
|---|---|---|
MarkdownComponent | You are displaying a document. This is the default choice. | Yes |
MarkdownPipe | You need the HTML inside a template you already control. | No |
MarkdownService | You need the HTML outside a template — pre-rendering, a custom pipe, a report generator. | n/a |
A complete example
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);
}
}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 },
}),
],
};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
Installation
Packages, peer dependencies, and the optional mermaid entry point.
Quick start
From an empty Angular project to a rendered document.
Configuration
Every option accepted by provideMarkdown(), with its default.
Embedding components
How selectors are registered, and what to watch out for.
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.