Configuration
Every option accepted by provideMarkdown(), with its default and what it affects.
provideMarkdown() takes a MarkdownConfig object. Every field
is optional, and the library works without the provider at all — the configuration token is
injected with { optional: true } and the defaults below apply.
provideMarkdown({
// Shiki theme — a string, or a dark/light pair
theme: { dark: 'github-dark', light: 'catppuccin-latte' },
// Shiki languages to preload at startup
languages: ['typescript', 'javascript', 'html', 'css', 'bash', 'json'],
// Languages/themes outside the library's defaults — imported by your app
extraLanguages: [],
extraThemes: [],
// Angular components registered as Custom Elements
components: {
'my-alert': AlertComponent,
'my-counter': CounterComponent,
},
// ...or whole modules, with selectors read from the decorators
componentModules: [() => import('./demos')],
// Additional markdown-it plugins
plugins: [markdownItAnchor, markdownItFootnote],
// markdown-it constructor options, merged with the library defaults
markdownOptions: { breaks: true },
// Block-level incremental rendering (MarkdownComponent only)
incrementalRendering: true,
// LRU block cache capacity
blockCacheSize: 512,
});Options
Prop
Type
Themes
A single string applies one theme to both colour schemes:
provideMarkdown({ theme: 'github-dark' });A pair emits both themes in one pass. Shiki writes the light colours as inline styles and the
dark ones as --shiki-dark-* CSS variables, so switching schemes is a pure CSS operation — no
re-highlighting, no flash.
provideMarkdown({ theme: { dark: 'github-dark', light: 'catppuccin-latte' } });Activating the dark values requires a few CSS rules; they are given in Styling and dark mode.
Changing the theme at runtime does not invalidate already-rendered blocks. Call
MarkdownService.clearCache() after the change.
Custom themes
Shiki ships 60+ themes, but shikidown only statically bundles the four listed above
(DEFAULT_THEME_NAMES) — importing all of them would defeat the purpose of a small initial bundle.
Any other theme must be imported by your own app and passed via extraThemes:
provideMarkdown({ theme: 'dracula', extraThemes: [() => import('@shikijs/themes/dracula')] });Use the () => import(…) loader form, not a static import: provideMarkdown() is typically called
from app.config.ts, read eagerly at bootstrap, so a static import would land the theme in the
initial bundle instead of a chunk loaded on demand.
Languages
Shiki loads grammars explicitly. The defaults cover the sixteen languages most often found in
technical documents (DEFAULT_LANGUAGE_NAMES):
typescript, javascript, jsx, tsx, html, css, scss, json, yaml, bash, shell,
markdown, sql, python, rust, go
Supplying languages replaces that list rather than extending it, so include the ones you
still need:
provideMarkdown({
languages: ['typescript', 'html', 'css'],
});A fence tagged with a language that was not preloaded is emitted as an unhighlighted
<pre class="shiki"> block. Nothing throws.
Custom languages
Shiki ships 235+ languages — statically importing every one of them would defeat the purpose of
keeping the initial bundle small, which is why only DEFAULT_LANGUAGE_NAMES is bundled by
shikidown itself. A language outside that list, such as angular-ts/angular-html or any
grammar not in the defaults, must be imported by your own app and passed via extraLanguages:
provideMarkdown({
languages: ['typescript', 'html', 'css'],
extraLanguages: [() => import('@shikijs/langs/angular-ts'), () => import('@shikijs/langs/angular-html')],
});Use the () => import(…) loader form, not a static import: provideMarkdown() is typically called
from app.config.ts, read eagerly at bootstrap, so a static import would land the grammar in the
initial bundle instead of a chunk loaded on demand. Only your app pays for that extra chunk — other
consumers of shikidown are unaffected.
markdown-it options and plugins
markdownOptions is merged over the library defaults, which are html: true,
linkify: true and typographer: true. html: true is what allows component selectors to survive
parsing, so overriding it to false disables component embedding.
import markdownItAnchor from 'markdown-it-anchor';
import markdownItFootnote from 'markdown-it-footnote';
provideMarkdown({
plugins: [markdownItAnchor, markdownItFootnote],
markdownOptions: { breaks: true },
});Plugins are applied in array order, once, when the service initialises.
Loading a plugin lazily
A plugin listed above is imported eagerly, so it lands in your initial bundle even on pages without a single formula or footnote. For a heavy one — KaTeX is around 266 kB — pass a loader instead:
provideMarkdown({
plugins: [markdownItAnchor, { load: () => import('@vscode/markdown-it-katex') }],
});Loaders resolve in parallel while the service initialises, then apply in array order — the position in the list decides precedence, not which download finished first.
The default export is unwrapped for you, however deeply it is nested. That matters for CommonJS
plugins: how many default wrappers arrive depends on the bundler, not on the plugin. A dev server
that prebundles its dependencies hands you one; esbuild putting the same package in its own lazy
chunk hands you two, because the chunk's default export is the module.exports object, which
carries a default of its own.
The lazy form is an object, while componentModules accepts a bare () => import('…'). There, a
module is an object and a loader a function, so the two are told apart on sight. A plugin is
itself a function, so nothing distinguishes it from a loader — not even arity, since a plugin may
ignore its argument. The load key removes the ambiguity.
Most markdown-it plugins are published as CommonJS, so the build reports one warning per package:
▲ [WARNING] Module '@vscode/markdown-it-katex' used by 'src/app/app.config.ts' is not ESM
CommonJS or AMD dependencies can cause optimization bailouts.Angular asks that you look for an ESM alternative first. When there is none, list the package under
allowedCommonJsDependencies in the build options of angular.json to acknowledge it — deep
imports of a listed package are covered too:
"allowedCommonJsDependencies": ["@vscode/markdown-it-katex", "katex"]You do not need a plugin for Mermaid. Fenced mermaid blocks are handled by the library
itself — see Mermaid diagrams.
Components
components and componentModules both register Custom Elements at bootstrap, for the whole
application. Their per-instance counterparts are inputs on
MarkdownComponent, and the trade-offs between the two are
covered in Embedding Angular components.
provideMarkdown({ componentModules }) registers every component a module exports — it has no
document to inspect. The [componentModules] input registers only the selectors the document
actually uses. That difference matters; see
Only what the page uses.