Attributes

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 and column alignment, 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. Three uses account for almost all of them: a class your stylesheet targets, target="_blank" on a link, and colspan or rowspan on a table cell. An id is not one of them: Leed Markdown has never let an author set an element’s id. A {#pricing} or {id="pricing"}, in any letter case, is read as attribute syntax and then dropped from every element, in a CMS page body and in markdown rendered straight from a template alike, and validation reports it as dropped content. The ids the renderer makes itself are unaffected: heading slugs, tab and panel ids, and checkbox ids.

## Pricing {.lead data-value="test"}

That renders <h2 id="pricing" class="lead" data-value="test">Pricing</h2> when the markdown is rendered straight from a template; the id is the heading’s generated slug, never one you write. In a CMS page body even the class goes: a heading or paragraph keeps only its data-id, so the class is discarded when the body is stored. To link to a heading, link to its generated id — see What survives a save below.

SyntaxMeaning
.classnameclass="classname"
#idnot supported: read as attribute syntax, then dropped
key="value"any attribute other than id
{.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.

Where shorthand works

This is the section to read before you write an attribute anywhere. The editor and the site read every {…} group with one grammar, but not every element accepts all of it: the attribute line after a table, a list or a blockquote drops .class shorthand, and it fails silently — the braces still vanish from the rendered page, so the source looks like it worked.

Inline and same-line block attributes

The general grammar accepts .class, #id (read as attribute syntax, but the id is then dropped) and key=value, with the value double-quoted, single-quoted or bare, and any number of them in one pair of braces. Headings, paragraphs, links, images, inline code, table cells, code fences, mermaid fences, math fences, the opening line of an alert or a collapsible, and a tab label all take it, .class included.

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 (a list’s may also end any item’s line), and that line keeps only key=value entries, with the value double-quoted, single-quoted or bare. .class shorthand is dropped.

Write the long form instead:

| Plan | Price |
| :--- | ----: |
| Free | 0     |

{class="wide"}

A tab container’s opening line is read the same way: ===tabs-container {data-tabs-groupId="operating-system"} works, ===tabs-container {.os} sets nothing. A tab label is the exception — it takes the general grammar, so @tab python {.lang-tab} and @tab python {class="lang-tab"} both put the class on the label.

Collapsibles

A collapsible’s opening line takes the general grammar, .class included: +++ Advanced {.wide} renders class="wide" on the <details>. An attribute line written directly after the closing marker, as older page bodies wrote it, is read too, but like a table’s line it keeps only key=value entries. One consequence bites in practice:

  • A page body keeps only data-id. A class on a collapsible renders when the markdown comes from a template, but a CMS page body does not store it.

A quoted value may contain spaces, so {title="two words"} sets the whole phrase. 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, images and inline code — immediately after the closing parenthesis or backtick, with no space at all.
  • Code, math and mermaid fences — on the opening fence line, after the language.
  • Tables and blockquotes — on a line of their own directly after the block. A table’s goes after a blank line, which is how the editor writes it; a quote’s goes directly under its last line.
  • Lists — at the end of any item’s line, after a space, or on a line of their own directly after the list.
  • 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

ElementWrite it asLands onShorthand accepted?Survives a save?
Heading## Text {…}<h2>Yesdata-id only
ParagraphText {…}<p>Yesdata-id only
Link[text](url){…}<a>YesThe modeled attributes only
Image![alt](url){…}<img>Yesdata-id, data-assetid, data-responsiver
Inline code`code`{…}Nothing — the site renders a bare <code>Yesclass and data-id
Code fenceon the fence info line<pre>Yesdata-id only
Mermaid fenceon the fence info line<pre class="mermaid">Yesdata-id only
Math fenceon the fence info line<pre>Yesdata-id only
Liston any item, or the line after the list<ul> or <ol>Nodata-id only
Tableline after the table<table>Nodata-id only
Table cellinside the cell<td> or <th>Yescolspan, rowspan and class
Blockquoteline after the quote<blockquote>Nodata-id only
Alert:::type Title {…}<aside>Yesdata-id only
Collapsible+++ Title {…}<details>Yesdata-id only
Tabs container===tabs-container {…}the container <div>Nodata-id and data-tabs-groupId
Tab label@tab name {…}the label <span>Yesclass

No row takes an id: on every element it is dropped, and validation reports it.

What survives a save

A page body is stored as blocks, not as text, and is re-serialized whenever it is read back or published. That 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. Markdown written over MCP or imported is parsed on the way in, so the loss happens at the write, not at a later save. create_page_from_markdown, set_page_body and update_page_body say so: each returns a warning when it stripped a trailing attribute group off a heading or paragraph, or an id off any element, and validation reports the loss as dropped content, though the content is still applied.

What it re-emits is data-id, data-assetid, and the attributes that belong to a modeled node — a link’s target, rel, class, role, download, title and ARIA attributes; a code span’s class; a table cell’s colspan, rowspan and class; a tab group’s data-tabs-groupId; a tab label’s class; an image’s data-responsiver. Nothing else, and never an id.

AuthoredAfter a save
## Head {.cls #hid data-id="h1"}## Head {data-id="h1"}
Paragraph {.lead data-id="p1"}Paragraph {data-id="p1"}
`code`{.x #c data-id="ic"}`code`{.x data-id="ic"}
@tab Python {class="lang-tab"}@tab Python {class="lang-tab"}
A table cell Total {.sum style="color:red" colspan="2"}Total {colspan="2" class="sum"}
1. Item {start="14"}14. Item

That last row is how a list’s start number is stored: the editor writes it natively, as the first item’s number, so a numbered list that starts at 14 in the editor publishes starting at 14. A {start="14"} group on the first item is still read. See Lists and Task Lists.

Only a recognized attribute block is stripped from a heading or paragraph: every whitespace-separated entry in the braces has to be .class, #id or key=value. A trailing brace group that is not attribute syntax is content, so ## Set the value {timeout} keeps its braces and reads back escaped as ## Set the value \{timeout\}, rendering as {timeout}.

Arbitrary embed parameters, and any table-cell key other than colspan, rowspan and a class, are lost the same way; table column alignment and cell spans are kept. The complete list, with what each loss 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_body, sees each block’s data-id, and anchors its operations to those ids — insert before this block, replace that one, delete the third. Ids are stamped as soon as a body is written, so a page created over MCP or by an import can be edited this way before anyone opens it in the editor. 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.

ESC