# Custom components Chaining `component(...)` calls inline is fine once, but it isn't reusable — the composition can't be named, shared, or given a typed interface. A **custom component** fixes that: define it once in a `.md` file under `components/`, with front matter declaring its **tag name** and **parameters** (each with a type, an optional default, and whether it's required), and a `{% %}` template body holding the `component(...)` calls. Then use it by its tag. A custom component is a **build-time macro**: at build it *expands* into the components it wraps and ships **no** JavaScript of its own. (That's the difference from a `snippets/*.jsx` React component — see [the bottom of this page](#custom-components-vs-snippets).) ## Define one `components/BadgeGroup.md` turns the classic "Group of Badges" chain into a tag: ```aardvark --- name: BadgeGroup # REQUIRED — the tag you'll write as {% BadgeGroup %} params: gap: type: string default: xs --- {% component('Group', gap=gap, children=children) %} ``` The declared params (`gap`) and the special `children` slot are available as variables in the body. ## Use it — inline or block Self-closing when there's no body, or a paired tag (`{% end %}`, first letter capitalized) when you want to wrap content. The wrapped content renders in the page and arrives as `children`: ```aardvark {% BadgeGroup %}{% component('Badge', children='New') %}{% component('Badge', children='Beta', color='grape') %}{% endBadgeGroup %} ``` renders, live: NewBeta `children` holds whatever you wrapped — inline text, `component(...)` calls, even other custom components. If you wrap **Markdown** (headings, lists, prose), pad the slot with blank lines in your definition so the page's Markdown pass renders it: `{% component('Card', children="\n\n" + children + "\n\n") %}` (the same trick [block components](/authoring/block-components/) use). If a macro needs audience-specific `changelog` or `openapi` output, put that directive directly inside a paired `{% visibility %}` block in the macro. Do not put visibility around `{% children %}` at that point: the children have already rendered their page-level RSS/navigation side effects, so aardvark fails the build rather than publish them to the wrong audience. ## Parameters Each entry under `params:` declares a typed input: ```yaml params: title: type: string required: true # no default → must be supplied color: type: string default: blue # supplied or this size: int # shorthand: a bare type = optional, no default ``` - **Types**: `string`, `int`, `float`, `bool`, `list`. Values from the call site are coerced to the declared type (so `count="3"` becomes the integer `3`, and a `list` accepts a comma-separated string). A value that can't be coerced is a build error. - **`required: true`** with no value supplied is a build error; **`default`** fills in when the param is omitted. Declaring both is rejected (a default already makes it optional). - Passing a param the component didn't declare is a build error — every mistake is caught at build time, naming the component, the param, and the file. The calling page is **not** visible inside the body — a component is a pure function of its declared inputs. Pass page data explicitly, e.g. `{% Hero title=page.title %}`. For the same reason, emit from a body with `print(...)` or `{% value %}` — `page.print()` isn't available here, since there's no `page` in scope. ## A definition body can blend anything The body is a full template, so it can freely mix **Mantine `component()` calls**, real **Python** (`{% %}` blocks), raw **HTML/CSS**, **` Two things worth knowing about injected JavaScript: - A **`