Skip to content

HighlightTheme

HighlightTheme maps hljs-* token classes to Compose SpanStyle values used by the highlight pipeline.

Full API in Dokka:

When to use each theme source

  • Built-in themes (tomorrow, tomorrowNight, atomOneDark, atomOneLight, githubLight, githubDark, draculaDark, alucardLight) and their aliases: fastest setup, precompiled maps, no CSS parsing at runtime.
  • fromAsset(...): best for shipping a highlight.js CSS file with your app.
  • fromCss(...): useful when CSS comes from network, config, or generated text.
  • fromColorMap(...): best when you want full programmatic control, for example Material 3 dynamic color integration.

Use remember*Theme() helpers inside composables so the theme instance stays stable across recompositions.

import dev.hossain.highlight.ui.HighlightThemeProvider
import dev.hossain.highlight.ui.rememberTomorrowNightTheme
import dev.hossain.highlight.ui.rememberTomorrowLightTheme

HighlightThemeProvider(
    lightHighlightTheme = rememberTomorrowLightTheme(),
    darkHighlightTheme  = rememberTomorrowNightTheme(),
) { /* your content */ }

Other bundled pairs now available:

  • rememberGithubLightTheme() + rememberGithubDarkTheme()
  • rememberAlucardLightTheme() + rememberDraculaDarkTheme() (or aliases rememberDraculaLightTheme() + rememberAlucardDarkTheme())

Theme discovery and persistence

When building user-customizable theme pickers (e.g. LazyRow, DropdownMenu) or storing user theme preferences in DataStore or SharedPreferences, use the discovery APIs on HighlightTheme.Companion:

  • HighlightTheme.bundled: List of all 8 bundled theme descriptors in canonical order.
  • HighlightTheme.bundledLight: Precomputed list filtered to light themes (isLight == true).
  • HighlightTheme.bundledDark: Precomputed list filtered to dark themes (isDark == true).
  • HighlightTheme.findBundledById(id): O(1) lookup returning the matching HighlightThemeDescriptor? by stable ID.

Each HighlightThemeDescriptor provides metadata:

  • id: Stable string identifier ("tomorrow", "tomorrow-night", "atom-one-light", "atom-one-dark", "github", "github-dark", "alucard", "dracula").
  • displayName: Human-readable name for UI presentation ("Tomorrow", "Tomorrow Night", "Dracula", etc.).
  • isDark / isLight: Mode indicators for filtering or matching the current system theme.
  • theme / create(): Lazily created and cached HighlightTheme instance.

Building a theme picker in Compose

LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
    items(HighlightTheme.bundledDark) { descriptor ->
        ThemeChip(
            label = descriptor.displayName,
            selected = selectedThemeId == descriptor.id,
            onClick = { onThemeSelected(descriptor.id) },
        )
    }
}

Persisting and restoring themes

Save descriptor.id into your persistence store. On app launch or restore, look up the descriptor and safely fall back if the ID is unrecognized or missing:

val savedId = preferences.getString("theme_id", null) ?: "tomorrow"
val activeTheme = HighlightTheme.findBundledById(savedId)?.create()
    ?: if (isSystemInDarkTheme()) HighlightTheme.tomorrowNight()
    else HighlightTheme.tomorrow()

Custom theme from asset CSS

import androidx.compose.ui.platform.LocalContext
import dev.hossain.highlight.engine.HighlightTheme
import dev.hossain.highlight.ui.HighlightThemeProvider

val appContext = LocalContext.current.applicationContext
val theme = HighlightTheme.fromAsset(
    context   = appContext,
    assetPath = "themes/github.css",
    name      = "github",
)

HighlightThemeProvider(lightHighlightTheme = theme) { /* your content */ }

Note

fromAsset() is lazy. CSS parsing happens on first theme usage, not at factory call time. Only lightHighlightTheme is set here, so dark mode keeps the default Tomorrow Night theme.

Custom theme from raw CSS

val theme = HighlightTheme.fromCss(
    cssText = css,
    name    = "my-runtime-theme",
)

Both fromAsset() and fromCss() support standalone .hljs / .hljs-* and compound .hljs-x.y selectors, ¾/6/8-digit hex colors (#RRGGBBAA), comma- and space-separated rgb() / rgba(), 21 CSS named colors, font-weight (bold, bolder, normal, lighter, 100-900), font-style (italic, oblique), and !important. At-rules (@media), descendant/sibling combinators, pseudo-selectors, var(--*) custom properties, and non-SpanStyle properties (text-decoration, opacity) are ignored.

Custom theme from a color map

import dev.hossain.highlight.engine.HljsSelectors
import dev.hossain.highlight.engine.HighlightTheme

val colorMap = mapOf(
    HljsSelectors.BASE     to SpanStyle(color = Color(0xFF24292E), background = Color(0xFFFFFFFF)),
    HljsSelectors.KEYWORD  to SpanStyle(color = Color(0xFFD73A49), fontWeight = FontWeight.Bold),
    HljsSelectors.STRING   to SpanStyle(color = Color(0xFF032F62)),
    HljsSelectors.COMMENT  to SpanStyle(color = Color(0xFF6A737D), fontStyle = FontStyle.Italic),
)
val theme = HighlightTheme.fromColorMap(
    name             = "my-dynamic-theme",
    colorMap         = colorMap,
    backgroundColor  = Color(0xFFFFFFFF),
    defaultTextColor = Color(0xFF24292E),
)

Theme identity behavior

HighlightTheme equality is based on both name and content identity. This gives stable memoization while still triggering re-highlighting when CSS content changes.

  • Same name and same CSS content -> equal.
  • Same name and different CSS content -> not equal.
  • Different names -> not equal.
val light = HighlightTheme.fromCss(lightCss, "custom")
val dark  = HighlightTheme.fromCss(darkCss,  "custom")
light == dark  // false

val a = HighlightTheme.fromCss(css, "custom")
val b = HighlightTheme.fromCss(css, "custom")
a == b         // true

Common pitfalls

  • Passing activity context to long-lived themes in non-Compose layers.
  • Expecting fromAsset() parse failures at construction time rather than first use.
  • Recreating custom themes every recomposition instead of remembering stable instances.