Styling¶
Each presentation has its own CSS. You can attach styles either as a raw string or via the kotlinx.css DSL — and you can mix the two on the same presentation. For brand-level styling, start with the typed customTheme {} block below before reaching for raw CSS.
Custom themes¶
"Make it look like our brand" doesn't require reverse-engineering reveal.js theme variables. The customTheme {} block (in presentationConfig {}, globally or per presentation, cascading like every other config) layers typed overrides on top of a stock theme:
fun customTheming() {
kslides {
presentationConfig {
customTheme {
baseTheme = PresentationTheme.WHITE // start from a stock theme
backgroundColor = Color("#f5f5f5")
mainColor = Color("#1a1a2e")
headingColor = Color("#0f4c81")
headingTextTransform = TextTransform.none // keep headings as written
linkColor = Color("#0f4c81")
headingFont = "Inter, sans-serif"
codeFont = "JetBrains Mono, monospace"
customProperty("--r-heading-letter-spacing", "0.02em") // unmodeled vars pass through
logo("assets/logo.svg", position = LogoPosition.TOP_RIGHT, size = 80.px)
}
}
presentation {
markdownSlide { content { "# On brand" } }
}
}
}
Each property maps to one of the CSS custom properties (--r-*) that reveal.js themes expose, and only the properties you assign are emitted — as a <style id="custom-theme"> block after the base theme's stylesheet, so your values win the cascade while css {} rules and slides.css can still override them. The overrides also apply in ?print-pdf view and PDF export.
A few details worth knowing:
baseThemepicks the stock theme to start from and takes precedence overtheme— it also drives theme-derived behavior like Mermaid's dark/light selection.logo()pins a brand image to a corner of every slide (and every exported PDF page). Without anhrefit ignores pointer events, so it never blocks slide interaction. A relativesrcresolves against the output root, so the same path works from a deck at any depth; absolute, external, anddata:values are used as written.headingTextTransformcontrols the stock themes' forced UPPERCASE headings. Eleven of the fourteen bundled themes set--r-heading-text-transform: uppercase(all butdracula,night, andserif), so headings render shouting no matter how you typed them —TextTransform.nonegets back exactly what you wrote.customProperty("--r-…", …)passes through any reveal.js theme variable the DSL doesn't model. Values are emitted verbatim, so they are checked for characters that would end the declaration (;,{,},<) and rejected at the assignment site rather than corrupting the stylesheet.
The theme example deck shows the result, with its own customTheme {} source on the second slide.
Raw CSS¶
fun stringCss() {
kslides {
presentation {
css +=
"""
h1 { color: gold; }
.slides { font-family: "Inter", sans-serif; }
"""
markdownSlide { content { "# Gold heading" } }
}
}
}
Anything that's valid CSS works. The string is appended to the presentation's stylesheet verbatim.
Whitespace matters
CSS in kslides content is space-sensitive. If you have a build step that auto-formats generated HTML, exclude the kslides output directory.
kotlinx.css DSL¶
fun dslCss() {
kslides {
presentation {
css {
rule("h1") {
color = Color.deepPink
fontWeight = FontWeight.bold
}
}
markdownSlide { content { "# Pink heading" } }
}
}
}
The DSL is type-safe and refactor-friendly — handy when sharing styles across presentations.
Your own stylesheet file¶
For CSS you'd rather keep in a file than inline, add it to cssFiles. Say where it lives with
origin — the default looks inside the bundled reveal.js asset directory, which is rarely what you
want for your own file:
kslidesConfig {
cssFiles += CssFile("css/site.css", origin = AssetOrigin.OUTPUT_ROOT)
jsFiles += JsFile("js/site.js", origin = AssetOrigin.OUTPUT_ROOT)
}
OUTPUT_ROOT publishes the file alongside your decks and resolves it from whatever depth a deck
sits at — see asset paths. A site-root-absolute /css/site.css also works
but breaks if you publish under a path prefix, which a GitHub Pages project site does.
Targeting specific slides¶
Set an id on the slide and write a rule against it:
markdownSlide {
id = "title"
content { "# Hello" }
}
// then
css += """
#title h1 { font-size: 4em; }
"""
htmlSlide exposes classes for the same purpose — see HTML slides.
Font sizes¶
Slide and code font sizes are plain config values — no CSS required. All three cascade from kslides { } → presentation { } → slide, same as any other configuration property:
presentationConfig {
slideConfig {
codeFontSize = "0.60em" // default for all slides
codeWrap = true // wrap long code lines
}
}
markdownSlide {
slideConfig {
fontSize = "34px" // this slide only
codeFontSize = "0.40em"
}
}
fontSize— font size for all content on the slide (any CSS length). Themes size headings inem, so everything scales together.codeFontSize— font size for code blocks (reveal.js's default is0.55em).codeWrap— whentrue, long code lines wrap instead of overflowing horizontally. A slide can setfalseto override a presentation-widetrue.
These values are interpolated into generated CSS, so they are checked where you assign them: a malformed length like "0.6em;}" throws IllegalArgumentException naming the property rather than silently breaking every rule after it. Units, a bare 0, calc()/var()/clamp() expressions, and a blank "unset" are all accepted.
The generated codeFontSize/codeWrap rules (fontSize renders as an inline style, not a head rule) are emitted into the document head after any presentation css additions, so on an equal-specificity tie the config-driven value wins over legacy hand-written rules.
fontSize and codeFontSize compound¶
Code blocks are content, so fontSize scales them too — and it does so before codeFontSize applies. fontSize renders as an inline font-size on the slide's <section>; <pre> sits inside that section, so an em code size resolves against the scaled section rather than the theme base, and the two multiply. On a 42px theme:
| Config | <section> |
rendered <pre> |
|---|---|---|
| neither set | 42px | 0.55 × 42 = 23.1px |
fontSize = "0.65em" |
27.3px | 0.55 × 27.3 = 15.0px |
fontSize = "0.65em" + codeFontSize = "0.60em" |
27.3px | 0.60 × 27.3 = 16.4px |
Setting codeFontSize = "0.55em" to "restore the default" is therefore a no-op — it is already the default, in the same relative units. To size code independently of fontSize, give it an absolute unit; px is immune to the section's em and still scales with the deck, since reveal.js zooms the whole canvas with a CSS transform:
slideConfig {
fontSize = "0.65em"
codeFontSize = "23px" // reveal.js's default rendered size, unaffected by fontSize
}
Or divide the factor out and stay in em: under fontSize = "0.65em", codeFontSize = "0.85em" lands back at roughly the 0.55em default.
Code font size¶
reveal.js renders code blocks — Markdown fences, code snippets, and htmlSlide <pre> blocks — at 0.55em relative to the slide's <section>, which fontSize scales. Because the slide canvas is a fixed size, long lines overflow with a scrollbar instead of shrinking to fit. For most cases, codeFontSize above is simpler; override .reveal pre directly when you need a selector the config API doesn't expose.
Every code block¶
Attach the rule to the kslides {} block (or a single presentation {}) so it applies everywhere:
fun globalCodeFont() {
kslides {
// Seeded into every presentation. reveal.js sizes code at 0.55em by default; lower
// ".reveal pre" so long lines fit the slide, or raise it to enlarge all code blocks.
css +=
"""
.reveal pre { font-size: 0.4em; }
"""
presentation {
markdownSlide {
content {
"""
```kotlin
val answer = 42
```
"""
}
}
}
}
}
Lower the value until the widest line fits; raise it to enlarge all code. To keep the rare over-long line inside the window instead of scrolling it, add .reveal pre code { white-space: pre-wrap; }.
One slide only¶
Give the slide an id and scope the rule to it. An id selector (#bigcode pre) outranks the global .reveal pre, so it wins with no !important and independent of order:
fun perSlideCodeFont() {
kslides {
presentation {
// "#bigcode pre" is an id selector, so it outranks the global ".reveal pre" rule and
// only this slide's code is enlarged. No !important needed.
css +=
"""
#bigcode pre { font-size: 0.7em; }
"""
markdownSlide {
id = "bigcode"
content {
"""
```kotlin
val answer = 42
```
"""
}
}
}
}
}
Several slides¶
An id must be unique per page — reveal.js uses it for deep links (#/bigcode) and the slide menu, so the same id can't be reused on two slides. Use a class instead; a class is made to be shared, so one rule styles every slide tagged with it:
fun sharedCodeFont() {
kslides {
// Shrink all code globally...
css +=
"""
.reveal pre { font-size: 0.4em; }
"""
presentation {
// ...then enlarge it on a subset of slides. An id must be unique, but a class can be reused.
// ".reveal .big pre" has two classes, so it outranks the global ".reveal pre" on specificity
// and wins no matter which rule is declared first.
css +=
"""
.reveal .big pre { font-size: 0.7em; }
"""
markdownSlide {
classes = "big"
content {
"""
```kotlin
val first = 1
```
"""
}
}
markdownSlide {
classes = "big"
content {
"""
```kotlin
val second = 2
```
"""
}
}
}
}
}
markdownSlide, htmlSlide, and dslSlide expose classes directly, and slideDefinition accepts a classes argument for the same purpose.
Qualify with .reveal to beat the global rule
A bare .big pre has the same specificity as the global .reveal pre, so the tie is broken by whichever is declared last — reorder the blocks and the override silently stops working. Writing .reveal .big pre (two classes) always outranks .reveal pre, so it wins regardless of order. A single-slide id selector (#bigcode pre) already outranks both and needs no qualifier.