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.
A shortcut is a styling macro: you compose new constructs from the ones that already exist — chain them, give them a
template, even gate a template's content with ifdef — instead of asking for new engine features. Complexity lives in
shortcuts.yaml; your prose stays clean. Keystone won't run arbitrary code — shortcuts compose existing constructs,
nothing more.
Where shortcuts live
Your shortcuts go in shortcuts.yaml at the project root, layered over the system shortcuts. Defining a system
shortcut's name shadows it (see Shadowing a system shortcut); deleting your entry
restores it. The file is optional — missing, empty or only comments, the system shortcuts are all you have, which is
plenty to start.
The build checks the whole file before it reads a chapter, and stops on anything it can't take, naming the key that caused it — see What gets rejected.
Anatomy
A shortcut names another shortcut as its class, and may add an interface and templates. These keys are allowed:
garamond:
class: font # the shortcut this builds on (required)
interface: # attributes the author may set
family:
bind: class.family
default: eb-garamond
class— the shortcut this one builds on: a system shortcut or another of yours. Required on a new shortcut.interface— the attributes your shortcut accepts and where each routes.body— the template for a div use (see Templates).span— the template for a span use: one paragraph of inline Markdown.content— names the template element that receives author content.classes— native classes the element also takes (see Headings).
Any other top-level key stops the build.
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 element the shortcut becomes — through the shortcut its class names, which must exposeX.key.Xroutes to every element of the shortcut's templates whose first class iskey— a template you wrote, so the shortcut needs one.
A default sets the value used when the author omits the attribute; without one, the attribute applies only when the
author writes it, and the handler underneath falls back to its own default. A value reaches the attribute as text: 3
arrives as "3", true as "true".
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 no interface entry takes is ignored with a warning — typo protection.
Required attributes
Mark an attribute required: true when the shortcut is meaningless without it and there's nothing sensible to default
to. If the author omits it, the build stops naming the shortcut and the field:
spacer:
class: vspace
interface:
size:
bind: class.size
required: true # no default — the author must choose a size
A default already supplies the value, so required: true beside it never fires; one definition setting both warns.
Some handlers require an attribute of their own — vspace needs a size, set needs a mark. A shortcut over one
must guarantee that attribute reaches it: an entry routing to it with a default, a const or required: true. The
system shortcuts already do, and your shortcut inherits their entries; the build stops when nothing in a chain or
template guarantees one.
Fixed values
Some values are what a shortcut means. print-only keeps content for PDF, so its symbol is always latex. A const
fixes the value:
print-only:
class: ifdef
interface:
symbol:
bind: class.symbol
const: latex
A const reaches the attribute as a default does, and nothing sets it: an author writing the attribute is
refused, and so is a template handing the shortcut a value for it, or a chain or shadow
writing any field of the entry. It stands alone in its entry — beside default or required it would say two things. A
shortcut may fix an entry it inherits: print-only fixes the symbol that ifdef requires, as the system's
latex-only does.
Accepted values
A system shortcut's attributes accept set values — a vspace size, an aside type, a family your fonts provide — which
the reference lists. An entry routing to one inherits what it accepts, through chains, shadows and
templates. A value is judged where it's written. A default, a const or a value a template writes is judged when the
build compiles your shortcuts, and one its entry doesn't accept stops the build, naming the key:
ERROR: shortcuts.yaml has defaults and consts their entries don't accept
garamond.interface.family.default: eb-garmond
Their entries take a font the registry holds.
Valid: biolinum, dejavu-mono, dejavu-sans, dejavu-serif, eb-garamond,
fourier-ornaments, imfell-flowers-1, imfell-flowers-2, latin-modern,
latin-modern-math, latin-modern-mono, libertine, noto-mono,
source-code-pro, tex-gyre-adventor, tex-gyre-bonum, tex-gyre-cursor,
tex-gyre-heros, tex-gyre-pagella, tex-gyre-schola, tex-gyre-termes
Name a bundled font, or one fonts/fonts-registry.yaml declares.
See https://keystone.knight-owl.dev/shortcuts/writing-shortcuts/#an-accepted-value
A value written at a use is judged as the chapter builds: one its entry doesn't accept is
refused, and the entry's default stands in where it has one.
An entry narrows what it inherits with accepts, a list of values or a rule:
emphasis:
class: font
interface:
style:
bind: class.style
accepts: [italic, bold-italic]
| Rule | Accepts | Options |
|---|---|---|
integer |
an integer in decimal digits | min, max |
css-length |
a CSS length | keywords: words it takes besides; percent: true adds a whole percentage to 100 |
hex-color |
a #rrggbb color |
— |
nonblank |
text holding something besides whitespace | — |
symbols |
one or more build symbols, separated by spaces | — |
A rule alone is its name, accepts: hex-color; with options, a map: accepts: { integer: { min: 2, max: 3 } }.
What you accept must lie within what the entry inherits: accepts: [italic, slanted] on a font style stops the build,
since font doesn't take slanted. A shadow's narrowing reaches every shortcut built on the one you shadow, through
its class or its template. Narrow font's style and pullquote takes only what you listed; a system default
outside it stops the build, naming your narrowing. It reaches no other: epigraph and sym set a face as font does,
without being built on it, so narrow each one you mean. A system shortcut's accepts names its handler's domain, as
ks-font@style; that form is the system's alone, and yours lists values or names a rule.
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 shortcut's "style"
default: center
font-style:
bind: font.style # the font element's "style"
default: italic
body: |
::: font
:::
The author writes alignment and font-style; neither is the literal style that both handlers use internally. Two
entries routed to the same attribute of the same target stop the build: which value landed would be left to chance.
Choosing a terminal class
A chain ends at a handler, through the system shortcut that fronts it. Two cover most needs:
container— pure grouping, no styling. Use it when the shortcut exists only for its template, to nest other shortcuts, or to name a heading.font— when the shortcut applies a typeface or size.
The other system shortcuts (align, aside, figure, quote, …) serve as classes when you need their behavior.
Chains
A shortcut whose class is another shortcut starts from it: its interface, templates, content and classes come along,
and it changes only what it writes.
small-garamond:
class: garamond # builds on the garamond above
interface:
size:
default: small
An entry under a name the parent already has replaces it, keeping whatever it leaves out — bind included — so
small-garamond writes only the new default. The rest of the parent's interface comes along untouched: small-garamond
still takes family.
An entry under a new name bound to class.X takes over the parent's entry X: it renames it, and X leaves the
interface. italic-center above renames align's style to alignment, so style isn't an attribute of
italic-center. A class.X the parent doesn't expose stops the build — the attribute is private to the parent.
A chain binds into a template only when the template is its own; a template it inherits is reached through the entries its parent exposes.
Shadowing a system shortcut
Defining a system shortcut's name changes that shortcut for your book, everywhere it appears — inside other system
shortcuts' templates too, so a shadow of figure changes aligned-figure. Your definition layers over the system's key
by key, down to a single entry field:
pullquote:
interface:
font-size:
default: huge # every pullquote in the book
A shadow may change defaults, templates and the routes into them, and add entries routed into its templates. It keeps
the system's class — writing one stops the build — since the entries it inherits bind through it, and it renders every
form the system shortcut does, so a template that would take one away stops the build. It keeps every entry the system
shortcut exposes and the attribute each routes to, so a take-over under another name is refused; rename in a shortcut of
your own, with the system one as its class. The handler's other attributes stay private. A value the system fixes takes
no field from a shadow, and a shadow may fix a system default with const for its book.
A layer only overrides: a null anywhere — default: ~, body: ~ — stops the build. To reach the state a default
excludes, write the value that names it, such as font's and align's normal.
Templates
A shortcut can carry a template: body for a div use, span for a span use. An empty use takes the template as its
content:
ornament:
class: align
interface:
style:
bind: class.style
default: center
body: |
~ ~ ~
::: ornament
:::
The empty div becomes a centered ~ ~ ~. A use with content wraps it: the content goes into the template's slot —
the element content names, or else its first empty element of the use's form, skipping one whose handler takes no
content. A template with no slot renders the same content every time, so a use wrapping text in ornament
stops the build rather than drop the text.
A shortcut renders the forms it has templates for: one with only a body is a div, and
used as a span it stops the build. With no template, it renders the forms the system
shortcut it builds on declares. A span is one paragraph of inline Markdown:
sic:
class: container
span: |
[]{.slot} [[sic]]{.font style="italic"}
The sign read [Pubic Library]{.sic}.
The quoted words land in the first empty span, []{.slot}, followed by an italic "[sic]".
The slot can carry the style itself:
ship:
class: container
span: |
HMS []{.font style="italic"}
Darwin sailed on [Beagle]{.ship}.
Beagle lands in the italic span, after an upright "HMS".
An interface binds into every template the shortcut has, so an author writes the same attributes in either form; a route to an element one template lacks stops the build.
Naming the content slot
When the template has several elements and the first empty one isn't where author content should go, name the slot with
content:
ornamented-quote:
class: container
content: font
body: |
::: ornament
:::
::: {.font style=italic}
:::
Without content, the slot would be the empty ornament, the one from Templates, whose template has no
room for text, and the build would stop. Naming font puts the text there, below the ornament.
When fixed elements must surround the author content inside one parent, use the slot placeholder — it receives the
content while its siblings stay put:
motto:
class: font
content: slot
interface:
source:
bind: optional-source.text
body: |
::: quote
::: slot
:::
::: optional-source
:::
:::
::: {.motto source="Family saying"}
Measure twice, cut once.
:::
Author content flows into slot inside quote, while the attribution stays a sibling sharing the quote's indented
width, and renders only when the use sets source.
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.
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 element) — but usually you needn't think about it.
An interface entry whose bind target isn't class routes onto the matching template element. 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 element carries its own style="italic" in the template. A
template attribute no entry routes to is private — the author can't change it. An entry routed to it, with no
default, makes it overridable while the template's value stays the built-in.
Nesting
Template elements may themselves use shortcuts — divs, spans and headings alike — and they expand recursively, so composition nests naturally:
poem-footer:
class: poem-date
body: |
::: small-garamond
:::
Templates that embed each other in a loop stop the build before any chapter is read.
Headings
A heading names a shortcut as a div does, and keeps its text. set and appendix take one, as their
reference shows; a shortcut built on container renders one too, and classes adds the Pandoc-native
classes it carries, so a heading style takes a name of your own:
front-matter:
class: container
classes: [unnumbered, unlisted]
# Preface {.front-matter}
The heading is unnumbered and unlisted, as # Preface {.unnumbered .unlisted} would be. A heading takes no template, so
a shortcut with a body or span renders only those forms. classes takes the
native classes Pandoc's writers read — unnumbered, unlisted and mark; smallcaps
and underline work only written in the manuscript.
A shortcut built on container adds classes alone. To style a heading's text, put a span inside it:
# [Preface]{.font style=italic}
The heading, its contents entry and its running head take the style; a PDF bookmark stays plain text. A size there
enlarges the contents entry and running head too. The look of every heading of a level is the
target's.
A heading in a template expands too, its text fixed by the template.
classes works in any form a writer reads the class in. A highlight is mark on a span:
hl:
class: container
classes: [mark]
A [crucial]{.hl} word.
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 a fuller version of
this composition as aligned-figure, so you rarely write it yourself — it's shown here only to illustrate identifier
routing.)
What gets rejected
The build compiles shortcuts.yaml before it reads a chapter, and stops on any mistake below. A message names each key
at fault by path, as in pullquote.interface.font-size.default, and links the group holding its rule.
The file
More than one YAML document, a null anywhere, a key written twice, a list or map used as a key, or something other than a map of names to definitions. See Anatomy, and Shadowing a system shortcut for why a null means nothing.
A key
An unknown top-level key, or an unknown key in an entry. A shortcut takes the keys in Anatomy — forms
belongs to a system shortcut fronting a handler; an entry takes bind, default, required, const and accepts.
A value
A class, body, span or content that isn't text; a classes or forms that isn't a list of names, or a form
other than div, span or heading; an interface or entry that isn't a map; a required that isn't true or
false; a default or const that is a list or a map; a const beside a default or required. See
The interface and Fixed values.
A class
Missing on a new shortcut, written on a shadow, naming nothing, or leading back to itself through a chain; one in
classes that Pandoc's writers don't honor. See Anatomy, Chains,
Shadowing a system shortcut and Headings.
A name
One starting with ks-, a ks- class anywhere in the file, or a shortcut named after a
Pandoc-native class no system shortcut fronts, which keeps Pandoc's meaning. The ks-
prefix is reserved: a handler of your own is fronted by a system shortcut, as
Writing a handler shows, and your shortcuts build on that.
A bind
Not target.attribute, or to id or class; a class.X its parent doesn't expose; a template target on a shortcut
without templates of its own, or on a chain that inherits one; an entry without bind that inherits none; a route to an
element a template lacks; two entries routed to one attribute. See The interface,
Decoupling names and Chains.
A shadow
Taking over a system entry, binding a class.X the system shortcut doesn't route, or a template leaving out a form the
system shortcut renders. See Shadowing a system shortcut.
A fixed entry
A chain or a shadow writing a field of, or taking over, an entry another layer fixes; a template handing a nested shortcut a value it fixes. See Fixed values.
A template
A span that isn't one paragraph; templates embedding each other in a loop; a content naming no element of its form;
a template for a form its chain doesn't declare, or on a shortcut whose handler takes no content. See
Templates, Naming the content slot and Nesting.
A template element
Naming two shortcuts, or naming a shortcut that can't render its form, refuses the content it holds, or requires a value
nothing supplies — an attribution a use may leave out embeds optional-source rather than source. See
Templates and Nesting.
A handler's requirement
An attribute the handler needs that nothing in the chain or its templates guarantees. See
Required attributes. In a fork, a system shortcut fronting a handler stops the image build when
it declares no forms, or one the handler doesn't render; see Writing a handler.
An accepted value
A default, a const or a template value its entry doesn't accept. An accepts that isn't a name, a list of text or a
rule with its options; with an option its rule doesn't take, or set to a value it can't take; naming no rule, or a
system source such as ks-font@style; whose keywords name no values; taking values its entry doesn't; or rejecting a
value the system writes beneath it. See Accepted values.
What only warns
- an entry one definition gives both
required: trueand adefault - a template handing a nested shortcut an attribute it doesn't take
- a template class that names nothing, checked as the manuscript's are
In the manuscript
The classes you use in the manuscript are checked as each one expands. A div, span, or heading class that names no
shortcut or Pandoc-native style — a typo like .vspcae, or # Preface {.unnumberd} — is
reported, quoting the class and suggesting a close match when one exists. The build
still finishes (the bogus class is simply unstyled), unless it's a
strict build — then it fails rather than ship the mistake. A value
its entry doesn't accept is refused the same way. A use stops the build when it sets a
required attribute to a value nothing stands in for, names two shortcuts, uses a shortcut
in a form it lacks, wraps content in one that takes none,
sets an attribute the shortcut fixes, or names a
class starting with ks-.
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 template. - 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.