Skip to content

Custom Attributes

Beyond standard formatting, applications frequently require domain-specific styling or metadata (e.g. pastel highlighter pens, @mentions, #hashtags, editorial comment anchors, spoiler tags). Arranger enables developers to create custom AttributeKey<T> types and resolve them into Compose styles with complete compile-time type safety.

Custom Attribute Sample


Four Implementation Steps

Introducing custom attributes involves four straightforward steps:

  1. Define Attribute Key: Implement SpanAttributeKey<T> or ParagraphAttributeKey<T>.
  2. Add DSL Extensions: Define helper functions on AttributeEditScope (optional).
  3. Configure Style Resolver: Map the attribute key to Compose SpanStyle or ParagraphStyle in AttributeStyleResolver.
  4. Render in Editor: Pass the custom resolver to RichTextEditor(styleResolver = ...).

Implementation Example 1: Highlighter Pen (HighlightKey)

An example of a custom flag span attribute without a parameter value (Unit):

Step 1: Define Attribute Key

// Define as a singleton object or data object
object HighlightKey : SpanAttributeKey<Unit> {
    override val name: String = "Highlight"
    override val defaultValue: Unit = Unit
}

Step 2: Add DSL Extensions

Add extension functions so the attribute can be called intuitively inside edit { ... } blocks.

fun AttributeEditScope.highlight() {
    setSpanAttribute(HighlightKey, Unit)
}

fun AttributeEditScope.clearHighlight() {
    setSpanAttribute(HighlightKey, null)
}

Step 3: Configure Style Resolver

Using AttributeStyleResolver(base = DefaultAttributeStyleResolver) allows you to retain all default formatting rules (bold, underline, etc.) while layering in custom styling rules.

val CustomAttributeResolver = AttributeStyleResolver(base = DefaultAttributeStyleResolver) {
    spanStyle(HighlightKey) {
        SpanStyle(
            background = Color(0xFFFFF59D), // Soft highlighter yellow
            color = Color(0xFFE65100),      // Deep orange text
            fontWeight = FontWeight.Bold,
        )
    }
}

Step 4: Render in Editor

@Composable
fun CustomAttributeScreen() {
    val text = "Arranger provides robust support for custom attributes."

    val state = remember {
        RichTextState(
            initialText = RichString(text).edit {
                editAttributes(range = text.rangeOf("custom attributes")) {
                    highlight()
                }
            }
        )
    }

    RichTextEditor(
        state = state,
        styleResolver = CustomAttributeResolver,
        modifier = Modifier.fillMaxWidth(),
    )
}

Implementation Example 2: Metadata Attribute (Mentions)

You can also attach arbitrary structured objects (such as user IDs or metadata) as attribute values:

data class MentionData(val userId: String, val username: String)

object MentionKey : SpanAttributeKey<MentionData> {
    override val name: String = "Mention"
    override val defaultValue: MentionData = MentionData(userId = "", username = "")
}

// Define resolver mapping
val MentionStyleResolver = AttributeStyleResolver(base = DefaultAttributeStyleResolver) {
    spanStyle(MentionKey) { data ->
        SpanStyle(
            color = Color(0xFF1976D2), // Blue link-style text
            fontWeight = FontWeight.SemiBold,
        )
    }
}

When tapped, you can retrieve MentionData directly via event.span.attributes[MentionKey] to navigate to a user profile or display a hovercard.