CSS subset
Fleuron uses CSS to describe how a book is typeset. This page describes all of the CSS rules that fleuron supports and how to use them. The engine warns about anything it does not support and continues, so one unsupported rule does not cost you the rest of the stylesheet.
The editor below is a live playground running the fleuron engine.
The defaults
The engine includes a built-in stylesheet that defines a basic trade paperback style: a 6x9 inch page, mirrored margins, and page numbers centered at the bottom. It sets the text in the bundled font at 11 pt with a line height of 1.4, left-aligned and unhyphenated. It indents every paragraph after the first and centers an ornament on each thematic break. A page break in the manuscript starts a new page, and a column break starts the next column.
The CSS for the built-in stylesheet is listed at the bottom of this page. Any additional CSS cascades over it regardless of specificity, so p { text-indent: 0 } overrides the built-in p + p rule.
The following example overrides the default page size, font size, text alignment, and hyphenation:
@page { size: a5; margin: 16mm 14mm 18mm 16mm;}
book { font-size: 10pt; text-align: justify; hyphens: auto;}The page
@page describes the page itself: its size, its margins, its columns, where its content sits, and the boxes in the margins that contain running heads and page numbers.
@page <name>? [ :first | :blank | :left | :right ]* { size: <length>{1,2} | <page-size> [ portrait | landscape ]?; margin: [ <length> | <percentage> ]{1,4}; margin-top: <length> | <percentage>; margin-right: <length> | <percentage>; margin-bottom: <length> | <percentage>; margin-left: <length> | <percentage>; background-color: <color> | transparent; background-image: none | <url>; background-repeat: repeat | no-repeat; background-size: auto | cover | contain | [ <length> | <percentage> | auto ]{1,2}; background-position: [ left | center | right | <length> | <percentage> ] [ top | center | bottom | <length> | <percentage> ]?; column-count: auto | <integer>; column-width: auto | <length>; column-gap: normal | <length>; column-rule-width: <length> | thin | medium | thick; column-rule-style: none | solid; align-content: start | center | end; @<margin-box> { content: none | <string> | counter(page) | counter(page, <counter-style>) | string(<name>); /* text properties */ }}Size and margins
size takes a width and a height, a single length for a square page, or a named paper size.
A named page size is one of these, portrait unless landscape follows.
a3 a4 a5 b4 b5 letter legal ledgermargin takes one to four lengths, and there is a longhand for each edge. What is left inside the margins is the content box, and its width is the width that lines of text break to.
The following example sets a 5.5 by 8.5 inch page, with the widest margin at the bottom and a slightly wider one on the left:
@page { size: 5.5in 8.5in; margin: 0.7in 0.6in 0.8in 0.7in;}Which pages a rule selects
@page on its own applies to every page. Adding a selector narrows it:
| selector | the pages it selects |
|---|---|
:left | left-hand pages, called versos |
:right | right-hand pages, called rectos |
:first | the opening page of a chapter |
:blank | a blank page the engine inserts, such as when a chapter has to begin on the right |
A new page group starts wherever a section begins a page, so :first matches the opening page of each chapter rather than only the first page of the book. break-before accepts recto and verso as values.
The following example mirrors the margins so the wider one always falls at the spine:
@page :left { margin-left: 0.6in; margin-right: 0.7in;}
@page :right { margin-left: 0.7in; margin-right: 0.6in;}page: <name> on an element names the pages its content falls on, and @page <name> selects those pages. The built-in stylesheet names every section’s pages chapter, which is what lets @page chapter:first match a chapter’s opening page and nothing else.
The following example numbers the front matter in lowercase roman numerals:
section.front { page: front }
@page front { @bottom-center { content: counter(page, lower-roman) }}A section read from a whole file takes its classes from the class: field of the file’s frontmatter, and its id from id:. A host can also give the sections of a source classes and an id beside the text. In the browser, the method is setAttributes. In Rust, it is Session::set_source_attributes.
Page rules cascade like any other rule: origin first, then specificity, then source order. A page name outranks :first and :blank, which outrank :left and :right.
Page backgrounds
A page carries a background: a color, an image, or a color with an image over it. The engine paints it over the whole page box, margins included, before anything else on the page.
The following example puts one scan behind every left-hand page and another behind every right-hand page:
@page :left { background-image: url("scans/verso.webp"); background-repeat: no-repeat; background-size: cover;}
@page :right { background-image: url("scans/recto.webp"); background-repeat: no-repeat; background-size: cover;}The five background properties are described under backgrounds. They mean the same thing on a page as on a block. The box they measure against is the page box rather than the border box.
The engine opens no files of its own. Whatever string url() holds is the name the host resolves to bytes, the same way @font-face resolves a face. A url nothing resolves warns at the line and column it was written at, and the engine skips the image.
Running heads
A margin box is a rule nested inside @page. content sets what it shows, and it accepts the text properties as well.
The engine draws these margin boxes:
@top-left @top-center @top-right @bottom-left @bottom-center@bottom-rightThese parse but draw nothing:
@top-left-corner @top-right-corner @left-top @left-middle@left-bottom @right-top @right-middle @right-bottom@bottom-left-corner @bottom-right-cornercontent | shows |
|---|---|
"Pride and Prejudice" | that string |
counter(page) | the page number |
counter(page, lower-roman) | the page number in another counter style |
string(chapter) | the current value of a named string |
none | nothing |
string-set on an element stores a named string, updated each time the text reaches such an element. content() inside it means the element’s own text, which headings, paragraphs, and inline elements have. content(text) is another spelling of the same thing. The built-in stylesheet already stores the chapter string from each section’s opening heading, so a running head takes one rule.
The following example puts the current chapter title at the top of every right-hand page:
@page :right { @top-center { content: string(chapter); font-size: 8pt; letter-spacing: 0.06em; }}string() reads the value the string had when the page began, not the value an element further down the page sets, so a chapter that starts halfway down a page does not change that page’s running head. A string that nothing has set yet shows nothing.
A -center box is centered on the page rather than on the content box, so a page number stays on the page’s axis even where mirrored margins push the content box off it. -left and -right align to the edges of the content box.
A page with no text on it draws no margin boxes at all. @page :blank still sets that page’s size and margins.
Page numbers
counter(page) in a margin box shows the page number.
A counter style is one of these.
decimal lower-roman upper-roman lower-alpha upper-alphaA number the counter style cannot express falls back to decimal.
counter-reset: page <integer> on an element restarts the count. The page that element begins takes that number, and later pages count on from it.
section.body { counter-reset: page 1 }The count includes blank pages. Restarting it does not change which side of the spread a page falls on, because that depends on where the page sits in the book rather than on the number printed on it.
Columns
These properties divide the page’s content box:
| property | gives |
|---|---|
column-count | that many columns |
column-width | as many columns of at least that width as will fit |
column-gap | the space between columns, where normal is one em of the book’s font size |
Given both a count and a width, the count is the maximum.
The following example sets two columns with a thin vertical rule between them:
@page { size: a4; margin: 20mm; column-count: 2; column-gap: 18pt; column-rule-width: 0.5pt; column-rule-style: solid;}Text fills the first column to the bottom, then the next, and only the last column running out starts a new page. orphans, widows, and break-inside: avoid apply within a column. break-before: column and break-after: column move to the next column instead of the next page.
column-rule-style: solid draws a vertical rule in each space between columns, column-rule-width wide and centered in that space. The rule takes no width from the columns. A page that ends up with a single column draws no rule.
Running heads and page numbers belong to the page rather than to a column, so dividing the content box leaves them where they are. An image you put against the page belongs to the page as well. See Images on a divided page for how the text of each column wraps around it.
Spanning the columns
In a book with two columns, the headings and the tables often go across both columns. To put a block across the whole content box, set column-span: all on it. The page then has columns above the block and columns below it.
The following example puts every h2 heading and every table across both columns:
@page { column-count: 2; column-gap: 18pt;}
h2, table { column-span: all;}A spanning block is a block with column-span: all. The engine does not balance the columns above a spanning block. They can end at different heights. The text after the block starts directly below it. That text fills the first column and then the next. A column rule stops above a spanning block and starts again below it.
If a spanning block does not fit in the space left on the page, it moves to the next page whole. Only a spanning block that is taller than a page splits across pages. The built-in stylesheet gives every heading break-after: avoid. If the text after a spanning heading cannot start on the same page, the heading moves to the next page with that text.
column-span applies to the blocks of a section. It does not apply to a block inside another block, such as a paragraph in a quotation or a table cell. On a page with one column, column-span has no effect.
Placing content down the page
align-content puts the content of a page at the top, the middle, or the bottom of the content box.
| value | where the content goes |
|---|---|
start | at the top of the content box, which is the default |
center | the same distance from the top and from the bottom |
end | at the bottom of the content box |
align-content moves only a page that has space below its content. A page can have that space when a break ends it, or when the book ends on it. A page that ends because nothing more fits on it does not move. The engine lays out a full page the same way under every value.
The running heads and the page numbers stay where they are. An image or a block against the page stays where its insets put it. If the text of a page wraps around an image or a block, the content of that page stays at the top.
The following example puts each chapter title on a page of its own, centered between the top and bottom margins:
h2 { break-after: page;}
@page chapter:first { align-content: center;}Selectors
The element names come from markdown:
book section notes note h1 h2 h3 h4 h5 h6 p blockquote prehr img ul ol li table thead tbody tr th td code em strong as spanA compound selector is one of these, optionally followed by any of the pseudo-classes below.
| compound | example |
|---|---|
<element> | p |
* | section > * |
.<class> | p.epigraph |
#<id> | #frontispiece |
:first-child :last-child :only-child :nth-child():nth-last-child() :first-of-type :last-of-type :only-of-type:nth-of-type() :nth-last-of-type() :empty :root :is():where() :not() :has()A combinator joins two compound selectors, and a , separates the selectors in a list (h1, h2).
| combinator | example |
|---|---|
| descendant | section p |
| child | section > p |
| next-sibling | h1 + p |
| subsequent-sibling | h1 ~ p |
The following examples use the combinators and pseudo-classes to reach particular paragraphs and headings:
h2 + p { text-indent: 0 }section > p:first-child { text-indent: 0 }blockquote p:not(:first-child) { text-indent: 1em }:is(h1, h2, h3) { break-after: avoid }h1, h2 { text-align: center }There are no attribute or namespace selectors, and no interaction pseudo-classes such as :hover, because nothing here is interactive.
A run of text is not an element. It takes the style of the paragraph or inline element around it, and it never counts as a child for :first-child.
Classes and ids
A class or an id names a particular element rather than every element of a kind: one image rather than every img. An element can have any number of classes and at most one id. Where two elements share an id, both match and the second one warns. See the markdown mapping for how a manuscript assigns them.
p.epigraph { font-style: italic; text-indent: 0; margin: 2em 4em;}
#frontispiece { margin: 0; break-after: page;}Pseudo-elements
The pseudo-elements are ::first-letter, ::first-line, ::before, and ::after.
Each pseudo-element starts from the style of the element it belongs to, so a rule on p::first-letter begins with that paragraph’s own font and size. ::first-letter produces a drop cap, and ::first-line styles a paragraph’s opening line. Where both apply to the same paragraph, ::first-letter takes the initial letter and ::first-line styles the rest of that line.
::before and ::after add content to an element. The content property holds what they add. A rule with no content, or with content: none, adds nothing. See Cross-references for how to print the page number or the text of another element in content.
On an inline element, ::before adds text before the element, and ::after adds text after it. An inline element is a link, emphasis, strong text, struck text, code, or a span. The text properties style that text.
On a block or a section, ::before adds a box as the first child, and ::after adds a box as the last child. The box is as wide as the content box of the element. It takes the text properties of the element, and a rule on the pseudo-element overrides them.
The box also takes the properties under Blocks: a margin, a border, padding, a height, and a background. It takes up height like any other block, so the blocks under it move down. A page break does not separate the box from its element.
content in the box holds a string, or a reference to a target you write, such as target-counter("#glossary", page). An empty string gives a box with no text. attr(href url) is the url of a link, so a block has none. The engine warns about it and adds nothing.
The following example draws a rule under every h4 heading and puts an opening quotation mark before every quotation:
h4::after { content: ""; height: 2pt; background-color: #d6075e;}
blockquote::before { content: "\201C";}position: absolute puts the box against the page, the same as a block. The place of the box in its element decides the page. The box then takes up no room among the paragraphs. With wrap-flow, the text wraps around the box. If the element is relative or absolute, the box measures its insets from that element. See The box the insets measure from.
The following example puts an ornament at the top left of the content box, on each page that an h2 heading starts on:
h2::before { content: "\2766"; position: absolute; top: 0; left: 0;}A book, a table row, a table cell, thead, and tbody take no box. If a rule adds one to them, the engine warns and adds nothing.
Declarations, values, and units
A declaration is <property>: <value> !important?.
A length can be written in any of these units, and the engine converts them all to points.
pt px pc in cm mm q em remem in font-size means a multiple of the parent’s font size. Everywhere else it means a multiple of the element’s own. rem is always a multiple of the root font size.
A color can be a CSS color name, a hex color, or rgb() with numbers or percentages. A hex color has 3, 4, 6, or 8 digits. rgba() is another name for rgb().
A color can have an alpha channel, which is how opaque the color is. At 0 the color is transparent, and at 1 it is opaque. In a hex color of 4 or 8 digits, the last digit or the last two digits are the alpha channel. In rgb(), the alpha is a fourth value after a comma, or a value after a slash where spaces divide the values. The alpha is a number from 0 to 1, or a percentage.
The following example gives block quotes a transparent black background and gives two text colors an alpha channel:
blockquote { background-color: rgba(0, 0, 0, 0.25) }h2 { color: rgb(140 30 30 / 80%) }.epigraph { color: #6b625780 }Custom properties
A custom property gives a value a name, so you write the value once and use it in many places. The name of a custom property starts with --.
A custom property declaration is --<name>: <value> !important?. var( --<name> [, <fallback> ]? ) stands for the value of a custom property in another declaration.
The following example names a color on the book, and uses it for the headings and for the border of each quotation:
:root { --accent: #d6075e;}
h2 { color: var(--accent);}
blockquote { border-left: 2pt solid var(--accent);}A custom property inherits. A value set on section reaches every element inside the section, and a value set on an element inside it overrides that value. var() can stand for a whole value or for part of one, in any property. The names are case-sensitive, so --Accent and --accent are two custom properties.
If a custom property has no value, var() uses the fallback after the comma. font-size: var(--size, 12pt) gives 12pt where no rule declares --size. If there is no fallback, the property takes its inherited value if it inherits, or its initial value if it does not. The engine warns and names the line and column of the declaration.
A custom property that depends on itself, such as --a: var(--b) with --b: var(--a), has no value. The engine warns about each custom property in the cycle. If the value that var() gives is not one the property takes, the engine warns about an unsupported value. The property then takes its inherited or initial value.
@page and the margin boxes can use var(). There, var() uses the custom properties of the book, which a rule on :root or book declares.
Text
These properties inherit. A value set on book or section reaches the text below it, and a rule on h2 overrides what the heading inherited.
| property | example |
|---|---|
font-family | font-family: "Author Serif", serif |
font-size | font-size: 11pt |
font-style | font-style: italic |
font-weight | font-weight: bold |
color | color: darkslategray |
line-height | line-height: 1.4 |
letter-spacing | letter-spacing: 0.05em |
font-variant-caps | font-variant-caps: small-caps |
font-feature-settings | font-feature-settings: "ss01" 1 |
font-variant-ligatures | font-variant-ligatures: discretionary-ligatures |
font-variant-numeric | font-variant-numeric: oldstyle-nums |
font-variant-alternates | font-variant-alternates: historical-forms |
text-transform | text-transform: uppercase |
text-decoration-line | text-decoration-line: underline |
text-decoration-color | text-decoration-color: #808080 |
text-decoration-style | text-decoration-style: double |
text-decoration-thickness | text-decoration-thickness: 0.06em |
text-decoration | text-decoration: underline |
text-align | text-align: justify |
text-justify | text-justify: inter-character |
text-indent | text-indent: 1.2em |
hanging-punctuation | hanging-punctuation: first |
hyphens | hyphens: auto |
orphans, widows | orphans: 3 |
page | page: chapter |
border-collapse | border-collapse: collapse |
list-style-type | list-style-type: decimal |
font-family: [ <family-name> | serif | sans-serif | monospace ]#;font-size: <length> | <percentage>;font-style: normal | italic | oblique;font-weight: normal | bold | <number [1,1000]>;color: <color>;line-height: normal | <number> | <length> | <percentage>;letter-spacing: normal | <length>;font-variant-caps: normal | small-caps;font-feature-settings: normal | <feature-tag-value>#;font-variant-ligatures: normal | none | [ common-ligatures | no-common-ligatures ] || [ discretionary-ligatures | no-discretionary-ligatures ] || [ historical-ligatures | no-historical-ligatures ] || [ contextual | no-contextual ];font-variant-numeric: normal | [ lining-nums | oldstyle-nums ] || [ proportional-nums | tabular-nums ] || [ diagonal-fractions | stacked-fractions ] || ordinal || slashed-zero;font-variant-alternates: normal | historical-forms;text-transform: none | uppercase | lowercase | capitalize;text-decoration-line: none | [ underline || overline || line-through ];text-decoration-color: currentcolor | <color>;text-decoration-style: solid | double;text-decoration-thickness: auto | from-font | <length>;text-decoration: none | [ underline || overline || line-through ] || solid || double || <color> || <length>;text-align: left | right | center | justify | start | end;text-justify: auto | inter-word | inter-character | distribute;text-indent: <length> | <percentage>;hanging-punctuation: none | [ first || [ force-end | allow-end ] || last ];hyphens: none | manual | auto;orphans: <integer>;widows: <integer>;page: auto | <name>;border-collapse: separate | collapse;list-style-type: disc | circle | square | decimal | lower-roman | upper-roman | lower-alpha | upper-alpha | none;Line breaking and justification
The engine breaks a paragraph as a whole rather than one line at a time. It costs every possible set of breaks by how far each line falls short of the full width, cubed, and takes the cheapest set. A word that would fill one line stays where it is if moving it leaves a bigger gap in the next.
text-align decides where a finished line sits:
| value | |
|---|---|
left, start | flush left |
right, end | flush right |
center | centered |
justify | spaced out to fill the line exactly, on every line but the last |
Lines break the same way under all four. When justifying, text-justify: inter-character also lets the space between letters change. The default does not.
Hyphenation
hyphens: auto lets a line break inside a word at a syllable boundary. Each such break costs extra, and more again directly after another one, so no more than two lines in a row end in a hyphen. none and manual add no syllable breaks.
The syllable patterns come from the book’s language. extra["language"] in the metadata names it as a BCP 47 tag, read by its primary subtag, so fr-CA is French. The engine hyphenates a book that declares no language as English. A language it has no patterns for leaves every word whole and warns, naming the tag. The language belongs to the book, so one book is hyphenated in one language.
An inline code span and a pre take no hyphen, whatever hyphens asks for around them. A break the author did not write changes what the code says.
The following example justifies the text, hyphenates it, and hangs punctuation into the margins:
book { text-align: justify; text-justify: inter-word; hyphens: auto; hanging-punctuation: first allow-end last;}Small caps, letter spacing, and case
font-variant-caps: small-caps uses the font’s smcp feature, which is capitals drawn at lowercase height. For a font without one, the engine draws the lowercase letters as capitals at four-fifths of the size and warns, naming the font.
letter-spacing adds space between the glyphs of a run, and line breaking measures it like any other width. Nothing is added after the last glyph on a line, so a letter-spaced title centers on its letters rather than on the trailing space.
text-transform changes which characters are drawn, not the text stored underneath. Extracting text from the PDF, searching it, and copying from it all return what the manuscript said. capitalize raises the first letter of each word, where a word runs through letters, digits, and an apostrophe inside it.
The following example sets chapter headings in centered small capitals:
h2 { font-variant-caps: small-caps; letter-spacing: 0.08em; text-align: center; font-weight: normal;}Font features
A font can carry OpenType features: alternate glyphs that a four-character tag names. Old-style figures, a stylistic set, and a discretionary ligature are all features. font-feature-settings turns a feature on by its tag.
h2 { font-feature-settings: "ss01" 1 }A value of 1 or on turns a feature on, and a value of 0 or off turns it off. A tag written on its own is on. A feature that draws more than one alternate takes the number of the alternate.
Three properties name a feature without its tag. Each value below turns on the features beside it:
| property | value | feature |
|---|---|---|
font-variant-ligatures | common-ligatures | liga, clig |
font-variant-ligatures | discretionary-ligatures | dlig |
font-variant-ligatures | historical-ligatures | hlig |
font-variant-ligatures | contextual | calt |
font-variant-numeric | lining-nums | lnum |
font-variant-numeric | oldstyle-nums | onum |
font-variant-numeric | proportional-nums | pnum |
font-variant-numeric | tabular-nums | tnum |
font-variant-numeric | diagonal-fractions | frac |
font-variant-numeric | stacked-fractions | afrc |
font-variant-numeric | ordinal | ordn |
font-variant-numeric | slashed-zero | zero |
font-variant-alternates | historical-forms | hist |
Each font-variant-ligatures value has a no- form that turns its feature off, such as no-common-ligatures. font-variant-ligatures: none turns off all four ligature features.
The engine reads the font-variant longhands first and font-feature-settings after them. Where both name a tag, font-feature-settings sets it.
The engine turns some features on for every run of text. The fonts page lists them. font-feature-settings: "liga" 0 turns one of them off.
A feature changes which glyphs the engine draws, so it changes the width of the text. The engine measures the glyphs a feature selects, and the lines break to that width.
For a font without a feature the stylesheet names, the engine warns and names the font. The text is laid out without the feature. Nothing is synthesized.
The following example sets a part title in the font’s first stylistic set and the prose under it in old-style figures:
h2 { font-feature-settings: "ss01" 1; letter-spacing: 0.08em;}
p { font-variant-numeric: oldstyle-nums;}Color
color sets the color of the text, and it inherits.
book { color: #241f1c }h2 { color: rgb(140, 30, 30) }.epigraph { color: #6b6257 }Rules across the text
text-decoration draws a rule under, over, or through a run of text. text-decoration-line names which rules the engine draws:
| value | draws |
|---|---|
none | nothing |
underline | a rule under the text |
overline | a rule over the text |
line-through | a rule across the text |
The font declares where an underline sits and how thick it is. The engine draws it there. Two faces put their underlines at different depths. A book that mixes them draws each rule at the depth of its own face. The position of a rule across the text comes from the font as well.
text-decoration-color sets the color of the rules. The default is currentcolor, which is the color of the text. text-decoration-thickness gives a thickness of its own, in place of the one the font declares. text-decoration-style: double draws each rule twice, one thickness apart.
These four properties inherit. A block that draws a rule draws it across every run inside it. A run inside that block can declare text-decoration-line: none, which drops the rule.
The engine draws one rule for each run of text on each line. Where a line break splits a decorated run, each line carries a rule as wide as its own text. A rule crosses the descenders under it without a break.
The built-in stylesheet draws a rule through s, which is what ~~struck~~ in the manuscript becomes. The following example underlines a cross-reference in gray and draws a double rule through a deleted passage:
a { text-decoration: underline; text-decoration-color: #808080; text-decoration-thickness: 0.06em;}
s { text-decoration: line-through double }Hanging punctuation
hanging-punctuation lets a punctuation mark sit outside the text width so the edge of the text block looks straight. A period or comma at the end of a line leaves a visible gap, and hanging it outside closes the gap.
| value | hangs |
|---|---|
first | an opening quotation mark or bracket, out into the left margin |
force-end | a closing mark past the right edge, on every line |
allow-end | a closing mark, only where doing so makes the line fit |
last | the mark a paragraph ends on |
How far a mark hangs depends on the mark: nearly all of a period or comma, less of a colon, none of a letter.
Inline boxes
An inline element is a link, emphasis, strong text, struck text, code, or a span. It can take a background, padding, and a border, the way a block does. The properties are the ones in margins, borders, and padding, backgrounds, and rounded corners. A margin on an inline element is not in the subset.
The engine paints one box per line the element reaches. Padding and a border on the left and the right are width. They move the text after them, and the line breaks against them. Padding and a border above and below the text leave the height of the line alone. A box deeper than the text reaches over the lines around it.
Where a line break splits the element, box-decoration-break decides which edges each part of it takes. With slice, the first part draws the left edge and the last part draws the right edge. The corners at the break are square. With clone, every part draws all four edges and all four rounded corners.
The engine paints the box over the background of the block around it, and under the text it sits behind. A block that the sheet puts against the page carries the boxes of its own runs with it.
position, z-index, and opacity on an inline element are not in the subset. The element takes the layer and the opacity of the block around it.
The following example sets a keyword in white on a gray chip with rounded corners:
code { background-color: #858585; color: #ffffff; padding: 2pt 4pt; border-radius: 3pt;}Blocks
These do not inherit. A block is a paragraph, a heading, a quotation, an image, a thematic break, a list, a list item, or a table.
| property | example |
|---|---|
content | content: "\2766" |
string-set | string-set: chapter content() |
counter-reset | counter-reset: page |
initial-letter | initial-letter: 3 |
position | position: relative |
top, right, bottom, left | top: 0 |
z-index | z-index: 10 |
opacity | opacity: 0.05 |
wrap-flow | wrap-flow: end |
shape-outside | shape-outside: polygon(0 0, 100% 0, 100% 100%) |
shape-margin | shape-margin: 6pt |
width, height, min-height | width: 8em |
max-width, max-height | max-width: 4in |
margin | margin: 1em |
margin-top, margin-right, margin-bottom, margin-left | margin-top: 1em |
padding | padding: 12pt |
padding-top, padding-right, padding-bottom, padding-left | padding-top: 6pt |
border, border-top, border-right, border-bottom, border-left | border: 2pt solid |
border-width | border-width: 2pt |
border-style | border-style: solid |
border-color | border-color: crimson |
border-radius | border-radius: 3pt |
border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius | border-top-left-radius: 3pt |
background-color | background-color: #f4f1ea |
background-image | background-image: url("scan.webp") |
background-repeat | background-repeat: repeat |
background-size | background-size: cover |
background-position | background-position: center |
box-decoration-break | box-decoration-break: slice |
break-before, break-after, break-inside | break-before: recto |
column-span | column-span: all |
content: none | [ <string> | target-counter( <target> , page , <counter-style>? ) | target-text( <target> ) ]+;string-set: none | [ <name> [ content() | content(text) | <string> ]+ ]#;counter-reset: none | [ page <integer>? || note <integer>? ];initial-letter: <integer>;position: static | relative | absolute;top: auto | <length> | <percentage>;right: auto | <length> | <percentage>;bottom: auto | <length> | <percentage>;left: auto | <length> | <percentage>;z-index: auto | <integer>;opacity: <number> | <percentage>;wrap-flow: auto | both | start | end;shape-outside: none | auto | polygon( [ <length> | <percentage> ]{2} [ , [ <length> | <percentage> ]{2} ]* );shape-margin: <length> | <percentage>;width: auto | <length> | <percentage>;height: auto | <length> | <percentage>;min-height: auto | <length> | <percentage>;max-width: none | <length> | <percentage>;max-height: none | <length> | <percentage>;margin: [ <length> | <percentage> ]{1,4};margin-top: <length> | <percentage>;margin-right: <length> | <percentage>;margin-bottom: <length> | <percentage>;margin-left: <length> | <percentage>;padding: [ <length> | <percentage> ]{1,4};padding-top: <length> | <percentage>;padding-right: <length> | <percentage>;padding-bottom: <length> | <percentage>;padding-left: <length> | <percentage>;border: [ <length> | thin | medium | thick ] || [ none | solid ] || <color>;border-top: [ <length> | thin | medium | thick ] || [ none | solid ] || <color>;border-right: [ <length> | thin | medium | thick ] || [ none | solid ] || <color>;border-bottom: [ <length> | thin | medium | thick ] || [ none | solid ] || <color>;border-left: [ <length> | thin | medium | thick ] || [ none | solid ] || <color>;border-width: [ <length> | thin | medium | thick ]{1,4};border-style: [ none | solid ]{1,4};border-color: <color>{1,4};border-radius: [ <length> | <percentage> ]{1,4} [ / [ <length> | <percentage> ]{1,4} ]?;border-top-left-radius: [ <length> | <percentage> ]{1,2};border-top-right-radius: [ <length> | <percentage> ]{1,2};border-bottom-right-radius: [ <length> | <percentage> ]{1,2};border-bottom-left-radius: [ <length> | <percentage> ]{1,2};background-color: <color> | transparent;background-image: none | <url>;background-repeat: repeat | no-repeat;background-size: auto | cover | contain | [ <length> | <percentage> | auto ]{1,2};background-position: [ left | center | right | <length> | <percentage> ] [ top | center | bottom | <length> | <percentage> ]?;box-decoration-break: slice | clone;break-before: auto | avoid | avoid-page | avoid-column | column | page | always | left | right | recto | verso;break-after: auto | avoid | avoid-page | avoid-column | column | page | always | left | right | recto | verso;break-inside: auto | avoid | avoid-page | avoid-column | column | page | always | left | right | recto | verso;column-span: none | all;Margins, borders, and padding
A block has a margin, a border, and padding around its content, and a background behind them.
The following example indents block quotes and rules them down the left edge:
blockquote { margin: 1em 2em; padding: 0.5em 1em; border-left: 2pt solid #b9b0a2; background-color: #f4f1ea;}| property | takes |
|---|---|
margin, padding | one to four lengths, clockwise from the top |
margin-top and the other longhands | one length, for one edge |
border | a width, a style, and a color, in any order |
border-top, border-right, border-bottom, border-left | the same three, for one edge |
border-width, border-style, border-color | one of the three, in one to four values |
The only border styles are none and solid.
A border or padding on the left or right narrows the text and moves its left edge inward. On the top and bottom they take vertical space, and they stop the margins on either side from collapsing: a top border on a block quote sits between the quote’s own top margin and its first paragraph’s margin, and both take space.
Where two edges of different colors meet, the corner takes the color of the horizontal edge rather than splitting diagonally.
Height
height gives the height of a block, not counting its padding, its border, and its margins. min-height gives the least height of a block. Each one takes a length or a percentage.
If the content of a block is shorter than its height, the engine leaves the rest as space below the content. The background and the border of the block cover that space. A page does not end between the content and the space below it. If the content is taller than its height, the block grows to hold the content.
A percentage is a percentage of the height of the page’s content box. Inside a block that has a height, a percentage is a percentage of that height instead. A min-height does not count for this.
The following example gives each chapter title a height of 3 inches. The text of each chapter starts 3 inches below the top of its title:
h2 { height: 3in;}Image size
width and height give the size of an image. max-width and max-height give the largest size of an image. Each one takes a length or a percentage, and max-width and max-height also take none.
If you give one side, the other side keeps the proportions of the image file. If you give both sides, the image takes both, and its proportions can change. If you give neither side, the image takes the size of its file.
A percentage on width or max-width is a percentage of the width of the block the image is in. Inside a quotation, that is the width of the quotation. A percentage on height or max-height is a percentage of the height of the page’s content box.
An image is never wider than the block it is in or taller than the page’s content box. If it does not fit, the engine makes it smaller and keeps its proportions. The engine warns when an image does not fit the height, or when a size you give does not fit the width.
On an image, height is the height of the image. The engine leaves no space below an image that it makes smaller.
The following example makes every image 200 points wide, and keeps each map under 3 inches tall:
img { width: 200pt;}
img.map { max-height: 3in;}Backgrounds
A background is a color, an image, or a color with an image over it. The engine paints it over the block’s border box, and the border draws over it. Where a block breaks across a page turn, the engine paints the background on each page, behind the part the page holds.
The following example tints a block quote and tiles an ornament over the tint:
blockquote { padding: 0.5em 1em; background-color: #f4f1ea; background-image: url("ornaments/vine.webp"); background-repeat: repeat; background-size: 24pt;}| property | takes |
|---|---|
background-color | a color, or transparent |
background-image | none, or a url() the host resolves |
background-repeat | repeat or no-repeat |
background-size | auto, cover, contain, or one or two lengths |
background-position | one or two of a keyword, a length, or a percentage |
background-size: cover is the smallest size that covers the box. The box crops what runs past it. contain is the largest size that fits inside the box. The box keeps the remainder.
One length sizes the image across the box. The image’s own proportions then size it down the box. A percentage measures against the box.
background-position places the image in the box. left, center, and right place it across the box. top, center, and bottom place it down the box. A single value centers the image on the other axis. A percentage aligns that fraction of the image with the same fraction of the box, which is why 50% centers it. A length is an offset from the box’s top left corner.
The engine opens no files of its own. Whatever string url() holds is the name the host resolves to bytes. A url nothing resolves warns at the line and column it was written at, and the engine skips the image.
Rounded corners
border-radius rounds the corners of a block. The background and the border follow the rounded corners, and the engine clips the background image to them.
| property | takes |
|---|---|
border-radius | one to four radii, clockwise from the top left corner |
border-top-left-radius and the other corners | one or two radii, for one corner |
A radius is a length or a percentage. With one radius, a corner is a quarter circle. With two, the first radius is horizontal, the second is vertical, and the corner is a quarter ellipse. In border-radius, the radii after a slash are the vertical radii.
A percentage on a horizontal radius is a percentage of the width of the border box. A percentage on a vertical radius is a percentage of its height. If the radii of two corners on one edge are longer than the edge, the engine makes all the radii smaller by the same factor until they fit.
Where a page turn breaks a block with box-decoration-break: slice, the corners at the break are square. With clone, each part of the block has its four rounded corners.
The following example gives each tag a gray box with rounded corners:
.tag { padding: 1pt 4pt; background-color: #e3e3e3; border-radius: 3pt;}Opacity
opacity makes a block and everything in it partly transparent. It takes a number from 0 to 1, or a percentage. At 0 nothing of the block shows, and at 1 all of it shows.
The opacity applies to the text, the background, the border, and the images of the block. It also applies to the blocks inside it. opacity does not inherit, but a block inside a transparent block is transparent with it. Two opacities multiply, so opacity: 0.5 inside opacity: 0.5 shows a quarter of the inner block.
The engine makes each part of the block transparent on its own. Where the text of a block covers its background, the background shows through the text. In a browser, opacity makes the block transparent as one picture, so the background does not show through the text.
The following example puts a large transparent ornament against the opening page of a chapter:
.ornament { position: absolute; top: 0; left: 0; font-size: 160pt; opacity: 0.05;}Layers
The engine paints every block in a layer. It paints a higher layer over a lower one. z-index names the layer of a block. Inside one layer, the engine paints the blocks in the order they are written.
z-index: auto is layer 0. The engine paints a block that names no layer there.
The following example puts a border against the page and draws the text of the chapter over it:
img.border { position: absolute; top: 0; left: 0; z-index: 1;}
h1, p, blockquote { z-index: 2;}A block does not take the layer of the block around it. The example names the paragraphs as well as the quotations, because a quotation at layer 2 leaves the paragraphs inside it at layer 0.
Two layers are the engine’s own. A stylesheet cannot name either one. The engine paints the background of a page under every layer a stylesheet can name. Where a stylesheet names no z-index, the text of a page still covers that background. The engine paints a page number and a running head over every layer, so a border against the page never covers them.
z-index here is one number in one order over the whole page. In CSS, z-index on a positioned block also starts a stacking context, which is a paint order of its own. The layers of the blocks inside that block then count against it rather than against the page. The engine reads the number and starts no stacking context. position, a background image, and a page break do not change the order.
Breaking across pages
break-before and break-after start a new page or column before or after a block. break-inside: avoid keeps a block from splitting. To start a new page or column at one place in the text, write a page or column break in the manuscript.
| value | |
|---|---|
auto | break wherever the text falls |
avoid, avoid-page, avoid-column | do not break here |
page, always | the next page |
left, verso | the next left-hand page |
right, recto | the next right-hand page |
column | the next column |
orphans and widows limit where a paragraph can split. A paragraph with fewer than orphans + widows lines moves whole to the next page instead. Where no permitted break is left, the page ends anyway.
The following example starts each chapter on a right-hand page, keeps a heading with the text below it, and keeps block quotes whole:
section { break-before: recto }h2 { break-after: avoid }blockquote { break-inside: avoid }
p { orphans: 2; widows: 3;}Where a page break splits a block, box-decoration-break decides how the two new edges are drawn. slice, the default, treats the block as one box and cuts it, leaving both new edges open. clone closes each piece on all four sides. Neither repeats the padding, which belongs to the block’s own top and bottom.
Ornaments
content sets the text of an empty element, which in markdown means the thematic break. The built-in stylesheet uses a floral heart, and any other string replaces it.
hr { content: "* * *"; text-align: center; margin: 1.5em 0;}Code blocks
A code block in the manuscript becomes a pre. The block is preformatted: it breaks its lines at its own newlines and nowhere else, its spaces stand where the author wrote them, nothing in it hyphenates, no line of it is justified, no mark of it hangs past the measure, and its first line carries no indent. No declaration turns any of that off. white-space is not in the subset, and a pre is preformatted whether or not a stylesheet says so.
In the built-in stylesheet, a code block has a monospace family and space above and below it.
The following example puts a rule above and below a code block and tints it:
pre { margin: 1.5em 0; padding: 0.5em; border-top: thin solid; border-bottom: thin solid; background-color: #f2f0eb;}A code block holds text rather than an inline code, so pre code selects nothing. code is the inline code span, and a rule on it reaches code written among prose.
A line wider than the measure runs past the measure, because a code block has nowhere else to break. The engine warns and names the line and column of the block. To keep a listing inside the measure, set a smaller font-size on pre, or break the line in the manuscript.
A code block breaks across pages between two of its lines, and each line keeps its own indentation on the page it lands on. orphans and widows hold lines together at each end, and break-inside: avoid keeps the whole block on one page.
The engine does no syntax highlighting. The word after the opening fence reaches the content tree as info, for a painter that reads it. See the markdown mapping.
Tables
A table in the manuscript becomes a grid. In the built-in stylesheet, the header row is bold, a rule runs under it, and a rule runs above and below the table.
width on a cell of the first row is the width of that column. The columns with no width share the rest of the width of the table equally. width is the width of the content of the cell, so the column also takes the padding and the borders of the cell.
The following example makes the first column 8em wide and tints every other row of the body:
th:first-of-type { width: 8em }
tbody tr:nth-child(odd) { background-color: #e3e3e3;}The header rows are in a thead, and the other rows are in a tbody. tbody tr:nth-child(odd) counts only the rows of the body. The rows inherit from thead and tbody. A border or a background on thead or tbody is not supported yet.
border-collapse: collapse draws one border where two cells meet. Where the two borders differ in width, the wider border is drawn. Where they are the same width, the border of the cell above or to the left is drawn. A border on a cell takes priority over a border on its row. A border on a row takes priority over a border on the table. border-collapse: separate gives every cell its own border, so two borders stand side by side where two cells meet. The built-in stylesheet uses collapse.
A table breaks across pages between two rows, and never inside a row. The header rows repeat at the top of every page that the table continues onto. A row that is taller than the page stays on one page and runs past the bottom of it, and the engine warns.
The engine applies the alignment that the delimiter row gives a column as text-align. A text-align rule in a stylesheet takes priority over it.
Lists
A list in the manuscript becomes a ul or an ol, and each item becomes an li. In the built-in stylesheet, the markers are in an indent of 1.5em to the left of the items. An ol has numbers, and a list inside an item has open circles.
list-style-type chooses the marker. The marker uses the font of the item, has a space after it, and ends at the left edge of the item. It is on the same baseline as the first line of the item.
| value | marker |
|---|---|
disc | • |
circle | ◦ |
square | ■ |
decimal | 1. 2. 3. |
lower-roman, upper-roman | i. ii. iii. |
lower-alpha, upper-alpha | a. b. c. |
none | no marker |
The following example numbers a list in capital roman numerals and puts space between its items:
ol { list-style-type: upper-roman;}
li + li { margin-top: 0.25em;}In a tight list, the text of an item is not in a p element. So li > p selects only the paragraphs of a loose list, and the built-in stylesheet uses it to put space between them.
A list breaks across pages between two items, or inside an item. A page does not end after the first item or before the last item. break-inside: avoid on a list or on an item keeps it on one page.
Moving a block
You can move a block away from its place among the paragraphs. Use position to choose how.
| value | what happens |
|---|---|
static | The block stays in its place. |
relative | The engine moves the block by its insets. Nothing around it moves. |
absolute | The engine takes the block out of the text and puts it against the page. |
top, right, bottom, and left are the insets. Each one takes a length or a percentage. A percentage on left or right is a percentage of the width of the content box. A percentage on top or bottom is a percentage of its height. Where a block around an absolute block is relative or absolute, the insets measure from that block instead. See The box the insets measure from.
Moving a block from its place
With position: relative, a positive top moves the block down, and a positive bottom moves it up. A positive left moves the block right, and a positive right moves it left. If you set both insets of an axis, the engine uses top or left and ignores the other one.
The block keeps the room it had among the paragraphs. The text around it stays where it was, and the pages break where they broke before.
The following example raises every chapter heading by 12 points and leaves the text under it in place:
h1 { position: relative; top: -12pt;}Putting a block against the page
position: absolute puts a block against the page the same way it puts an image there. The place where you write the block decides the page. The insets decide the place on that page. The block takes up no room among the paragraphs.
The insets also decide the width of the block. If you set both left and right, the block fills the space between them. If you set only one, the block fills the space from that inset to the far edge of the content box. If you set neither, the block is as wide as a line of text. The lines of the block break to that width.
wrap-flow and shape-outside work on a block as they work on an image. With shape-outside: auto, the text wraps around the box of the block.
The following example puts a quotation 1 inch above the bottom of the content box, and half an inch in from each side:
blockquote.epigraph { position: absolute; bottom: 1in; left: 0.5in; right: 0.5in;}The box the insets measure from
The insets of an absolute box measure from its containing block. The containing block is the nearest block around it with position: relative or position: absolute. The insets measure from the padding box of that block, which is the space inside its border. A percentage on an inset is a percentage of the width or the height of the containing block. A negative inset reaches outside it.
If no block around the box is relative or absolute, the containing block is the content box of the page.
An inset of a relative block moves the block. The boxes inside it move with it.
The place where you write the box still decides the page. A block around the box can run over more than one page. The box measures from the part of that block on the page the box lands on.
The following example moves a quotation 30 points down and 30 points to the right. The first paragraph of the quotation sits at the top left of the quotation and fills half of its width:
blockquote { margin: 0; position: relative; top: 30pt; left: 30pt;}
blockquote p:first-child { position: absolute; top: 0; left: 0; right: 50%;}Images against the page
You can put an image against the page rather than among the paragraphs. The text then wraps around it.
position: absolute takes the image out of the text. The insets place it on the page. top, right, bottom, and left measure from the content box, which is the space inside the page margins. A percentage is a percentage of the width or the height of the content box. A negative inset reaches into the margin. An inset of auto lets the opposite inset place the image. Where both insets of an axis are auto, the image sits at the left or the top edge of the content box.
If a relative or absolute block holds the image, the insets measure from that block rather than from the page. See The box the insets measure from.
The image takes its size the same way as an image among the paragraphs. See Image size. A percentage on width or max-width is a percentage of the width of the content box. If the image does not fit the content box, the engine makes it smaller. Margins on the image add space between it and the text.
Use wrap-flow to choose the side of the image the text wraps on.
| value | what goes beside the image |
|---|---|
auto | Nothing. The text breaks as if the image is not there. |
start | the side a line starts at |
end | the side a line ends at |
both | both sides |
The following example puts an image in the top right corner of the page it lands on. The text of that page wraps down the left side of the image.
img { position: absolute; top: 0; right: 0; margin-left: 12pt; margin-bottom: 6pt; wrap-flow: start;}The place where you write an image decides which page it lands on. An image written above a paragraph lands on the page that the paragraph starts on, and it takes up no room of its own. The image makes the lines beside it shorter, so the text above the paragraph can take more lines. If that pushes the paragraph onto the next page, the engine puts the image on the next page instead.
The engine shortens every line the image reaches to the space the image leaves. The lines under the image run the full width. Where the image covers the full width, the text continues below it.
Wrapping to the shape of an image
shape-outside wraps the text around the shape of an image instead of around its box. That outline is called a contour.
A contour works only on an image you put against the page with a wrap-flow other than auto. Those are the only images that text goes beside.
| value | the shape the text goes around |
|---|---|
none | the box, which is the default |
auto | the shape the image’s own alpha channel covers |
polygon(…) | the points you write |
polygon() takes pairs of lengths, one pair to a point, separated by commas. The first of a pair measures across the box, and the second measures down it. A percentage measures against the box. The polygon closes itself, so you write at least three points.
The following example wraps the text down a diagonal, from the top left corner of the image to its bottom right:
img { position: absolute; top: 0; left: 0; wrap-flow: end; shape-outside: polygon(0 0, 100% 100%, 0 100%);}auto takes the shape from the image’s own alpha channel. PNG, GIF and WebP carry one. JPEG does not, and neither does a PNG saved without transparency. Where there is no alpha channel, the text goes around the box instead, and the engine warns you.
shape-margin adds space between the contour and the text. Margins on the image do not, because a contour replaces the box they surround. Use shape-margin instead. An upright edge gets exactly the space you ask for, and a sloping edge gets at least that much.
The following example wraps the text around the shape the image’s own alpha channel covers, with 9pt of space between them:
img { position: absolute; top: 0; left: 0; wrap-flow: end; shape-outside: auto; shape-margin: 9pt;}The text wraps to the contour one line at a time. A line goes either beside the image or below it. Where the contour leaves a whole line’s height empty across the image, that line runs the full width.
Images on a divided page
An image against the page belongs to the page rather than to a column. It can reach more than one column.
The engine shortens the lines of every column the image reaches. Each of those lines runs to the space the image leaves in its own column. A column the image does not reach keeps its full width.
Where the image covers the whole width of a column, the text of that column continues below the image. The engine leaves the other column alone.
The columns still fill in order. An image changes the width of a line, not the order of the columns.
The following example divides the page in two and puts an image across the space between the columns. The text of the first column wraps down the left of the image, and the text of the second wraps down its right:
@page { column-count: 2; column-gap: 18pt;}
img { position: absolute; top: 0; left: 120pt; wrap-flow: both;}Footnotes
A footnote is written in the manuscript where its reference belongs. The engine puts the note at the foot of the page that reference lands on. The markdown mapping says how to write one.
Two elements style a note. note is the note itself: the reference in the line, the number beside the note, and the blocks of the note. notes is the area at the foot of the content box that holds the notes of one page.
The following example puts the notes of a page under a rule, in a smaller size than the text:
notes { margin-top: 1em; padding-top: 0.4em; border-top: thin solid;}
note { font-size: 9pt; line-height: 1.2; padding-left: 1.5em;}The area takes the properties under Blocks: a margin, a border, padding, and a background. It is as wide as a column, which on a page that does not divide is the whole content box. Its height is what the notes in it come to.
A note takes those properties, and the text properties with them. The box it paints is the box at the foot of the page. The reference in the line takes the text properties alone, so it stands against the word it was written after whatever padding the note has.
The blocks of a note are elements of their own, so note p selects the paragraphs of a note. A note stands where its reference was written, so it inherits from the paragraph that holds the reference. The built-in stylesheet returns font-style, font-weight, and text-indent to their initial values. A reference in italic therefore leaves the note upright.
The engine prints the number twice: as the reference in the line, and as a mark beside the note. list-style-type on the note spells both, in the values a list marker takes. The mark carries a point and a space after it, as a list marker does. It hangs in the padding to the left of the note, so padding-left is the indent it hangs in.
The engine draws the reference in the font’s superior figures, which are small raised figures. They are the sups feature. The feature reaches the reference alone, so the figures in the text of the note are the ordinary ones. The reference sits on the baseline of its line, at the size note gives it. For a font without superior figures, the engine warns and names the font, and draws the reference in the ordinary figures.
The area and the page
The engine takes the area off the column before it places a line. A column with notes under it holds fewer lines of text. A line that no longer fits moves on, and the note of a reference on that line moves with it.
A note longer than the room the column has left is split. The rest of it opens the area of the next column, above the notes of that column, and carries no reference of its own. The last column of a page hands what it cannot hold to the next page.
On a page that divides into columns, each column sets its own notes under itself.
Numbering
The note counter runs through the book. counter-reset: note restarts it, and where the rule is decides how often:
| the rule is on | the numbering restarts |
|---|---|
section | at every chapter, which is what the built-in stylesheet does |
notes | at every page |
| any other element | at that element |
counter-reset: note 5 restarts the numbering at 5 rather than at 1. The page and the note counters go in one declaration: counter-reset: page 1 note 1.
The page a note lands on is a layout result, so a note numbered by its page takes its number from a layout that is finished. The engine lays the book out again, up to four times, until the numbering stops changing. A book that has not settled by then keeps the numbers of the last layout, and the engine warns.
Chapter openings
Drop caps
A drop cap is a large first letter that sinks into the paragraph below it. Set initial-letter on the ::first-letter pseudo-element to the number of lines the letter should span.
The letter is sized to fit those lines rather than by font-size. Its top lines up with the top of the capitals on the first line, and its baseline sits on the last.
Punctuation next to the first letter is part of the drop cap. For example, the drop cap of a paragraph that opens with “Sir,” is “S. A paragraph that opens with a dash has no drop cap.
The following example gives the first paragraph after a chapter heading a three-line drop cap:
h2 + p { text-indent: 0;}
h2 + p::first-letter { initial-letter: 3; font-family: "Author Display", serif;}First lines
::first-line accepts only these properties:
color font-family font-size font-style font-weightfont-variant-caps letter-spacing text-transformtext-decoration-line text-decoration-colortext-decoration-style text-decoration-thicknesstext-decorationThe engine warns about any other property in a ::first-line rule and drops it.
::first-line applies to the paragraph’s first line, not to the first line on a page, so a paragraph split across a page break is styled on its opening line and nowhere else.
The following example sets the opening line of a chapter in small capitals:
h2 + p::first-line { font-variant-caps: small-caps; letter-spacing: 0.06em;}The following example opens a chapter on an italic line:
h2 + p::first-line { font-style: italic;}font-family, font-style, and font-weight are separate properties, and a rule can set one of them alone. A word inside the line that carries a weight of its own keeps that weight. An italic first line over a bold word sets that word in the bold italic.
Which words fall on that line depends on the style applied to it. A property that changes the width of the text moves the break, and the style then applies to whatever words end up on the line. The properties that pick a face or a size change widths. So do font-variant-caps, letter-spacing, and text-transform.
font-size grows the line around the baseline the paragraph shares. color and the rules text-decoration draws change no widths. Lines break the same way with them or without them.
Cross-references
A rulebook, a table of contents, and an index all refer the reader to other pages. You can print the page number of the element that a link goes to, or the text of that element. That element is the target of the link. A reference is a target-counter() or a target-text() in content.
The target is a heading, or an element with an id. In markdown, a link names a heading after a #, by its slug or by its text. A link can also name a heading in another source. See the markdown mapping for the rules.
## The Hunter
For the rules on tracking game, see [the hunter](#the-hunter).The following example prints the page number after every link. When the heading is on page 12, the text above prints as “see the hunter (page 12)”.
a::after { content: " (page " target-counter(attr(href url), page) ")";}target-counter() takes these arguments:
- The target.
attr(href url)is the url of the link. A string such as"#glossary"names the element with the written idglossary. It names the same target for every element that the rule selects. - The counter.
pageis the only counter that a reference can print. - The counter style, which is optional. It is one of the counter styles that
counter(page)takes. The default isdecimal.
The number is the page number on the page of the target. If a counter-reset restarts the count before the target, the reference prints the number from the restarted count.
target-text() takes only the target. It prints the text of a heading or a paragraph, or the description of an image. The following example prints the text of the target after each link with the class see:
a.see::after { content: " (" target-text(attr(href url)) ")";}A reference prints only in ::before and ::after. If a reference names a source, a heading, or an id that the book does not have, the pseudo-element prints nothing. The engine warns and names the line and column of the link. The engine does the same for attr(href url) on an element that is not a link. A reference to a web address, such as https://example.com, prints nothing, and the engine does not warn.
Two layouts
If a book prints a page number with target-counter(), the engine lays out the book twice. In the first layout, the engine finds the page of each target. In the second layout, it prints those page numbers. The engine lays out a book with no target-counter() once. target-text() does not add a second layout.
A printed number can change the width of its line. A line that changes width can move a target to another page. If the second layout moves a target, the reference prints the page from the first layout. The engine warns and names the target and both pages.
Fonts
@font-face registers a font under the family name the stylesheet gives it.
The following example loads a font file and uses it for the body text:
@font-face { font-family: "Author Serif"; src: url("faces/author.ttf") format("truetype"), local("Author Serif"); font-style: normal; font-weight: 400;}
book { font-family: "Author Serif", serif;}| descriptor | example |
|---|---|
font-family | font-family: "Author Serif" |
font-style | font-style: italic |
font-weight | font-weight: bold |
src | src: url(fonts/serif.otf) |
font-family: <family-name>;font-style: normal | italic | oblique;font-weight: normal | bold | <number [1,1000]>;src: [ <url> format(<string>)? | local(<string>) ]#;The string in url() goes to the host’s loader unchanged and does not have to be a real URL. A font that loads registers under the family name the stylesheet gave it, not the name inside the file. A font that fails to load warns, and the text falls back to the next family in the list.
The engine tries each family in font-family in order and uses the first one registered. Within a family it matches style before weight, and it synthesizes neither. See fonts for the full matching rules and how variable fonts register.
The built-in stylesheet
The complete stylesheet that any additional CSS cascades over.
/* Fleuron's default stylesheet: a trade paperback at 6x9 inches, with mirrored margins and a page number at the bottom. Any additional CSS cascades over every rule here. */
book { font-family: serif; font-size: 11pt; line-height: 1.4; text-align: left; hyphens: none; orphans: 2; widows: 2;}
/* A chapter opens on a right-hand page. Naming its pages lets @page tell an opening page from a continuation. */section { page: chapter; break-before: recto; counter-reset: note;}
h1, h2, h3, h4, h5, h6 { font-size: 18pt; break-after: avoid;}
/* An indent separates one paragraph from the next, so it belongs only to a paragraph that follows another one. A paragraph after a heading, a thematic break, or an image starts flush. */p + p { text-indent: 1.2em;}
/* A quotation is narrower than the surrounding text and set off from it. */blockquote { margin: 1em 2em;}
/* A code block has space above and below it, and a monospace family. The bundled family is the only face a book gets without @font-face, so a book that wants a monospace face declares one. */pre { font-family: monospace; margin: 1em 0;}
/* A list has space above and below it. Its markers are in the indent to the left of its items. */ul, ol { margin: 0.5em 0; padding-left: 1.5em;}
ol { list-style-type: decimal;}
/* A list inside an item has no space above or below it, and its markers are open circles. */li > :is(ul, ol) { margin: 0;}
li > ul { list-style-type: circle;}
/* An item of a loose list holds paragraphs, with space between them and no indent. The items of a tight list have no space between them. */li > p { margin: 0.5em 0; text-indent: 0;}
/* A thematic break is a centered ornament. It belongs between paragraphs, so it never opens or closes a page. */hr { content: "\2766"; text-align: center; margin: 1em 0; break-before: avoid; break-after: avoid;}
/* The notes of a page stand at the foot of its content box, under a rule. */notes { margin-top: 1em; padding-top: 0.4em; border-top: thin solid;}
/* A note is set smaller than the text, and starts flush in the letters the book is set in, wherever its reference was written. The number is the reference in the line and the mark beside the note, and it hangs in the indent to the left of the note. */note { font-size: 9pt; line-height: 1.2; font-style: normal; font-weight: normal; text-indent: 0; list-style-type: decimal; padding-left: 1.5em; margin-top: 0.4em;}
/* A page break and a column break hold no text. The content after one starts on a new page or in the next column. */pagebreak { break-after: page;}
columnbreak { break-after: column;}
/* A section's opening heading is its chapter title. Storing it as a named string lets a running head pick it up with string(chapter). */section > :is(h1, h2, h3, h4, h5, h6):first-child { string-set: chapter content();}
/* A table has a rule above it, a rule below it, and a rule under its header row. Neighboring cells share one border. */table { margin: 1em 0; border-collapse: collapse; border-top: thin solid; border-bottom: thin solid;}
/* The text of a cell starts at the left of the cell, even where the prose around the table is justified. Padding keeps the text off the rules. */th, td { padding: 0.25em 0.5em; text-align: left;}
th { font-weight: bold; border-bottom: thin solid;}
em { font-style: italic }strong { font-weight: bold }code { font-family: monospace }s { text-decoration: line-through }
@page { size: 432pt 648pt; margin: 54pt 42pt 54pt 54pt; @bottom-center { content: counter(page); font-size: 9pt; line-height: 1.4; }}
/* Margins mirror across the spread, so the wider one is always at the spine. */@page :left { margin-left: 42pt; margin-right: 54pt;}
@page :right { margin-left: 54pt; margin-right: 42pt;}
/* A chapter's opening page counts toward the numbering but does not show its number. */@page chapter:first { @bottom-center { content: none }}CSS in an EPUB
Fleuron can also write a book as a reflowable EPUB. See EPUB. The EPUB has one stylesheet, made from the same stylesheets as the PDF. It holds the CSS on this page, less the CSS that describes pages.
HTML has no book element and no note element. So in the EPUB, a rule for book selects body, and a rule for note selects aside.
The alignment that the markdown gives a column of a table is a rule of its own. It comes after the built-in rules and before the author rules. So an author rule for the cells overrides it, as for a PDF.
What the EPUB leaves out
The reading system makes the pages. CSS that describes a page has nothing to act on there. Fleuron leaves out the rules, declarations, and selectors below.
@page rules, with their margin boxes. The reading system chooses the size and the margins of the page.
Declarations that control where pages and columns break:
page break-before break-after break-inside orphans widowsbox-decoration-break column-spanDeclarations that print running heads and page numbers:
string-set counter-reset content: target-counter()Declarations that put a box against the page:
position: absolute wrap-flow shape-outside shape-marginSelectors that name notes, pagebreak, or columnbreak. These elements exist only on a page.
Fleuron leaves out this CSS with no warning, from the built-in stylesheet and from each author stylesheet. The same stylesheet makes the PDF, and the PDF uses this CSS.
CSS that fleuron does not support yet warns as it does for a PDF, with the line and the column. Fleuron leaves it out of the EPUB too.
A reading system uses the CSS that it supports. The support is different from one reading system to the next. So a declaration in the EPUB can have no effect on some devices.
Not in the subset
| area | not supported yet |
|---|---|
| layout | floats, grid, flexbox, transforms, min-width, max-width and max-height on anything but an image, align-content on a block, and the align-content values other than start, center, and end |
| lists | list-style-position, list-style-image, the list-style shorthand, ::marker, a string as a marker, and counter(list-item) |
| tables | column widths measured from the content of the cells, colspan, rowspan, border-spacing, vertical-align, and caption-side |
| table boxes | a border or a background on thead or tbody, and width on anything but an image or a cell of the first row |
| positioning | position: fixed and position: sticky, the inset shorthand, wrap-flow: clear, minimum and maximum, a width or a height on a positioned block, and position, z-index or opacity on an inline element |
| contours | shape-outside: circle(), ellipse(), inset(), path(), url() and the <box> keywords, shape-image-threshold, and a contour on an image you leave among the paragraphs |
| code blocks | white-space, tab-size, and syntax highlighting |
| at-rules | media queries |
| custom properties | a custom property declared in @page or in a margin box, var() in @font-face, and the @property rule |
| counters | any counter other than page and note, and counter-increment |
| footnotes | ::footnote-call and ::footnote-marker, a note set in the margin or at the end of the book, float: footnote, a rule that reaches the area of one page rather than every page, a note inside a block against the page, and vertical-align, which raises a reference in a font with no superior figures |
| generated content | ::before and ::after on the book, a table row, a table cell, thead, or tbody, position: absolute on ::before and ::after of an inline element, target-counters(), the second argument of target-text(), and attr() outside a reference |
| fonts | the font-variant shorthand, font-variant-east-asian, font-variant-position, font-variation-settings, font-kerning, font-synthesis, the functional values of font-variant-alternates, such as styleset(), and the @font-feature-values rule |
| text decorations | text-decoration-skip-ink, text-underline-offset, text-underline-position, text-emphasis, text-shadow, and the dotted, dashed and wavy values of text-decoration-style |
| backgrounds | the background shorthand, background-attachment, background-clip, background-origin, more than one layer in a background, and background-repeat: repeat-x, repeat-y, space and round |
| borders | longhands naming both an edge and a value, such as border-top-width |
| inline boxes | a margin on an inline element, and a border or padding on @page |
| columns | column-fill, column-rule-color, the columns and column-rule shorthands, columns on any element, and column-span on a block inside a quotation or a table cell |
Two of those have visible consequences today. The last page fills its columns in order and stops short rather than balancing them. A column rule uses the book’s text color.
The engine reports each unsupported declaration on its own, naming the line and column it was written at, and the rest of the stylesheet still applies. See diagnostics for how to read a warning. These are properties the engine does not support yet, not ones it refuses.
The subset as data
fleuron --css-subset writes this page’s vocabulary as JSON: every selector, property, value, and at-rule, with the engine version it came from. A host that embeds the engine can pin that file for its version and drive its own editor from it. The library exposes the same description as fleuron::style::subset::Subset::describe(), and the tables on this page are generated from it.
The fleuron npm package ships the description as well, under the name SUBSET. It describes the engine in the module it is read from. A host reads it with no book laid out and no file of its own. SUBSET.version is the engine version the description came from.
The following example lists every property a style rule takes, with the values it accepts:
import { SUBSET } from 'fleuron';
for (const property of SUBSET.properties) { console.log(`${property.name}: ${property.syntax}`);}