Custom snippets
A custom snippet is a real React component in snippets/*.jsx — for behavior composition can't express. Usable as both component('Name', …) and a {% Name %} tag. This page explains how a snippet becomes a tag and when to reach for one over a custom component.
A custom snippet is a real React component you drop in snippets/*.jsx. Reach for
one when you need actual React — state, effects, hooks, a third-party React library,
or a browser API — that you can’t express by composing existing components. Unlike a
custom component, which is a build-time macro that
ships no JavaScript, a snippet is JavaScript: it’s bundled and mounts as an
island in the browser.
Write one
A snippet is a .jsx (or .tsx) file under snippets/ that default-exports a React
component. Its props arrive as the component’s props, and anything you wrap becomes
children:
// snippets/Stepper.jsx
import { forwardRef, useState } from 'react';
import { Button } from '@mantine/core';
const Stepper = forwardRef(function Stepper({ label = 'Clicks', start = 0 }, ref) {
const [n, setN] = useState(start);
return <Button ref={ref} onClick={() => setN((c) => c + 1)}>{label}: {n}</Button>;
});
export default Stepper;
That useState is the tell — it holds live client state, which a .md macro can’t.
The file name (Stepper) is the component name. It’s bundled into the site’s single
islands bundle alongside everything else, and a stylesheet it imports
(import './Stepper.css') rides along with it — this site’s snippets/CodeBlock.jsx
does exactly that.
- Forward the ref (
forwardRef, with the ref placed on a real DOM element, as above). That’s what letsattr={…}reach your snippet’s root; a snippet that swallows the ref silently drops everyattrpair — no error, the attributes just never land. - Touch the browser only in effects and handlers. By default islands are prerendered to
HTML at build time (see How islands render), where
there is no
window,documentorlocalStorage. Read them insideuseEffector an event handler, and gate anything that must differ between server and browser (a portal, a measured size) behind amountedflag set in an effect. Otherwise the prerender of that page fails (it then mounts client-side only), or the baked markup disagrees with what the browser renders and React replaces it on load — the reflow prerendering exists to avoid. - A snippet’s name wins.
snippets/Button.jsxreplaces Mantine’sButtoneverywherecomponent('Button')is called — the built-in{% button %}tag included — see Overriding a built-in.
Two ways to use it
A snippet is reachable both as a call and as a tag; both mount the same island,
so use whichever reads better. This site ships snippets/ProductCard.jsx. The call form:
{% component('ProductCard', product='Aardvark Pro', badge='New') %}
and the tag form:
{% ProductCard product="Aardvark Pro" badge="New" %}
both render the same island, live:
Aardvark Pro
Docs that build themselves
- Tag form —
{% ProductCard … %}. Defining the snippet gives you the tag, the same way a custom component does. Scalar attributes become props; a wrapped body (closed with{% endProductCard %}, first letter capitalized) becomeschildren. Theattr={…}escape hatch from Components & snippets works here too. - Call form —
{% component('ProductCard', …) %}. Needed for the two things a tag can’t do: pass a non-scalar prop (a list or dict — tag attributes are scalar, like every directive), and emit from inside aforloop (a{% Name %}tag is expanded when the page is scanned, before any loop runs, so loops use the call form — see the storefront example).
Snippet or custom component?
Both let you define your own building block and call it as a {% Tag %}.
The difference is what’s behind the tag:
Custom component (.md) |
Custom snippet (.jsx / .tsx) |
|
|---|---|---|
| What it is | A build-time macro | A real React component |
| Ships JavaScript? | No — expands into component() calls at build |
Yes — bundled, mounts as an island |
| Body written in | A {% %} template |
JSX |
| Reach for it when | Composing existing components into a named, parameterized tag | You need new React: state, effects, hooks, a third-party React lib, browser APIs |
| Tag form | {% Name %} |
{% Name %} |
| Call form | — | {% component('Name', …) %} |
Rule of thumb: composing what already exists → custom component; writing new React →
snippet. Both earn a {% Name %} tag, so the call site reads the
same — the choice is only about whether you’re wiring existing pieces together or writing
genuine React.
A case that has to be a snippet
The Stripe provider on the Component libraries page is
the canonical “must be a snippet.” Mounting Stripe’s card fields needs loadStripe('pk_…')
— which returns a Promise and injects Stripe.js — wrapped in Stripe’s <Elements> React
context provider. None of that is expressible as a build-time macro (there’s no JavaScript
to run, no React context to hold), so StripeProvider is a snippets/*.jsx file. Because
it’s a snippet, you still get the natural {% StripeProvider %} tag —
that’s exactly this behavior at work.