Skip to content

fix(android,ios): honor textDecorationColor and textDecorationStyle on Text#56748

Open
quantizor wants to merge 3 commits intofacebook:mainfrom
quantizor:fix-android-text-decoration-color-on-main
Open

fix(android,ios): honor textDecorationColor and textDecorationStyle on Text#56748
quantizor wants to merge 3 commits intofacebook:mainfrom
quantizor:fix-android-text-decoration-color-on-main

Conversation

@quantizor
Copy link
Copy Markdown

@quantizor quantizor commented May 11, 2026

Summary:

textDecorationColor is declared on both TextStyleIOS and TextStyleAndroid in the public types, but on Android the value has no visible effect: the underline always paints in the text's foreground color. iOS honors color correctly via NSUnderlineColorAttributeName. Separately, textDecorationStyle: 'wavy' is silently dropped on both platforms (Fabric's C++ enum doesn't include Wavy, and the JS StyleSheet types describe it as supported).

This PR closes both gaps cross-platform:

Color (Android): Android's Layout.draw paints the underline produced by setUnderlineText(true) using paint.color, ignoring paint.underlineColor on every API level. The same applies to strikethrough. ReactUnderlineSpan and ReactStrikethroughSpan now extend DrawCommandSpan and paint the decoration themselves in onDraw via Canvas.drawLine / Canvas.drawPath, so the requested color actually reaches the paint.

Style (Android): Wires textDecorationStyle through the existing C++ → Kotlin pipeline (TA_KEY_TEXT_DECORATION_STYLE was a no-op handler). Implements solid, double, dotted, dashed, and wavy. The wavy curve uses Chromium/Blink's formula from decoration_line_painter.cc (wavelength = 1 + 2 * round(2 * thickness + 0.5), controlPointDistance = 0.5 + round(3 * thickness + 0.5), one cubic Bezier per wavelength with both control points at the midpoint, one above and one below the y-axis). The minimum stroke thickness is density-aware (1.5 dp) so decorations read consistently across display densities.

Style (iOS, wavy): UIKit's NSUnderlineStyle has no native wavy value, so Wavy ranges are tagged with a custom RCTWavyDecorationAttributeName (storing the line kinds + stroke color) in RCTAttributedTextUtils.mm and painted by RCTTextLayoutManager.mm after drawGlyphsForGlyphRange: using WebKit's formula from Source/WebCore/style/InlineTextBoxStyle.cpp (controlPointDistance = fontSize * 1.5 / 16, step = fontSize / 4.5, one cubic Bezier per wavelength, control points at the midpoint above and below the y-axis). The other four styles continue to use UIKit's native NSUnderlineStyle pattern bits.

ReactTextView (Android): invokes DrawCommandSpan.onDraw after super.onDraw, mirroring what PreparedLayoutTextView.onDraw already did. Without this, the new spans have no effect on the older view class, which is what some Text components on the new architecture still route through.

C++ enum: TextDecorationStyle::Wavy added to primitives.h and the conversions.h (de)serialization, so the wavy JS value flows through Fabric instead of being rejected with an Unsupported value log.

Related: #4579 (2015) flagged the color gap.

Changelog:

[ANDROID] [FIXED] - Text underlines and strikethroughs honor textDecorationColor
[GENERAL] [ADDED] - textDecorationStyle: 'wavy' (rendered natively on both Android and iOS using each platform's reference browser-engine formula)
[ANDROID] [ADDED] - Text decorations honor textDecorationStyle (solid, double, dotted, dashed, wavy)

Test Plan:

Rendered <Text> components with textDecorationLine set to "underline" or "line-through", textDecorationStyle cycling through solid / double / dotted / dashed / wavy, and textDecorationColor set to a value distinct from the foreground color. On stock 0.85.2 the decorations render in the text color with no style variation (and wavy is dropped); with this patch each style renders in the specified color with the requested stroke geometry. Verified single-line and wrapped multi-line cases on an Android API 36 emulator and iPhone 17 sim (iOS 26.4): each visual line within a wrapped block receives its own correctly-colored decoration that starts and ends at the line's content boundaries.

<Text style={{
  color: 'black',
  textDecorationLine: 'underline',
  textDecorationStyle: 'wavy',
  textDecorationColor: '#ff00aa',
}}>
  Hello
</Text>

Android's `Layout.draw` paints the underline produced by
`setUnderlineText(true)` using `paint.color`, ignoring
`paint.underlineColor` on all API levels. This caused
`textDecorationColor` to be silently dropped on Android.

Refactor `ReactUnderlineSpan` to extend `DrawCommandSpan` and paint the
underline itself in `onDraw`, falling back to the text color when no
color was specified. Thread the color through `TextAttributeProps` (both
MapBuffer and ReadableMap ingestion paths) and `TextLayoutManager`. Add
`DrawCommandSpan` invocation to `ReactTextView.onDraw`, mirroring the
existing `PreparedLayoutTextView` behavior so both text view classes
honor custom-drawing spans.

## Changelog

[ANDROID] [FIXED] - Text underlines honor `textDecorationColor`

## Test Plan

Render a Text component with `textDecorationColor` set to a value
distinct from the text color; the underline now renders in the specified
color rather than the text color. Verified on Android API 36 emulator.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label May 11, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label May 11, 2026
@quantizor quantizor changed the title fix(android): honor textDecorationColor on Text underlines fix(android): honor textDecorationColor on Text underlines and strikethroughs May 11, 2026
@quantizor quantizor changed the title fix(android): honor textDecorationColor on Text underlines and strikethroughs fix(android): honor textDecorationColor and textDecorationStyle on Text May 11, 2026
@quantizor quantizor force-pushed the fix-android-text-decoration-color-on-main branch from ac5bbab to 1a8cbda Compare May 11, 2026 05:56
@quantizor quantizor changed the title fix(android): honor textDecorationColor and textDecorationStyle on Text fix(android,ios): honor textDecorationColor and textDecorationStyle on Text May 11, 2026
quantizor added a commit to quantizor/react-native that referenced this pull request May 11, 2026
Iterations on the textDecorationStyle implementation that landed in PR
facebook#56748, based on visual comparison with Chrome / Safari and side-by-side
testing of the two platforms.

iOS:
- Wavy: thickness divisor relaxed from `fontSize / 8` to `fontSize / 12`
  and control-point distance multiplier halved (`1.5 * thickness + 0.5`
  vs Blink's literal `3 * thickness + 0.5`). At iOS point sizes the
  literal Blink amplitude renders as a very pronounced wave; the dialed-
  back values read as a clear-but-subtle browser-style wave.
- Dotted: switched from UIKit's `NSUnderlineStylePatternDot` (which
  doesn't match browser geometry) to a custom CG path with a zero-length
  dash + round line caps, producing actual circular dots at `2 *
  thickness` spacing.
- Dashed: switched from UIKit's `NSUnderlineStylePatternDash` to
  custom CG path with `[2 * thickness, thickness]` intervals — short
  rectangular dashes with a tight gap, closer to Safari's geometry than
  UIKit's default.
- The custom decoration attribute (formerly `RCTWavyDecorationAttributeName`)
  is now `RCTCustomDecorationAttributeName` and carries a `style` key so
  the same drawing pipeline handles wavy + dotted + dashed.

Cross-platform:
- Wavy drawing loop now iterates `while x < x2` instead of
  `while x + wavelength <= x2`, so the final cycle continues through
  the last character (including trailing punctuation). Previously a
  trailing period could be visually uncovered when the run width was
  not an integer multiple of the wavelength.

## Changelog:

[IOS] [CHANGED] - Wavy, dotted, and dashed text decorations render with custom CoreGraphics paths instead of UIKit pattern bits, matching browser geometry more closely
[GENERAL] [FIXED] - Wavy underline / strikethrough now extends through the final character of the run, including trailing punctuation

## Test Plan:

Side-by-side comparison on Android API 36 emulator and iPhone 17 sim
(iOS 26.4) of a `<Text>` with `textDecorationLine="underline"` and
`textDecorationStyle` cycling through `wavy` / `dotted` / `dashed`,
verified against Chrome (Android view) and Safari (iOS view) rendering
of the same CSS. Trailing periods now fall under the wavy stroke on
both platforms.
@fabriziocucci
Copy link
Copy Markdown
Contributor

@quantizor would it be too hard to split this into 3 separate PRs? 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants