Headings¶
The Rich Text Editor models headings as a first-class paragraph field, not as a visual style fingerprint. Every paragraph carries a HeadingStyle (levels Normal and H1..H6), so heading identity survives theme changes, font customization, and round-trips through HTML and Markdown.
Basic Usage¶
Applying a heading¶
// Make the current paragraph an H2
richTextState.setHeadingStyle(HeadingStyle.H2)
// Remove heading level (back to a normal paragraph)
richTextState.setHeadingStyle(HeadingStyle.Normal)
setHeadingStyle applies to every paragraph that intersects the current selection. Wrap the call in recordHistory automatically so undo/redo restores heading changes alongside other formatting.
Reading the current level¶
val current = richTextState.currentHeadingStyle
val isH1 = current == HeadingStyle.H1
val level = current.level // 0 for Normal, 1..6 for H1..H6
Use this from toolbar code to highlight the active heading button:
HeadingButton(
label = "H2",
isActive = state.currentHeadingStyle == HeadingStyle.H2,
onClick = {
val next = if (state.currentHeadingStyle == HeadingStyle.H2)
HeadingStyle.Normal
else
HeadingStyle.H2
state.setHeadingStyle(next)
},
)
Heading Levels¶
HeadingStyle is an enum with seven values:
| Style | Level | HTML | Markdown | Default size | Weight |
|---|---|---|---|---|---|
Normal | 0 | (none) | (none) | inherited | inherited |
H1 | 1 | <h1> | # | 2.0em | Bold |
H2 | 2 | <h2> | ## | 1.5em | Bold |
H3 | 3 | <h3> | ### | 1.17em | Bold |
H4 | 4 | <h4> | #### | 1.12em | Bold |
H5 | 5 | <h5> | ##### | 0.83em | Bold |
H6 | 6 | <h6> | ###### | 0.75em | Bold |
The defaults are framework-agnostic (em-based) so the core library doesn't depend on Material 2 or 3. Your app's TextStyle on the editor composable is still respected as the base from which em sizes derive.
Customizing the heading typography¶
Set RichTextConfig.headingTextStyles to give a level your own typography, for example the heading styles of your theme or a brand font:
richTextState.config.headingTextStyles = mapOf(
HeadingStyle.H1 to TextStyle(
fontSize = 28.sp,
fontWeight = FontWeight.SemiBold,
fontFamily = BrandFontFamily,
lineHeight = 36.sp,
),
HeadingStyle.H2 to MaterialTheme.typography.headlineSmall,
)
- Each
TextStyleis applied on top of the default of its level, so what it leaves unset keeps coming from the table above.TextStyle(color = Color.Red)colors a level and keeps its size and weight. - The paragraph part of the style (line height, alignment, and so on) applies to the heading paragraph.
- Levels without an entry keep the default. An entry for
HeadingStyle.Normalis ignored. - Changing the map re-styles the headings already in the editor, and in a read-only
RichText.
This is an appearance setting of the editor, like linkColor. The document stores the heading level only, so the output of toHtml() and toMarkdown() does not change: a restyled H1 is still written as a plain <h1> or #, and a loaded <h1> renders with your style. Set the map on every RichTextState that displays the content.
Formatting added to the heading itself still wins over the configured style and is still exported: a color or font size on part of a heading, or an alignment on the heading paragraph.
Note
A font size or weight equal to the level's default (bold, or 2em on an H1) counts as the heading's own typography, not as added formatting. It is not exported, and it is replaced by the configured style.
Note: This API is marked
@ExperimentalRichTextApiand may change in a future release.
Converting between levels¶
// From an integer (e.g. a toolbar picker)
val style = HeadingStyle.fromLevel(3) // H3
// From an HTML tag name
val fromHtml = HeadingStyle.fromHtmlTag("h2") // H2
val unknown = HeadingStyle.fromHtmlTag("div") // Normal
// Access the serialization constants directly
HeadingStyle.H3.markdownPrefix // "### "
HeadingStyle.H3.htmlTag // "h3"
Typing shortcut¶
With headingTypingShortcutsEnabled, typing # to ###### at the start of a paragraph sets heading level 1 to 6 and removes the marker, as in Notion and Google Docs. The shortcut is off by default:
The marker only counts at the very start of a paragraph, followed by a space. A marker typed in a list item stays as text, and so does one when headings are left out of config.features (see Editor features). One undo restores the typed marker.
Pressing Enter in a heading¶
Splitting a heading follows the same rules as Google Docs, Word and Notion:
- Inside the heading text: both halves stay headings of the same level.
- At the end of the heading: the new paragraph is a normal one, so what you type next is normal sized.
- At the start of the heading: an empty normal paragraph is inserted above, and the heading keeps its level and its content.
Serialization¶
HTML¶
Paragraphs with a heading level serialize to their corresponding tag:
state.setMarkdown("# Title\n\nBody paragraph.")
val html = state.toHtml()
// <h1>Title</h1><p>Body paragraph.</p>
On import, recognized tags (h1..h6) become the matching HeadingStyle; other block tags stay as Normal paragraphs.
Markdown¶
ATX-style headings (#..###### followed by a space) import and export losslessly:
state.setHtml("<h2>Section</h2><p>Body.</p>")
val markdown = state.toMarkdown()
// "## Section\n\nBody."
Setext-style headings (Title\n=====) are not supported on export but may be parsed on import depending on the Markdown dialect.
Why a first-class field?¶
Earlier revisions of the library modeled headings as a bundle of SpanStyle/ParagraphStyle (e.g. "this paragraph looks bold at 2em, so it must be an H1"). That approach breaks the moment a user applies partial formatting - italicizing one word in an H1 shouldn't demote it.
Storing the level directly on RichParagraph.headingStyle means:
- Toolbar highlighting stays accurate regardless of partial inline styling.
- Round-trips are lossless -
setHtml β toHtmlandsetMarkdown β toMarkdownpreserve the heading, even if the user tweaks font size mid-line. - Theme changes update visuals without losing structure - swap the app typography and every H2 re-renders at the new size without re-tagging.
Related¶
- For inline formatting on heading text, see Span Style.
- For other paragraph-level properties (alignment, indentation), see Paragraph Style.
- For HTML/Markdown conversion details, see HTML Import and Export and Markdown Import and Export.