Components & snippets
Embed any Mantine component or your own React snippet from Markdown with component(), including nesting.
Call component('Name', **props) to drop a React component into a page. At
build time Aardvark records a mount point and bundles the component; in the browser
it mounts as an island wrapped in a Mantine provider.
How islands render: by default the build prerenders each SSR-capable
widget to static HTML (islands.ssr, on out of the box) — the browser-only ones,
most community widgets and the native map, mount on the client instead — so
crawlers and no-JS readers see real markup and the first paint doesn’t shift; the bundled runtime then
hydrates that markup on load. Set islands: false to skip the prerender —
islands then mount client-side with React (createRoot) on load. A build
without the Node toolchain is a different case: it can’t produce the bundle
either, so it warns and island widgets don’t render at all. This isn’t
Astro-style partial hydration — the whole component bundle ships and every
island mounts.
How islands render: by default the build prerenders each SSR-capable
widget to static HTML (islands.ssr, on out of the box) — the browser-only ones,
most community widgets and the native map, mount on the client instead — so
crawlers and no-JS readers see real markup and the first paint doesn’t shift; the bundled runtime then
hydrates that markup on load. Set islands: false to skip the prerender —
islands then mount client-side with React (createRoot) on load. A build
without the Node toolchain is a different case: it can’t produce the bundle
either, so it warns and island widgets don’t render at all. This isn’t
Astro-style partial hydration — the whole component bundle ships and every
island mounts.
Mantine components
Any Mantine component is available by name:
{% component('Button', children='Get started', color='blue') %}
renders, live:
childrenis the content (text, or anothercomponent(...)call).- All other keyword args become props. They must be JSON-serializable, so pass
data and
defaultValue/defaultCheckedrather than functions. To attach an event handler or any raw HTML attribute, use theattrargument (below). - A name nothing registers — a typo, or a Mantine export your installed version
doesn’t have — doesn’t fail the build: the call renders as an HTML comment (so the page
shows nothing there) and
vark buildwarns which name it couldn’t find, on which pages. Thecomponentsvariable lists every name that is registered (see What’s in scope).
See the Component gallery for live examples.
Custom snippets
Put a React component in snippets/ (a .jsx or .tsx file) and reference it by
filename. This site includes snippets/ProductCard.jsx:
{% component('ProductCard', product='Aardvark Pro', tagline='Docs that build themselves', badge='New') %}
Aardvark Pro
Docs that build themselves
snippet(...) is an alias of component(...) if you prefer to make the intent
explicit.
Overriding a built-in
A snippet wins any name collision with a Mantine component or a built-in island. Name a
file after one — snippets/Button.jsx, snippets/Survey.jsx — and component('Button')
mounts your component everywhere, not the shipped one; Aardvark bundles your file in its
place. That’s the supported way to customize a built-in island wholesale (restyle it, swap
its data source, change its behavior) without forking the theme — for example, replacing the
built-in survey card with your own snippets/Survey.jsx. The precedence is
Mantine → built-in island → your snippet, last one wins, so your snippets/ always takes
priority.
A snippet is also a tag
Putting a file in snippets/ gives you a matching {% Name %}
directive too — the same way a custom component
does. Write it as a tag, pass scalar props as attributes, and let the body become the
snippet’s children:
{% ProductCard product="Aardvark Pro" tagline="Docs that build themselves" badge="New" %}
That’s exactly {% component('ProductCard', …) %}, so reach for
whichever reads better. Two things only the call form can do: pass a non-scalar prop
(a list or dict — tag attributes are scalar, like every other directive), and run inside
a for loop (a {% Name %} tag is expanded when the page is
scanned, before any loop runs). The attr={…} escape hatch below
works in both forms. See Custom snippets for the full story
and when to choose a snippet over a custom component.
Attaching HTML attributes (attr)
Components render to real DOM in the browser, so you never write their final tag —
which means there’s normally nowhere to hang an onclick, a data-* hook, or an
id. The attr argument fills that gap: pass a dict of raw HTML attributes and
they’re applied to the component’s rendered root element when it mounts.
{% component('Button', children='Click me', attr={'onclick': "this.textContent='Clicked!'", 'data-track': 'cta'}) %}
renders a real button whose onclick runs your JavaScript — click it:
Because the value is just a string of JavaScript — and nothing says it has to be a
single line — you can drop a whole anonymous function straight into attr. A
triple-quoted Python string carries a multiline body, so the handler can do real
work inline (here an IIFE; swap in a fetch, analytics, a modal — anything):
{% component('Button', children='Say hello', attr={'onclick': '''
(() => {
const who = 'world';
alert(`Hello, ${who}!`); // …or call any API you like
})()
'''}) %}
Values may be strings, numbers, or booleans (True → a bare boolean attribute;
False/None → omitted). A plain attribute is applied with setAttribute, so a value
can only set an attribute on that one element — it can never inject markup elsewhere.
An on* handler is wired as an event listener on that element: inside it this is the
element, event is the event, and returning false calls preventDefault(). The
handler’s source stays inspectable on the element as data-aardvark-event-<name>.
- Use
attrfor event handlers (onclick, …),data-*/aria-*,id, and other custom attributes. - Use normal props for
classNameandstyle— React manages those, and anattrwould fight it (Aardvark warns in the console if you try). - A few Mantine components render only context and have no DOM node of their own —
Accordion,MenuSub,NumberFormatter.attron these applies only at the top level; nested, put theattron a child element instead. (Menu,Popover, andHoverCarddo takeattr, but it lands on the rendered.Dropdownoverlay — not the trigger; onComboboxit lands on the input.) - Custom snippets support
attrwhen they forward their ref to a root element — seesnippets/ProductCard.jsxand Custom snippets. Every Mantine component supports it out of the box. - An attribute name that isn’t well-formed, or that the site’s
attrPolicy(below) rejects, is dropped with a build warning — never applied, never fatal.
A custom class or style is the one case to reach past attr: pass them as the
className and style props so Mantine merges them with its own. (Setting class
or style through attr would overwrite the component’s — Aardvark warns if you try.)
{% component('Button', children='Custom look', className='cta-pill', style={'borderRadius': '999px', 'textTransform': 'uppercase'}) %}
cta-pill lands on the rendered button alongside Mantine’s own classes (ready for
your CSS to target), and the inline style is merged in — neither clobbers the
component’s styling.
attr is intentionally powerful (it can run JavaScript), in keeping with Aardvark’s
trusted, in-repo content model. A site that wants to restrict it can set an
attrPolicy in aardvark.config.yaml — patterns may end in * for a prefix match:
attrPolicy:
deny: ['on*'] # block inline event handlers
# allow: ['data-*', 'aria-*', 'id'] # …or allowlist-only
Matching is case-insensitive and exact unless the pattern ends in *. deny always
wins; a non-empty allow turns the policy into an allowlist, so list every name you
still want — data-* and aria-* aren’t exempt. The policy covers every attr on the
site, including one forwarded by a tag such as {% button onclick=… %}.
Nesting
Because a component(...) call returns its mount markup, you can nest calls by
passing one as another’s children:
{% component('Group', children=component('Badge', children='New') + component('Badge', children='Beta', color='grape')) %}
This is how compound components (Accordion, Tabs, Card sections, …) are built — see the gallery for examples. For a compound whose body is Markdown rather than a string, reach for a block component instead.
To reuse a chain like this — name it, give it typed parameters, and call it as
{% MyTag … %} instead of repeating the component(...) calls —
define a custom component. It’s the recommended way
to package a composition for reuse.