Skip to content

Let a button variant name a theme colour - #137

Draft
simonhamp wants to merge 2 commits into
mainfrom
feat/button-theme-token-variant
Draft

simonhamp wants to merge 2 commits into
mainfrom
feat/button-theme-token-variant

Conversation

@simonhamp

@simonhamp simonhamp commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

A button's variant can now name any colour token in the theme, as well as the built-in ones. The button fills like primary in that token, with its label in the token's on- pair, or in on-primary when the theme has none.

// config/native-ui.php
'light' => ['brand' => '#0c8599', 'on-brand' => '#ffffff'],
'dark' => ['brand' => '#3bc9db', 'on-brand' => '#111112'],
<native:button variant="brand">Follow</native:button>

Why

The devnull app draws its main buttons in the colour each person picks for their lane, so the colour is only known at runtime. Button deliberately takes no per-instance colour. This keeps that rule: the colours still live in the theme and the button only says which pair to use, so :variant="'lane-'.$lane" works.

How

PHP resolves the token's light and dark shades and sends them as tint_color and label_color, plus dark_tint_color and dark_label_color when they differ. That's how the pager's tab-* colours travel in #136. Both renderers draw them where they drew primary, and iOS's glass styles pick them up too. Disabled is still surface-variant on both.

The built-in variants send nothing new. A name the theme doesn't have still draws as primary, as an unknown variant always has.

The second commit renames the label's key. It was on_tint at first, and the PHP extension sends any prop starting with on_ as a callback id, so the label arrived as 0 and was transparent in light mode (dark used dark_on_tint, which worked). There's a test that no colour goes out under an on_ key. That rule is easy to trip over for any element, so it might be worth a line in the element docs.

Worth a look

  • On Android, accent has no case in ButtonRenderer.kt and falls through to primary. That's the same on main and I've left it alone, but it means an accent button is primary-coloured on Android.
  • I haven't built the Android change. There's no Kotlin compiler here and the app I tested in has no Android project yet. The change is small (a local tokenColor() and two arguments swapped), and CI doesn't compile Kotlin either.

Verified

  • tests/ButtonTokenVariantTest.php: the props for a token variant from Blade and the fluent API, the on-primary fallback, built-ins and unknown names sending nothing, no on_ keys, and source checks on both renderers. 282 tests pass, Pint clean, swiftc -parse clean.
  • Built into the devnull app on an iPhone 17 Pro simulator (iOS 27) through combined/dev-null-app, in light and dark, with green, rose, teal and ochre lanes.

The Account screen: green in light, then rose in light and dark, with the built-in secondary and destructive buttons beside them.

Account screen

Labels per lane: teal and ochre in dark and light.

Teal and ochre

The light-mode label before and after the key rename.

Before and after

Screenshots live on the orphan branch button-theme-token-variant-screenshots. Delete it after merging.

Used by NativePHP/dev-null-app#8, and merged into combined/dev-null-app for it.

🤖 Generated with Claude Code

simonhamp and others added 2 commits October 17, 2026 15:57
A button's variant can now be the name of any colour token in the theme, as well as one of the built-in ones. The button fills like primary in that token and draws its label in the matching on- token, or in on-primary when the theme has none.

PHP resolves both colours from the theme and sends them as tint and on_tint, with dark_ shades when they differ, the way the pager's tab colours work. Both renderers draw them in place of primary. The built-in variants send nothing new, and a name the theme doesn't have still draws as primary.

This lets an app pick a button's colour at runtime, say a colour each user chooses, while every colour still comes from the theme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The label's colour went as on_tint, and the bridge sends every prop that starts with on_ as a callback. It reached the renderers as 0, so the label was transparent wherever on_tint was used, which was light mode. Keys ending in _color travel as colours, and both of these are already in the interned key table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant