Declared components

Accent used to expand [shortcode] syntax, which embedded a reusable component but declared nothing about it. Nothing knew which attributes [alert] took, so a misspelled attribute rendered as nothing and a typo in a variant name reached the page.

A declared component fixes that. You write a small YAML manifest beside the template, author the component with tag syntax, and Accent validates every use against the declaration.

This is the only component syntax. The square-bracket form has been removed, and accent migrate shortcodes rewrites a site that still uses it.

This page is the reference. For a hands-on walk-through that builds a hero, a call-to-action band, and a teaser grid and composes a landing page from them, see the landing page components tutorial.

Declare the component

A manifest lives beside its template, in your theme’s shortcodes/ directory:

themes/default/shortcodes/
├── callout.html.jinja    the markup
└── callout.yaml          the declaration
# themes/default/shortcodes/callout.yaml
name: callout
description: A titled box that sets a passage apart.
params:
  - name: type
    type: enum
    values: [info, success, warning, danger]
    required: false
    description: The kind of notice.
body: markdown

name must match the filename. params are the attributes, in the order you declare them. body says whether the component wraps content and how that content meets your template:

  • body: markdown is block content: paragraphs, lists, headings, nested components. Accent splices it between blank lines, so the block element your template puts it in closes first and the body renders as markdown.
  • body: inline is a label or a phrase that stays on its line, such as a button’s text. Accent splices it flush; inline markdown in it still renders.
  • Omit body and the component is self-closing.

Attribute types come from the same vocabulary document models use: string, number, boolean, enum, list, object, reference, and media.

A list declares what each item is under items:: a type, and the values, items or fields that type needs. An object declares its fields under fields:, each written like a parameter, with a name and optionally required and description. They nest, so a list of links is:

# themes/default/shortcodes/menu.yaml
name: menu
params:
  - name: links
    type: list
    required: true
    items:
      type: object
      fields:
        - name: label
          type: string
          required: true
        - name: href
          type: reference
          required: true

Content writes the value as an array of objects, and the template reads it like any other attribute:

{% menu links=[{label: "Home", href: "/"}, {label: "Blog", href: "/blog"}] /%}
<ul>{% for link in attrs.links %}<li><a href="{{ url(link.href) }}">{{ link.label }}</a></li>{% endfor %}</ul>

accent validate checks every item and field, and names where a value does not match, such as links[1].label. A list without items: or an object without fields: is a manifest error, because nothing would say what the value holds.

This theme’s related component takes its links that way, and the list below is one:

Inline components

By default a component renders as a block of its own: Accent puts blank lines around its markup so the surrounding markdown reads it as a block. A component meant to sit inside a line – a badge in the middle of a sentence, or in a heading – declares inline: true:

name: edition
description: An inline badge naming the license tier a feature needs.
inline: true
params:
  - name: edition
    type: enum
    values: [free, core_plus, standard, pro]
## Integrate {% edition edition="pro" /%}

Without inline: true that heading would be split into three blocks.

button declares it too, for a different reason: a button is written in a sentence as often as on a line of its own, and {% button href="/x" %}Go{% /button %} on one line is an inline placement. An inline component is valid in both positions; a block component written inside a line is reported as misplaced by accent validate.

Components that read their children

Most components wrap a body. One kind composes: a tabbed switcher has to emit a button per panel, carrying that panel’s label, before any panel. By the time the parent renders, its body is one string of markup and a label cannot be recovered from it – so a component template can read children, the component’s immediate child tags, each with its own attributes and body.

{# themes/default/shortcodes/tabs.html.jinja #}
<div class="tabs" data-tab-group="{{ attrs.group | default("default") }}">
  <div class="tabs-nav" role="tablist">
  {%- for c in children if c.name == "tab" %}
    <button role="tab" data-tab="{{ c.attrs.label | slugify }}">{{ c.attrs.label }}</button>
  {%- endfor %}
  </div>
  {%- for c in children if c.name == "tab" %}
  <div class="tabs-panel" role="tabpanel">{{ c.body }}</div>
  {%- endfor %}
</div>
{% tabs group="os" %}
{% tab label="macOS" %}Install with Homebrew.{% /tab %}
{% tab label="Linux" %}Install with the script.{% /tab %}
{% /tabs %}

Each child’s attrs are the same resolved, escaped values the parent’s own attrs are, so reading a child is no more privileged than reading an attribute. A component that reads children normally ignores body, because it is rendering the children itself.

Author with it

{% callout type="warning" %}
Back up your content directory before upgrading.
{% /callout %}

A self-closing component takes no body. The shipped edition badge is one:

{% edition edition="pro" /%}

The shipped button is the worked example of a label body. Its manifest declares both keys:

name: button
description: A call-to-action link styled as a button.
params:
  - name: variant
    type: enum
    values: [default, primary, secondary]
    required: false
  - name: href
    type: reference
    required: true
body: inline
inline: true

and its template keeps the label on the line, inside the link:

{%- set variant = attrs.variant | default("default") -%}
<a href="{{ url(attrs.href) }}" class="btn btn-{{ variant }}">{{ body }}</a>

Passing href through url() is what keeps the link correct on a sub-path deployment, and keeps the slash literal rather than HTML-escaped.

It is written on one line, in a sentence or on a line of its own:

Ready? {% button variant="primary" href="/download" %}Download{% /button %}

Values you interpolate are escaped, so an attribute containing <script> reaches the page as text.

A component body renders as markdown. The body is spliced into the page as markdown source at the spot where the template interpolates {{ body }}, and the manifest’s body value decides how. A body: markdown body arrives between blank lines, so a template can put it straight inside a <div> and it renders: paragraphs, lists, headings, fenced code, and nested components all work, through the same pipeline as the rest of the page. A body: inline body arrives flush, which is what a label inside an <a> or <span> needs. Two things to keep in the template: write {{ body }} and the tag that closes after it in column 0, because an indented line after a blank line is an indented code block; and declare body: inline for a label, because blank lines inside an inline element turn the label into a paragraph. accent validate --templates reports both mistakes.

Variables

A component attribute can reference a variable with $:

{% callout type="info" %}
Welcome to {% $site.name %}.
{% /callout %}

Four namespaces are available:

VariableWhat it holds
$sitename, description, language, url, base_path
$frontmatterthe page’s own frontmatter, including your custom fields
$templatethe template rendering the page
$editionthe licensed edition, when the site supplies one

Nothing else is available, deliberately. The clock, environment variables, data files, remote URLs, and anything that varies per request are all unavailable, because Accent’s page cache cannot tell when they change. A page built from one of them would serve stale output until an unrelated edit happened to clear the cache. The three sources above are exactly the three the cache knows how to invalidate.

Conditionals

{% if $frontmatter.draft %}
This page is a draft.
{% else /%}
This page is published.
{% /if %}

Conditions are evaluated by Accent’s own expression engine, not by the template language. Content stays declarative: it can select between branches, but it cannot run arbitrary template code.

Partials

A partial splices another markdown file into the page:

{% partial file="shared/upgrade-notice.md" /%}

The name is resolved against your content directory first, then your theme’s partials/ directory. A partial may include another; a cycle is reported rather than followed.

Partials are tracked. Editing a partial re-renders every page that includes it – in the dev server and in an incremental accent build alike – because Accent records which files each page was built from. Deleting a partial re-renders its former includers too.

Canonical components

Components are theme-scoped, so a page that uses a component only one theme declares breaks when the site switches to another theme. The canonical components are the part of the vocabulary you can rely on: every conforming theme implements each of them with the same attribute names, so content that uses only these renders under any conforming theme.

ComponentAttributesWhat it is
callouttype, title, collapsiblea titled box for a note, a warning, or another notice
accordionsummary, opena section collapsed behind a summary line
buttonhref, varianta link styled as a button
cardtitle, href, imagea block with an optional image, title, and link
tablenonethe built-in table; the theme styles it
taghrefa small inline label
imagesrc, alt, captionan image with alt text and an optional caption
breadcrumbnonea trail of links written in Markdown
tabs / tabgroup / label, idtabbed panels

Names are fixed and values are a floor. A conforming theme declares every attribute above, and where an attribute has fixed values, the theme accepts at least those: callout takes info, success, warning, and danger, and button takes default, primary, and secondary. A theme can add attributes, values, and components of its own; content that uses them depends on that theme.

Markup landmarks. Each theme writes its own markup and classes, but every conforming theme marks the same landmarks, so a script or a stylesheet that finds a callout’s title or a tab’s panel works under any conforming theme. The component’s root element carries data-canonical with the component’s name, and each part carries data-part:

ComponentParts
callouttitle
accordionsummary; the root is a <details> element
tabstablist, and a tab and a panel for each tab
cardtitle and image, when those attributes are set
imageimage, and caption when it is set
button, tag, breadcrumbthe root only; a button is an <a> or a <button>, and a breadcrumb is a <nav>
<div class="callout callout-warning" role="complementary" aria-label="Heads up" data-canonical="callout"><p class="callout-title" data-part="title">Heads up</p>
...
</div>

A theme you write for the canonical components carries the same attributes.

The set was chosen by measurement. A component is canonical when at least three of four CSS frameworks – Bulma, Pico CSS, Foundation, and USWDS – style it and content writes it, rather than page chrome such as a navigation bar. tabs is the exception: two of the four have it, and it is canonical because the documentation Accent ships uses tabs throughout.

accent validate checks the active theme against the set, with or without a page that uses a missing component, and reports each gap as an error. When a page uses a component the theme does not declare, the finding names the theme and says whether the component is canonical:

error  /docs/example
       line 8: theme `minimal` does not implement `card`, one of the canonical
       components every theme is expected to provide; add it to the theme, or
       use a theme that implements the canonical components
       [accent::component-not-in-theme]

A theme that provides no components, such as a marketing site’s theme, declares that in theme.yaml, and accent validate does not check it:

components: none

For a site part way through a theme change, accent validate --lax reports a theme’s gaps, and pages using a canonical component the theme lacks, as warnings that do not fail the run. A name that no theme, builtin, or plugin declares stays an error under --lax, because it may be a misspelling.

Builtin components

Four components need something a theme template is never given, so Accent renders them itself. Every theme has them, and neither a theme nor a plugin can redefine them.

ComponentWhat it rendersAttributes
downloada file beside the page, as a card with its name, type, and sizesrc (required), title, show_size, class
videoa video beside the page, with its captionssrc (required), captions, poster, width, controls, autoplay, loop, muted
diagrama diagram, the way a fenced diagram block renders it, with a caption and alt texttype (required), src, theme, width, height, caption, alt, class
searchthe site’s search box, for the active search backendplaceholder, limit
{% download src="report.pdf" /%}

{% video src="talk.mp4" captions="talk.vtt" poster="talk.jpg" /%}

{% diagram type="mermaid" caption="Publishing flow" alt="A draft goes to review, then to publishing" %}
flowchart LR
  A[Draft] --> B{Review} --> C[Publish]
{% /diagram %}

{% search placeholder="Search the docs" limit=5 /%}

download and video serve files the page owns: put the file in the page’s directory and name it. Both refuse a URL, because Accent can neither read a remote file’s size and type nor serve it. For a PDF, download renders the same card as the pdf_card() template function.

The file must also be a type Accent serves. Images, video, audio, PDFs, text and data files such as .csv and .json, and .vtt captions are served as they are. Any other type, such as .zip, needs its extension in media.attachments.extensions, which serves it as a download:

media:
  attachments:
    extensions: [zip]

diagram takes its source from the tag’s body, or from a file beside the page named by src, not both. It renders through the same pipeline, cache, and diagrams.on_error policy as a fenced block. search renders what search_form() renders, and nothing while search is turned off.

Captions are part of video. Name a WebVTT (.vtt) caption file with captions, or write captions="none" when the video has no speech or no captions exist. accent validate reports a video with neither, and accent validate --lax relaxes that finding. A SubRip (.srt) file is refused with a message to convert it, because browsers load WebVTT captions only.

accent validate also reports a src that names a file the page does not have, listing the files it does have, and a builtin given a URL. Both stay errors under --lax.

Where a component can come from

Every declared component records where it was declared, and three sources are possible: Accent’s own built-in tags, your theme, and an installed plugin. When two sources declare the same name, the higher-precedence one wins:

built-in  >  theme  >  plugin

A plugin cannot redefine a tag your theme declares, and neither can redefine a built-in. This matters because a schema does not control one tag’s markup – it controls what counts as valid content across your whole site, so installing a plugin must not silently change what your existing pages mean. When a declaration is shadowed, Accent logs which source won and which lost.

Two declarations at the same level are an error naming both files, never a silent overwrite.

{% for %} is not content

Components share {% %} with the theme’s template language, and that is the one thing worth knowing before you start. Template control flow belongs in the theme. A page cannot loop, include, or extend, and writing

{% for item in $items %}

in a page is reported as its own kind of problem rather than as a misspelled component:

error  /docs/example
       line 12: `{% for %}` is template syntax, not content. Template control
       flow belongs in the theme's `.html.jinja` files, which content cannot
       reach. If you meant a component, declare one in the theme's
       `shortcodes/` directory and use its name here.

The reserved words are for, endfor, include, extends, block, set, macro, call, filter, raw, with, and import. A component whose name merely starts with one – setting, for-each – is unaffected, and a reserved word inside a code fence or backticks is prose, not a tag, so pages that document template syntax are fine.

What gets reported

accent validate reports every component problem, so a broken tag fails CI instead of reaching a reader. The same findings appear in the admin panel and in dev-server logs.

ProblemWhat you see
A component the theme does not declarethe tag is left in the page, and the error names the theme and whether the component is canonical
A theme missing a canonical componentreported against the theme, whether or not a page uses the component
A builtin given a URL, a file the page lacks, a file type Accent does not serve, or .srt captionsthe tag is left in the page, and the error names the file
A video with no captionsreported; --lax relaxes it
Template syntax in contentthe directed message above, under its own check
Undeclared attributereported with the attribute name
Wrong type, or a value outside an enumreported with what was expected
Missing required attributereported with the attribute name
Manifest that will not parsereported with the file path; other components still load
Manifest with no templatereported at load, not on the first page that uses it

Migrate your existing shortcodes

accent migrate shortcodes rewrites [shortcode] content into tag syntax. It reports what would change and writes nothing until you pass --write:

accent migrate shortcodes --config config.yaml
accent migrate shortcodes --config config.yaml --write

It rewrites a shortcode only when it can verify the result, and leaves everything else exactly as you wrote it, with the file, the line, and the reason. Shortcodes inside fenced code blocks are never touched, and neither is frontmatter.

Tell it what your positional arguments meant. Shortcodes take bare words; tags take named attributes. Add a migrate block to the manifest so the command does not have to guess:

name: callout
params:
  - name: type
    type: enum
    values: [info, success, warning, danger]
body: markdown
migrate:
  positional: [type]   # [callout warning] -> {% callout type="warning" %}

Without it, [callout warning] is reported rather than rewritten.

Tell it what a renamed component used to be called. When a theme renames a component, list the old name under migrate.from, with any attribute it renamed. The default theme renamed alert to callout and details to accordion, so its callout manifest says:

migrate:
  positional: [type]
  from:
    - name: alert
      rename:
        variant: type   # [alert variant="warning"] -> {% callout type="warning" %}

A component declared under the shortcode’s own name still wins, so a theme that keeps an alert component migrates [alert] to it.

Useful flags:

FlagWhat it does
--writeapply the rewrite; without it nothing is written
--only NAMEmigrate only this component; repeatable
--include-frozenalso rewrite pages in frozen documentation versions

Pages in a frozen documentation version are reported but not rewritten, because a frozen version is errata-only and its content hashes are recorded. Passing --include-frozen takes them, and records the new hashes as it goes, so accent validate stays clean. The report names every page it rebaselined – those are released documents that changed after release, and you should be able to see which.

Check the result with a build diff. Build before and after, and compare:

accent build --output /tmp/before --clean
accent migrate shortcodes --write
accent build --output /tmp/after --clean
diff -rq /tmp/before /tmp/after

Rendered pages should be identical. The word count in each page’s structured data, and llms-full.txt, read your markdown source rather than the rendered page, so those do change – everything else changing is worth investigating.