Skip to main content

ADR-0014: Renderer-neutral interaction indication

  • Status: Accepted
  • Date: 2026-08-15
  • Extends: ADR-0013

Context

ViewCompose layout primitives and component NodeSpecs exposed Android-shaped rippleColor properties. Several components also exposed complete pressed, focused, and hovered state layers, creating two precedence paths for the same feedback. StateLayerBox and StateLayerRow then made interaction feedback look like a distinct layout primitive even though it did not change measurement or placement.

This leaked Android's RippleDrawable execution model into UI Contract, expanded otherwise simple layout DSLs, and forced a NodeSpec change and rebind for feedback that can be patched as ordered modifier appearance. It also made a single pressed color incapable of representing selected versus unselected internal targets in native-backed controls.

Decision

UI Contract owns a sealed, immutable UiInteractionIndication value and an ordered Modifier.interactionIndication element. The current StateLayer subtype carries complete UiStateLayerColors; Android Renderer maps it to platform state and mask drawables. New indication subtypes require an explicit compatibility decision because renderers must handle every subtype in the artifact version they consume.

Box, Row, and other layout primitives own only measurement, placement, child scope, and caller modifiers. They never accept interaction colors. High-level components resolve semantic roles, enabled policy, sparse overrides, and the active design-system recipe before installing an indication. BasicSurfaceStyle may carry the complete resolved indication, but application-facing sparse overrides remain component-specific under ADR-0013.

The theme boundary follows the same model. UiColors contains semantic color roles and UiStateColors contains persistent component states; neither exposes the Android-shaped ripple/controlHighlight slots. UiInteractionTokens owns pressed, focused, and hovered opacities. A design-system adapter may read a platform highlight internally, but it must resolve that input into the neutral interaction policy or an explicit component indication before entering UI Foundation.

Native-backed components with multiple internal interaction targets carry complete typed values in their component NodeSpec. Segmented controls and navigation destinations therefore retain selected and unselected state-layer snapshots. Tab rows emit eager keyed child boxes, so each child owns its indication through the ordinary modifier path.

Disabled or non-interactive high-level components install no indication. An absent low-level indication leaves feedback fallback to the renderer only when a custom node is otherwise interactive. Android ripple color, state-list construction, masking, and drawable lifecycle remain private renderer details.

This is an alpha-line hard cut. rippleColor, UiColors.ripple, UiStateColors.controlHighlight, StateLayerBox, and StateLayerRow are removed rather than deprecated, and indication changes use modifier-only patching instead of logical-node rebind.

Consequences

  • Layout APIs remain small and portable across Android and future renderers.
  • Design systems resolve one complete feedback contract without Material 3 becoming framework policy.
  • Theme snapshots cannot carry an unused Android highlight slot beside the effective interaction opacity policy.
  • Pressed, focused, and hovered precedence is explicit and testable.
  • Native multi-target controls can render distinct selected and unselected feedback.
  • Custom components may opt into feedback through one ordered modifier without manufacturing a pseudo layout primitive.
  • Custom renderers must exhaustively implement the indication types shipped by their UI Contract version.
  • Modifier equality makes an indication change patch the retained View without re-composing layout semantics or rebuilding the node.

Rejected alternatives

Keep rippleColor as a compatibility fallback

Rejected because it preserves two sources of truth and cannot express focus, hover, or per-target roles.

Put feedback fields on every clickable NodeSpec

Rejected because general appearance belongs to the ordered modifier channel and would otherwise force component-specific binder and differ logic.

Keep StateLayerBox and StateLayerRow

Rejected because state-layer drawing does not define a different measurement, placement, state, or lifecycle primitive.

Make one design system own indication policy

Rejected because Material 3, One UI 7, application design systems, and future recipes are peer implementations above the same neutral contract.

Validation

The contract requires equality and modifier-order tests, pressed/focused/hovered state-list tests, disabled absence tests, retained-View modifier-only patch tests, native selected/unselected target tests, multi-design-system compilation, API dumps, compiled samples, and a source gate that rejects public rippleColor and interaction pseudo-layout APIs.