shikidown

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.

app.config.ts
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.

On this page