In the CMS you set these through block dialogs rather than by typing them — the link dialog writes target and rel, the table menu writes cell spans, the image dialog writes alt text and the asset id. This page is the underlying format, which is what you need when you are writing Leed Markdown by hand, generating it from a script, or reading a page back through the API.
What attributes are for
Curly braces after an element attach HTML attributes to it. Four uses account for almost all of them: an id you can link to, a class your stylesheet targets, target="_blank" on a link, and colspan or rowspan on a table cell.
## Pricing {#pricing .lead data-value="test"}That renders <h2 id="pricing" class="lead" data-value="test">Pricing</h2>.
| Syntax | Meaning |
|---|---|
.classname | class="classname" |
#id | id="id" |
key="value" | any attribute |
{.a .b .c} | three classes on one element |
A class only does something if a stylesheet defines it — see How Styling Works for where your site’s CSS comes from and how to add your own.
The three parsers
This is the section to read before you write an attribute anywhere, because Leed does not have one attribute parser. It has three, they accept different syntax, and the two stricter ones fail silently: the braces still vanish from the rendered page, so the source looks like it worked.
Inline and same-line block attributes
The general parser is @mdit/plugin-attrs, and it is the permissive one. It accepts the full set — .class, #id, key="value", and any number of them in one pair of braces. It handles headings, paragraphs, links, images, table cells, code fences, mermaid fences, math fences and the opening line of an alert.
If you are working with one of those, everything in the shorthand table above applies and there is nothing else to learn.
Attribute lines
Tables, lists and blockquotes are different. Their attributes go on a line of their own, and that line is read by parseAttributeLine, whose regular expression matches only key="value" with double quotes.
Write the long form instead:
| Plan | Price |
| :--- | ----: |
| Free | 0 |
{id="pricing" class="wide"}The same rule governs a tab container and a tab label, which are read by their own regular expression using the identical key="value" pattern: ===tabs-container {data-tabs-groupId="operating-system"} works, ===tabs-container {#os} does not.
Collapsibles
A collapsible block has a third parser again. It splits the brace contents on whitespace, keeps only the fragments containing an =, and strips quotes from the value. Two consequences follow, and both bite in practice:
- No shorthand.
{#advanced}contains no=, so it is discarded entirely. - No spaces in a value.
{title="two words"}splits at the space and yieldstitle="two", withwords"thrown away.
Keep collapsible attributes to single-word values, and see Collapsible Sections for what a collapsible accepts.
Where the braces go
Placement is per element, and getting it wrong usually means the braces render as literal text.
- Headings and paragraphs — at the end of the line, after a space.
- Links and images — immediately after the closing parenthesis, with no space at all.
- Code,
mathandmermaidfences — on the opening fence line, after the language. - Tables, lists and blockquotes — on a line of their own directly after the block, with no blank line between.
- Alerts, collapsibles and tab containers — on the opening line, after the title.
- Table cells — inside the cell, after the content.
Here is the link form, live: an external link— that one opens in a new tab because of the braces, not because of anything the theme does.
Where attributes land
| Element | Write it as | Lands on | Shorthand accepted? | Survives a save? |
|---|---|---|---|---|
| Heading | ## Text {…} | <h2> | Yes | data-id only |
| Paragraph | Text {…} | <p> | Yes | data-id only |
| Link | [text](url){…} | <a> | Yes | The modeled attributes only |
| Image | {…} | <img> | Yes | data-id, data-assetid, data-responsiver |
| Inline code | — | — | Unsupported | — |
| Code fence | on the fence info line | <pre> | Yes | data-id only |
| Mermaid fence | on the fence info line | <pre class="mermaid"> | Yes | data-id only |
| Math fence | on the fence info line | <pre> | Yes | data-id only |
| List | on the first item | <ul> or <ol> | No | data-id only |
| Table | line after the table | <table> | No | data-id only |
| Table cell | inside the cell | <td> or <th> | Yes | No |
| Blockquote | line after the quote | <blockquote> | No | data-id only |
| Alert | :::type Title {…} | <aside> | Yes | data-id only |
| Collapsible | +++ Title {…} | <details> | No | data-id only |
| Tabs container | ===tabs-container {…} | the container <div> | No | data-id and data-tabs-groupId |
| Tab label | @tab name {…} | the label <span> | No | No |
What survives a save
A page authored in markdown and then opened in the editor is parsed into blocks, edited, and re-serialized. That round trip is lossy for attributes, because the editor can only re-emit what its schema models. Everything else is not stored anywhere and has nowhere to come from.
What it re-emits is data-id, data-assetid, and the attributes that belong to a modeled node — a link’s target, rel, class, id, role, download, title and ARIA attributes; a tab group’s data-tabs-groupId; an image’s data-responsiver. Nothing else.
| Authored | After a save |
|---|---|
## Head {.cls #hid data-id="h1"} | ## Head {data-id="h1"} |
Paragraph {.lead data-id="p1"} | Paragraph {data-id="p1"} |
`code`{data-id="ic"} | `code` |
@tab Python {class="lang-tab"} | @tab Python |
1. Item {start="14"} | 1. Item {start=14 …} — unquoted, and then dropped by the site renderer |
That last row is a genuine defect rather than a modeling gap: the editor writes the start value without quotes, and the attribute-line parser requires quotes, so a numbered list that starts at 14 in the editor publishes starting at 1. It is described in full on Lists and Task Lists.
Table column alignment, cell spans, tab-label attributes and arbitrary embed parameters are lost the same way. The complete list, with what each one costs, is on Fidelity and Unsupported Syntax.
The one attribute you never write: data-id
data-id is different from everything else on this page: it is assigned, not authored. Every top-level block in a CMS page carries one, added by the schema itself, and it is stable across edits.
It exists because it is the addressing scheme MCP edits use. An AI client reads a page with get_page_markdown, sees each block’s data-id, and anchors its operations to those ids — insert before this block, replace that one, delete the third. That contract is the subject of Markdown for AI, MCP and Import.
Braces in ordinary text
A literal { or } in body text is not safe from the round trip. The editor’s serializer escapes {, }, : and @ with a backslash on the way out, so a paragraph you wrote as Set {timeout} to 30 comes back as Set \{timeout\} to 30. The escaping is what stops your prose from being mistaken for an attribute block, and the rendered page is unaffected — but the stored markdown looks like someone has been at it.
This is the single most confusing artifact in a round-tripped file, and it is worth recognizing rather than trying to clean up: removing the backslashes just means the next save puts them back. If you are writing Handlebars into a page body rather than into a template, the same escaping applies, and Template Formatting Reference is the place that covers doing it properly.