SkillAgentSearch skills...

Hyphen

⌨️ WYSIWYG markdown editor library for Compose Multiplatform. Live formatting, keyboard shortcuts, clipboard, and undo/redo history.

Install / Use

/learn @DenserMeerkat/Hyphen

README

Hyphen

<picture> <source srcset="assets/images/banner.jpg"> <img alt="Hyphen — WYSIWYG Markdown editor for Compose Multiplatform" src="docs/images/banner.jpg"> </picture> <br> <p align="center"> A <strong>WYSIWYG Markdown editor</strong> for Compose Multiplatform.<br> Type in Markdown, see formatting live. Copy as Markdown. Works on Android, Desktop, and Web. </p> <p align="center"> <a href="https://github.com/densermeerkat/hyphen/releases"><img alt="Maven Central" src="https://img.shields.io/maven-central/v/io.github.densermeerkat/hyphen?color=4CAF50&label=Maven%20Central"></a> <a href="https://kotlinlang.org"><img alt="Kotlin" src="https://img.shields.io/badge/Kotlin-2.2.21-7F52FF?logo=kotlin&logoColor=white"></a> <a href="https://www.jetbrains.com/compose-multiplatform/"><img alt="Compose Multiplatform" src="https://img.shields.io/badge/Compose%20Multiplatform-1.10.1-4285F4?logo=jetpackcompose&logoColor=white"></a> <img alt="Platforms" src="https://img.shields.io/badge/Platforms-Android%20%7C%20Desktop%20%7C%20Web-orange"> </p> <p align="center"> <a href="https://densermeerkat.github.io/hyphen/"><strong>→ Try the live web demo</strong></a> </p> <picture> <source media="(prefers-color-scheme: dark)" srcset="assets/images/demo_dark.png"> <source media="(prefers-color-scheme: light)" srcset="assets/images/demo_light.png"> <img alt="Hyphen editor Demo screenshot" src="assets/images/demo_light.png" width="100%"> </picture>

Features

✍️ Live Markdown Input

Type Markdown syntax directly and watch it convert as you write — no mode switching, no preview pane required.

| Syntax | Style | |-------------------------|----------------------| | **text** | Bold | | *text* | Italic | | __text__ | Underline | | [text](url) | Link | | `text` | Inline code | | ~~text~~ | ~~Strikethrough~~ | | ==text== | Highlight | | # at line start | Heading 1 | | ## at line start | Heading 2 | | ### at line start | Heading 3 | | #### at line start | Heading 4 | | ##### at line start | Heading 5 | | ###### at line start | Heading 6 | | - at line start | Bullet list | | 1. at line start | Ordered list | | > at line start | Blockquote | | - [ ] at line start | Checkbox (unchecked) | | - [x] at line start | Checkbox (checked) |

📋 Markdown Clipboard

Cut, copy, and paste all work across Android, Desktop, and Web. Copying a selection serializes it to Markdown automatically, paste into any Markdown-aware editor and all formatting travels with it.

⌨️ Keyboard Shortcuts

Full hardware keyboard support on Desktop and Web:

| Shortcut | Action | |--------------------------|---------------------------------| | Ctrl / Cmd + B | Toggle bold | | Ctrl / Cmd + I | Toggle italic | | Ctrl / Cmd + U | Toggle underline | | Ctrl / Cmd + Shift + S | Toggle strikethrough | | Ctrl / Cmd + Shift + X | Toggle strikethrough | | Ctrl / Cmd + Alt + X | Toggle strikethrough | | Ctrl / Cmd + Shift + H | Toggle highlight | | Ctrl / Cmd + Space | Clear all styles on selection | | Ctrl / Cmd + 1 | Toggle Heading 1 | | Ctrl / Cmd + 2 | Toggle Heading 2 | | Ctrl / Cmd + 3 | Toggle Heading 3 | | Ctrl / Cmd + 4 | Toggle Heading 4 | | Ctrl / Cmd + 5 | Toggle Heading 5 | | Ctrl / Cmd + 6 | Toggle Heading 6 | | Ctrl / Cmd + Enter | Toggle checkbox on current line | | Ctrl / Cmd + K | Toggle link on selection | | Ctrl / Cmd + Z | Undo | | Ctrl / Cmd + Y | Redo | | Ctrl / Cmd + Shift + Z | Redo |

↩️ Undo / Redo History

Granular history with snapshots saved at word boundaries, pastes, and Markdown conversions. The redo stack is maintained correctly across all operations, including toolbar toggles and programmatic edits.

🌍 Compose Multiplatform

Single shared implementation targeting Android, Desktop (JVM), and Web (WasmJS / JS).


Installation

Using libs.versions.toml (recommended)

Add the version and library entry to your version catalog:

gradle/libs.versions.toml

[versions]
hyphen = "0.4.0-alpha01"

[libraries]
hyphen = { group = "io.github.densermeerkat", name = "hyphen", version.ref = "hyphen" }

Then reference it in your shared module:

shared/build.gradle.kts

kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation(libs.hyphen)
        }
    }
}

commonMain is the source set that compiles for every target at once — Android, Desktop, and Web. Declaring Hyphen there means you write the dependency once and all platforms pick it up automatically.

Using string notation

// shared/build.gradle.kts
kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("io.github.densermeerkat:hyphen:0.4.0-alpha01")
        }
    }
}

Quick Start

val state = rememberHyphenTextState(
    initialText = "**Hello**, *Hyphen*!"
)

HyphenTextField(
    state = state,
    label = { Text("Notes") },
)

// Read the result at any time
val markdown = state.toMarkdown()

Choosing an Editor Component

Hyphen ships two editor composables. Use whichever fits your design:

HyphenBasicTextEditor

A thin wrapper around BasicTextField with no decoration. Use this when you control the layout yourself or want full design freedom.

HyphenBasicTextEditor(
    state = state,
    modifier = Modifier
        .fillMaxWidth()
        .padding(16.dp),
    textStyle = TextStyle(
        fontSize = 16.sp,
        color = MaterialTheme.colorScheme.onSurface,
    ),
    cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
    onMarkdownChange = { markdown -> /* sync to ViewModel */ },
)

HyphenTextField (Material 3)

Wraps HyphenBasicTextEditor inside a standard Material3 filled text field decorator — labels, placeholder, leading/trailing icons, supporting text, and error state all work out of the box.

HyphenTextField(
    state = state,
    label = { Text("Notes") },
    placeholder = { Text("Start typing…") },
    trailingIcon = { Icon(Icons.Default.Edit, contentDescription = null) },
    supportingText = { Text("Markdown supported") },
    modifier = Modifier.fillMaxWidth(),
)

Both composables accept the same styleConfig, onTextChange, onMarkdownChange, and clipboardLabel parameters. The Material3 variant additionally accepts colors, shape, labelPosition, contentPadding, and all standard decoration slots.


Usage

Toolbar buttons — keeping focus on Desktop & Web

On Desktop and Web, clicking a button moves keyboard focus away from the editor. This causes the text selection to be lost before the style toggle runs. Fix this by adding focusProperties { canFocus = false } to every toolbar button so focus never leaves the editor when a button is tapped:

IconToggleButton(
    checked = state.hasStyle(MarkupStyle.Bold),
    onCheckedChange = { state.toggleStyle(MarkupStyle.Bold) },
    modifier = Modifier.focusProperties { canFocus = false }, // ← required on Desktop & Web
) {
    Icon(Icons.Default.FormatBold, contentDescription = "Bold")
}

This applies to any clickable element in your toolbar — IconButton, Button, IconToggleButton, etc.

Custom style config

HyphenBasicTextEditor(
    state = state,
    styleConfig = HyphenStyleConfig(
        boldStyle = SpanStyle(
            fontWeight = FontWeight.ExtraBold,
            color = Color(0xFF1A73E8),
        ),
        highlightStyle = SpanStyle(
            background = Color(0xFFFFF176),
        ),
        inlineCodeStyle = SpanStyle(
            background = Color(0xFFF1F3F4),
            fontFamily = FontFamily.Monospace,
            color = Color(0xFFD93025),
        ),
    ),
)

Programmatic control

// Load new Markdown content (resets undo history)
state.setMarkdown("# New content\n\nHello!")

// Toggle formatting from a custom button
Button(onClick = { state.toggleStyle(MarkupStyle.Bold) }) { Text("B") }

// Remove all inline formatting from the current selection
Button(onClick = { state.clearAllStyles() }) { Text("Clear") }

// Undo / redo
state.undo()
state.redo()

Reactive observation

// Callback — fires on every text or formatting change
HyphenBasicTextEditor(
    state = state,
    onMarkdownChange = { markdown -> viewModel.onContentChanged(markdown) },
)

// Flow — collect anywhere, debounce freely
viewModelScope.launch {
    state.markdownFlow
        .debounce(500)
        .collect { markdown -> repository.save(markdown) }
}

API Reference

HyphenBasicTextEditor

| Parameter | Type | Default | Description | |:--------------------|:----------------------------|:------------------------------|:------------------------------------------------------------------------------------------------| | state | HyphenTextState | Required | Holds text content, spans, selection, and undo/redo history. | | modifier | Modifier

View on GitHub
GitHub Stars39
CategoryDevelopment
Updated1d ago
Forks0

Languages

Kotlin

Security Score

95/100

Audited on Mar 30, 2026

No findings