# Theme component library A theme ships the components your pages use: the `{% tag %}` definitions in its `components/` folder, the React islands they mount in `islands/`, and the extra component libraries its `theme.yaml` declares. The packaged `vark` theme is one such library. This page is the contract a theme's library is written against, theme API `"1.0"`: how theme folders layer, what `theme.yaml` holds, which aardvark modules an island may import, and what an island declares so that aardvark can read its props. How to write a component itself is in [Custom components](/authoring/custom-components/). ## Layers and `theme.yaml` aardvark reads a theme as **layers**: the packaged theme of that name (only `vark` ships), then your `themes//` on top of it. Part of a theme merges across the layers, and part doesn't: - A theme's **`components/`** and **`islands/`** folders and its **`theme.yaml`** are merged across the layers. A file in your folder replaces the packaged file of the same name, and everything you don't override keeps coming from aardvark, new components included when you upgrade. Every `.md` file in `components/` is a component definition, as in your project's own `components/` (a `README.md`, `CONTRIBUTING.md` or `CHANGELOG.md` excepted). - The **top-level templates and assets** (`default.html`, `theme.scss`, the scripts) are not merged: they come from the top theme folder alone, with `templates/` on top (see [Theme & customization](/theming/#where-the-theme-lives)). A file missing from that folder is not filled in from the packaged theme. `vark new` sets up exactly this: it copies the top-level files, not the theme's `theme.yaml`. A folder whose `theme.yaml` declares `themeApi` is a **complete** theme instead: it replaces the packaged theme of the same name rather than layering over it (it still builds on the theme it extends, below). Use that for a theme you maintain in full: a complete `vark` builds on no other theme unless its `theme.yaml` says `extends:`, so none of the packaged built-in components lie beneath it; it defines the ones its pages use in its own `components/`. Its components follow the rules aardvark's own do: each needs a [Python body](/authoring/custom-components/#python-bodies), and an unknown front-matter key stops the build. An island under a name the packaged theme defines, or one with an `@aardvark-island` comment, is treated as the packaged ones are, so a complete copy of `vark` builds exactly as the packaged theme does; an island of its own with no comment is your code, like a snippet (see [Island metadata](#island-metadata)). A folder without `themeApi` follows the rules of your project's `components/`, and its islands are your own code, like [snippets](/authoring/custom-snippets/): aardvark doesn't read their props for search, PDF or the Markdown export. ### Building on another theme A theme named anything other than `vark` builds on the packaged `vark`: its `components/`, `islands/` and `theme.yaml` go on top of vark's, so they only need the files the theme changes. Its top-level templates and assets don't layer; they come from its own folder whole. `extends:` in its `theme.yaml` names another theme to build on, and `extends: null` builds on nothing: ```yaml # themes/brand/theme.yaml extends: vark # the packaged vark plus your themes/vark/; null for none ``` `extends` names a theme in your `themes/` folder or a packaged one, with both of its layers. Without it, a theme builds on the packaged `vark` alone, so a `themes/vark/` folder left in the project doesn't change a site whose theme is another. A value that isn't a plain theme name, a theme that doesn't exist, a loop, or a chain of more than four `extends` links stops the chain at that theme with a build warning. The theme then builds on the packaged `vark`, unless the chain already reached `vark`, so a typo there doesn't break the build either. ### `theme.yaml` keys | Key | What it does | | --- | --- | | `themeApi` | The theme API version the theme is written against, as a quoted `"MAJOR.MINOR"` string: `"1.0"` today. Declaring it makes the folder a complete theme. Keep the quotes: YAML reads an unquoted `1.10` as the number `1.1`. A theme that needs a newer theme API than your aardvark implements stops the build, naming both versions. | | `extends` | The theme this one builds on (see above). | | `componentLibraries` | Extra React component libraries, merged one key at a time over the layers below: an entry replaces the one with the same key, and `: null` removes it. The packaged `vark` declares `charts` and `carousel`. See [Component libraries](/components/extras/component-libraries/). | | `runtime` | The islands runtime the theme's islands render through: `module`, and optionally `css` and `noDomRef`. The packaged `vark` names `@aardvark/islands/mantine-runtime`. See [A theme's runtime](#a-themes-runtime). | | `baseLibrary` | The React component library the theme's islands are built on: `key`, `package`, `vocabulary` and optionally `meta` (its components' [metadata](#island-metadata)), or `null` for none. The packaged `vark`'s is Mantine. | | `nativeMap` | The island that gets the map runtime aardvark ships for `{% map %}`, or `null`: one of the theme's own islands, by name. The packaged `vark`'s is `Map`. | `runtime`, `baseLibrary` and `nativeMap` are each taken whole from the highest layer that declares them. In a complete theme an unknown key, a malformed value or a file the theme names but doesn't have stops the build; in a folder without `themeApi` it warns and is ignored, and the layer below it stands. A `theme.yaml` that can't be read, as a file or as YAML, stops the build in any folder, since aardvark can't tell whether it declares `themeApi`. So does the `themes/` folder, a theme folder, or its `components/` or `islands/`, when it is a symlink to nothing or a file. `theme.yaml` is build input: a built site never serves it. ## The theme's islands A theme's `islands/` folder holds its React components. Each `.jsx` or `.tsx` file is one island, named by its file: `Card.jsx` is the island the `{% card %}` component mounts, and what `component('Card')` names. A file whose name starts with `_` is a helper the islands beside it import (`import { normalizeComment } from './_treeHelpers'`), never an island itself, and an island's stylesheet sits beside it (`Card.jsx` imports `./Card.css`). An island or helper in your folder replaces the packaged one of the same name whatever its extension: your `ArticleCard.tsx` replaces `ArticleCard.jsx`, in the listings that show article cards too, since vark's islands import one another without the extension (`./ArticleCard`). Write your imports that way too. A `.js` or `.ts` file named like an island is not one: the build skips the islands bundle, with a warning naming both files, until one is renamed. Every island a site uses is bundled into one script and one stylesheet. Only the files at the top of `islands/` are bundled (`.jsx`, `.tsx`, `.js`, `.ts`, `.css` and `.json`, which an island imports as data, except a `package.json`, `tsconfig.json` or `jsconfig.json`, which esbuild reads as settings), so keep helpers there too: an island that imports a file that is not bundled (one in a subfolder, say), or a stylesheet whose `url(./…)` names an image, gets a build warning naming the import, and the bundle fails. Two file names that differ only in letter case would be one file on a case-insensitive disk, and two islands or helpers whose names do (`Tabs.jsx`, `tabs.tsx`) are one import, so the build bundles the first (the lower layer's) and warns about the other; the islands that are that file or import it render as their static placeholders. The listing islands (`KnowledgeBase`, `ArticleList` and `Changelog`) take the English defaults of their headings, buttons and messages from `islands/_ui_strings.json`, and the build localizes and translates those labels from the same file. A copy of it in your `islands/` replaces the packaged one whole, so keep every label it has: the build stops before it starts, naming the file and each label yours leaves out. The site chrome is aardvark's own and belongs to no theme: the search box, the reader assistant, page feedback, the survey, the 404 page's search suggestions, the breadcrumbs, the tag cloud, the header buttons and the fallback navigation icon (`Search`, `AiAssistant`, `PageFeedback`, `Survey`, `NotFoundSuggestions`, `SiteBreadcrumbs`, `TagCloud`, `TopButton` and `NavIcon`). A theme island with one of those names is ignored, with a build warning. ### Modules aardvark provides to islands An island imports its siblings in `islands/` (by `./`), npm packages the site installs, and the modules below, which aardvark provides under the specifier `@aardvark/islands/`. The site chrome is built from them too, so a theme's islands behave like it. They are part of theme API `"1.0"`, and they are the only aardvark modules an island may import. An import that climbs out of `islands/` (`../`, in any `import`, `import()`, `require()` or stylesheet `@import`) reaches aardvark's own files, which can change in any release, so the build warns about it. ```jsx import { sanitizeHref } from '@aardvark/islands/safe-href'; ``` #### `@aardvark/islands/safe-href` `sanitizeHref(href)` returns `href` when it is an http(s) or relative URL and `undefined` otherwise — a `javascript:`, `data:` or protocol-relative URL, even percent-encoded — so a link built from an author's attribute can't run script. It reads no browser globals, so it also runs in the build's prerender. #### `@aardvark/islands/tabler-icons` [Tabler](https://tabler.io/icons) icons, fetched once per name from the icon CDN the build passes an island as `iconCdn`. `LazyTablerIcon({ name, iconCdn, style, className })` renders a fixed-size box and fills it with the icon when it arrives. `classifyIcon(value)` sorts an icon value into `svg`, `image`, `fa`, `emoji`, `tabler`, `unknown` or `none`; `tablerIconUrl(iconCdn, name, style)` is the URL an icon is fetched from. `sanitizeSvg(svg)` rebuilds an SVG an author wrote or the icon CDN served from an allowlist of SVG elements and attributes, keeping only same-document references, and returns `null` for anything that isn't one ``. It is meant for those two sources, not for SVG from untrusted input. `tablerGlyph(icon, iconCdn, className)` renders a Tabler name only, `bulletNode(bullet, iconCdn, className)` a Tabler name or any other text as it is, and `resolveCardIcon(icon, options)` any icon value inside a Mantine `ThemeIcon`. #### `@aardvark/islands/code-actions` The Copy and Download buttons of a code block: the default export, `CodeActions({ text, filename })`, and `HighlightedPre({ code, codeHtml, lang, className, ariaLabel })`, the `
` they sit beside. They render the same markup the theme's `code-blocks.js` adds to
Markdown code: an `.aardvark-code-actions` group of `.aardvark-code-btn` buttons, each carrying
`data-aardvark-code-action="copy"` or `"download"`, and a copy announced through the page's
`#aardvark-live` region. A theme that draws its own code buttons keeps that attribute:
[Content Reach](/content-reach/) counts code copies by it.

#### `@aardvark/islands/highlight`

A small syntax highlighter for code an island renders itself. It emits the class names the
build's own highlighting uses, so the theme's syntax colors apply: `tokenizeCode(code, lang)`,
`highlightHtml(code, lang)`, `resolveHighlightLanguage(lang)` and `LANG_SYNTAX` (the languages it
knows), with `extForLang(lang)`, `codeDownloadFilename({ title, lang, index, uid })` and
`codeDownloadFilenames(specs)` for the file a Download button saves.

#### `@aardvark/islands/tag-cloud`

Tag filtering for listing islands, shared with the chrome's tag cloud so all of them read and
write the same `#tag=` address: `collectTags(entries)`, `filterByActiveTags(items, active)`,
`useTagFilter()`, `useSyncedTagFilter(tagSlugs)`, `tagsFromHash(hash, tagSlugs)`,
`samePath(a, b)`, `dispatchTaxonomyFilterChange(slugs)`, and the `TagBadge` and `TagCloudBox`
components. `TagCloudBox` has no text of its own: it shows the `title` and `clearLabel` its
caller passes, so each island keeps its own labels, and without them it has no heading or Clear
button.

### Islands runtimes

aardvark provides two more modules under `@aardvark/islands/`, and they are not for an
island to import. They are *islands runtimes*: the build wraps each island's root in one, in
the prerender and in the browser. A theme names its own in `theme.yaml` (see
[A theme's runtime](#a-themes-runtime)); the packaged `vark` names `mantine-runtime`, and the
site chrome renders through `mantine-runtime` whatever the theme. An island that called `wrap`
itself would put a second provider inside the one it already has.

#### `@aardvark/islands/mantine-runtime`

The Mantine islands runtime: the site chrome's, and vark's. `wrap(element, ctx)` returns an
island's root inside Mantine's provider, with the site's Mantine theme and, in the browser, the
reader's light/dark choice. `ctx` says whether the build prerendered the island (`hydrate`) and whether
this is that prerender (`ssr`), and carries the `theme` and the reader's `colorScheme` store.
`createLibraryTheme(input)` makes that theme from the palettes and font stacks the build derives
from the theme's `$brand` colors and `theme.fonts`. `ssrHoist` is the CSS selector of the
provider's two global `