Skip to content

Zebra Stripes

This example shows how to build a ViewPlugin that applies alternating line decorations — a common pattern for line-level styling.

The approach

Use Decoration.line() with a SpanStyle background color, applied through a ViewPlugin that rebuilds decorations when the document changes.

Full implementation

// Dark-theme stripe color matching CodeMirror reference (#34474788)
private val stripe = Decoration.line(
    LineDecorationSpec(style = SpanStyle(background = Color(0x88344747)))
)

private fun buildStripes(doc: Text, step: Int): DecorationSet {
    val builder = RangeSetBuilder<Decoration>()
    for (i in 1..doc.lines) {
        if (i % step == 0) {
            val line = doc.line(LineNumber(i))
            builder.add(line.from, line.from, stripe)
        }
    }
    return builder.finish()
}

private class ZebraPlugin(
    session: EditorSession,
    private val step: Int
) : PluginValue, DecorationSource {
    override var decorations: DecorationSet = buildStripes(session.state.doc, step)

    override fun update(update: ViewUpdate) {
        if (update.docChanged || update.viewportChanged) {
            decorations = buildStripes(update.state.doc, step)
        }
    }
}

private val zebraPlugin = ViewPlugin.fromDecorationSource { session ->
    ZebraPlugin(session = session, step = 2)
}

Using it

val session = rememberEditorSession(
    doc = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6",
    extensions = basicSetup + zebraStripes
)
KodeMirror(session = session)

Even-numbered lines get a subtle gray background.

Configurable step

Make the stripe interval configurable using a Facet:

val stripeFacet = Facet.define(
    combine = { values: List<Int> -> values.firstOrNull() ?: 2 }
)

fun zebraStripes(step: Int = 2): Extension =
    stripeFacet.of(step) + ViewPlugin.fromClass(::ConfigurableZebraPlugin).asExtension()

class ConfigurableZebraPlugin(view: EditorSession) : PluginValue, DecorationSource {
    private var step = view.state.facet(stripeFacet)
    override var decorations: DecorationSet = computeDecorations(view)
        private set

    override fun update(update: ViewUpdate) {
        val newStep = update.state.facet(stripeFacet)
        if (update.docChanged || newStep != step) {
            step = newStep
            decorations = computeDecorations(update.view)
        }
    }

    private fun computeDecorations(view: EditorSession): DecorationSet {
        val builder = RangeSetBuilder<Decoration>()
        val doc = view.state.doc

        for (i in 1..doc.lines) {
            if (i % step == 0) {
                val line = doc.line(i)
                builder.add(line.from, line.from, stripeDecoration)
            }
        }

        return builder.finish()
    }
}

Key concepts

  • Decoration.line() creates a line-level decoration (applied to the whole line, not a character range). In Kodemirror, use SpanStyle for styling instead of CSS classes.
  • RangeSetBuilder builds a sorted range set. Ranges must be added in ascending from order.
  • ViewPlugin.fromClass() wraps a class that implements both PluginValue and DecorationSource, automatically wiring up the decorations property.
  • DecorationSource is the Compose alternative to the upstream decorations plugin spec option.

Based on the CodeMirror Zebra Stripes example.