Writing your own shortcuts
The system shortcuts cover the built-in styling capabilities.
On top of them you define your own vocabulary in shortcuts.yaml — names
that mean something in your book. Instead of repeating
::: {.font family="eb-garamond"} throughout the manuscript, define garamond
once and write ::: garamond. When the design changes, you edit one place.
This is where you unleash your inner developer. A shortcut is a styling macro:
you compose new constructs from the ones that already exist — chain them, inject
body content, even gate a shortcut's body with ifdef — instead of asking for
new engine features. Complexity lives in shortcuts.yaml; your prose stays
clean. Keystone won't run arbitrary code, but composing shortcuts is about as
close as it gets to scripting your Markdown.
Where shortcuts live
User shortcuts go in shortcuts.yaml at the project root. They layer on top of
the system shortcuts: defining a name that already exists shadows the system
version; deleting your entry restores it. The file is optional — without it, the
system shortcuts are all you have, which is plenty to start.
Anatomy
A shortcut maps a name to a handler (or to another shortcut), optionally exposing attributes and injecting content. Four keys are allowed:
garamond:
class: font # what this routes to (required)
interface: # attributes the author may set
family:
bind: class.family
default: eb-garamond
class— the target: a handler, a system shortcut, or another user shortcut. Required.interface— the attributes your shortcut accepts and where each routes.body— Markdown injected into the element (see Body injection).content— names the body slot that receives author content.
Any other top-level key is an error.
Use it like any shortcut:
::: garamond
This paragraph renders in EB Garamond.
:::
A word in [EB Garamond]{.garamond} inline.
The interface
Each interface entry binds a name the author writes to a target the value lands
on. The target is always where.attribute:
class.Xroutes to the outer handler — whateverclassresolves to.classis a symlink to the shortcut's own class.name.Xroutes to a div namednameinside the shortcut'sbody.
Every interface attribute is optional. A default sets the value used when the
author omits it; without one, the attribute is applied only when the author
supplies it — and the underlying handler falls back to whatever default it
defines:
garamond:
class: font
interface:
family:
bind: class.family
default: eb-garamond
size:
bind: class.size # no default — applied only if the author sets it
The author overrides any interface attribute inline:
::: {.garamond family="libertine"}
This renders in Libertine instead of EB Garamond.
:::
An inline attribute that doesn't match an interface name produces a build warning — typo protection.
Decoupling names
Because the interface renames, two handlers that use the same internal attribute can be exposed under distinct author-facing names — no collision:
italic-center:
class: align
interface:
alignment:
bind: class.style # the align handler's "style"
default: center
font-style:
bind: font.style # the font div's "style"
default: italic
body: |
::: font
:::
The author writes alignment and font-style; neither is the literal style
that both handlers use internally.
Choosing a terminal class
A chain ultimately has to resolve to a handler. Two cover most needs:
container— pure grouping, no styling. Use it when the shortcut exists only to inject a body or nest other shortcuts.font— when the shortcut applies a typeface or size.
The other system shortcuts (align, aside, figure, quote, …) are
available as terminal classes when you need their specific behavior.
Chains
A shortcut's class can point at another shortcut, forming a chain. A chain
bind (class.X) requires the parent to expose X:
small-garamond:
class: garamond # builds on the garamond above
interface:
size:
bind: class.size
default: small
small-garamond binds size through garamond, which exposes size (bound to
font's size). If garamond didn't expose it, this would error — the attribute
is private to garamond.
Chains can be any depth and merge first-writer-wins: when the same interface name appears at two levels, the outermost entry wins.
Body injection
A shortcut can carry a body — Markdown parsed and injected when the element is
empty:
ornament:
class: align
interface:
style:
bind: class.style
default: center
body: |
~ ~ ~
::: ornament
:::
The empty div becomes a centered ~ ~ ~. If the author does supply content,
it wins — the body wraps it by dropping the content into the body's first empty
div.
Naming the content slot
When the body has several divs and the first empty one isn't where author
content should go, name the slot with content:
pullquote:
class: align
content: font
body: |
::: rule
:::
::: font
:::
::: rule
:::
Author content now always lands in the font div; the empty rule divs are
left alone. Pointing content at a font div can look odd, but the font div is
where the visible text belongs — the rule divs are just decoration.
When fixed elements must surround the author content inside one parent, use the
slot placeholder — it receives the content while its siblings stay put:
epigraph:
class: font
content: slot
body: |
::: quote
::: slot
:::
::: source
:::
:::
Here author content flows into slot inside quote, while source remains a
sibling sharing the quote's indented width.
Composition across formats
PDF and EPUB nest freely — each level becomes a nested LaTeX environment or a nested HTML element with its classes — so a composition renders faithfully and the order you stack handlers in is mostly cosmetic. These are Keystone's publishable formats, and this is where composition fidelity matters.
DOCX and ODT work differently: a paragraph carries a single style, so in a
stacked composition only the style closest to the content survives — deeper
layers flatten away. That's fine. DOCX and ODT aren't publication targets;
they're collaboration vehicles for handing a draft to an editor, so good-enough
styling is the bar, not pixel fidelity. If a particular look there does matter to
you, put the style that must survive closest to the content (it's why
pullquote slots into its font div) — but usually you needn't think about it.
An interface entry whose bind target isn't class forwards onto the matching
body div. This is how one shortcut drives several handlers:
poem-date:
class: align
interface:
style:
bind: class.style
default: right
body: |
::: {.font style="italic"}
:::
::: poem-date
Published 1850.
:::
style routes right to the outer align; the font div carries its own
style="italic" baked in. A body attribute with no interface entry pointing at
it is private — the author can't change it. Add an interface entry (without a
default) to make it overridable while keeping the body value as the built-in.
Nesting
Body content may itself use shortcuts — both divs and spans — and they expand recursively, so composition nests naturally:
poem-footer:
class: poem-date
body: |
::: small-garamond
2026
:::
A depth limit (10 levels) guards against shortcuts that reference each other in a loop.
Cross-references
Most binds set key-value attributes. The identifier target is special — it
sets Pandoc's native id, what #id and id= populate. A single-handler
shortcut needs nothing here: an #id you write rides through to the element on
its own. You bind identifier only when a composed shortcut must carry the id
onto an inner element — here, wrapping a figure in centering and forwarding the
id inward:
centered-figure:
class: align
interface:
align:
bind: class.style
default: center
identifier:
bind: figure.identifier
width:
bind: figure.width
body: |
::: figure
:::
::: {.centered-figure #fig-map width="50%"}

:::
#fig-map lands on the inner figure, where it becomes the cross-reference
target. (Keystone ships this exact composition as aligned-figure, so you
rarely write it yourself — it's shown here only to illustrate identifier
routing.)
What gets rejected
Shortcuts are validated when the build loads them. Common errors:
- a missing
class - a name starting with
ks-(reserved for handlers) - an unknown top-level key (only
class,interface,body,content) - a bind that isn't
class.attributeorbodydiv.attributeform - a non-
classbind with no body div of that name - a chain bind to an attribute the parent doesn't expose
contentwithout a body, or naming a slot the body doesn't contain- two shortcuts that reference each other in a cycle
The build halts with a message pointing at the offending entry, so mistakes surface immediately rather than producing silently wrong output.
What you can build
A few patterns that fall out of these mechanics:
- Named typefaces —
garamond,chapter-face,caption-face: afontshortcut with the family pinned, so the choice lives in one place. - Decorative breaks —
ornament,flourish: analignshortcut with the glyphs in its body. - Recurring attributions —
byline,poem-date: composealignoverfontso a single name carries both placement and style. - Running-header marks — a shortcut over
set(e.g.poem-title) so you set a mark by meaning, not by repeatingmark="…". See Running headers & footers. - Edition gates — a shortcut over
ifdef(e.g.draft-note) so a recurring symbol reads as intent. See Conditional content.
The guideline: when you find yourself repeating the same attributes, that's a shortcut waiting to be named.