Skip to content

Harden Tailwind customization across Reflex charts - #328

Merged
Alek99 merged 4 commits into
mainfrom
agent/tailwind-live-chart-classes
Jul 27, 2026
Merged

Harden Tailwind customization across Reflex charts#328
Alek99 merged 4 commits into
mainfrom
agent/tailwind-live-chart-classes

Conversation

@Alek99

@Alek99 Alek99 commented Jul 26, 2026

Copy link
Copy Markdown
Member

Summary

  • audit Tailwind v4 customizability end to end: Reflex source discovery, static/live/facet payloads, browser cascade, repeated DOM slots, interaction state, and canvas boundaries
  • make reflex_xy.chart(..., tailwind_classes=...) a verbatim compile-time inventory for live token/Var charts, including quoted content, raw backslashes, Unicode, arbitrary values/properties, and escaped selectors
  • move defeatable visual defaults out of inline styles so normal utilities control root typography, solid and SVG legend swatches, custom-tooltip chrome, and axis-title weight
  • extend the selection slot to lasso paths/handles while preserving structural pointer behavior
  • rebuild live views only when constructor-owned chrome changes, preserving every surviving named-axis range and durable box/range/lasso geometry without callback, linked-selection, history, or DOM-event echo
  • configure/document selector-based dark mode, correct automatic DOM class discovery, and publish a full capability audit

User-facing API

reflex_xy.chart(
    Dashboard.chart,
    tailwind_classes=[
        "rounded-2xl border border-slate-200 dark:border-slate-800",
        "bg-white text-slate-900 dark:bg-slate-950 dark:text-slate-100",
        "before:content-['✓']",
        r"[&_[data-xy-slot=legend\_label]]:font-semibold",
    ],
)

tailwind_classes accepts a complete string or ordered iterable, rejects mappings/unordered sets, de-duplicates candidates, merges static discovery, and applies to every facet panel. It is compile-only and never becomes a DOM attribute. State-driven charts must inventory every complete class they may emit.

Production Tailwind matrix

Verified in a real reflex run --env prod --single-port build:

  • md:, arbitrary max-*, @container, and arbitrary @min-*
  • selector dark:, hover:, focus-visible:, active:, not-*, has-*, and supports-*
  • arbitrary values/properties, CSS-variable shorthand, important modifiers, transitions, and alpha colors
  • quoted Unicode pseudo-content and escaped underscores in descendant selectors
  • root and slot typography, spacing, borders, radius, shadows, backgrounds, SVG fill/stroke, and cursor utilities
  • static charts, live tokens, live rose→sky class swaps, and every facet panel

Measured results include matching static/live styles, no scan-prop leak, selector dark mode, a 31px→23px arbitrary container-query title change at 780px→480px, and renderer-default plus Tailwind-override coverage for solid, gradient-scatter, and line legend handles.

Boundaries

Tailwind owns durable visual appearance on DOM slots. XY still owns structural geometry and conditional interaction state. CSS can style canvas/chrome as whole elements but cannot select individual WebGL or canvas-painted pixels. Full constructor-chrome replacement preserves durable viewport/selection state; transient view-local state such as the selected drag tool, history, legend toggles, and moved modebar position resets with the replaced view.

The detailed audit is in spec/process/tailwind-customizability-audit-2026-07-26.md.

Before / after

Before: live classes reached DOM, but Tailwind emitted no matching rules.

Before

After: the same production app emits and applies the requested utilities.

After

The second comparison holds utilities constant and isolates former inline-cascade blockers versus the fixed client:

Cascade before and after

Validation

  • uv run --with pre-commit pre-commit run --all-files
  • uv run ruff check .
  • uv run ruff format --check .
  • node js/build.mjs
  • npm run typecheck
  • focused adapter/component/static-client tests: 157 passed
  • Chromium Tailwind/interaction/rebuild/payload suite: 26 passed
  • documentation suite: 92 passed, 1 expected failure
  • exact full local suite: 2,583 passed, 66 skipped; the only failure is the existing local asset assertion that looks for readable function names after the intentionally minified generated client build
  • production Reflex/Tailwind build and browser computed-style matrix: passed

Summary by CodeRabbit

  • New Features
    • Added tailwind_classes support for live and static charts (including facet panels) with ordered class inventory.
    • Expanded chart DOM styling customization to 29 slots, including the full lasso selection overlay (path + handles).
    • Integrated Tailwind v4 dark mode via selector-based switching for Reflex.
  • Bug Fixes
    • Improved live update handling to preserve selections, axis ranges, and interactive geometry without echoing events.
    • Fixed lasso pointer-event behavior and ensured selection updates respect suppression.
    • Refined legend swatches, tooltip visuals, and default typography weight/cascade behavior.
  • Documentation
    • Updated Tailwind, slot/cascade, dark mode, and export guidance for the new behavior.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds build-time Tailwind class inventories for Reflex charts, updates chart chrome cascade and selection-slot behavior, restores durable view state across live chrome rebuilds, and documents and tests the resulting styling and interaction contracts.

Changes

Tailwind styling and live chart integration

Layer / File(s) Summary
Tailwind inventory and adapter wiring
python/reflex-xy/reflex_xy/component.py, python/xy/_figure.py, tests/reflex_adapter/*, tests/test_components.py
chart(..., tailwind_classes=...) validates ordered inventories, merges them with discovered DOM classes, applies them to static, live, and facet charts, and excludes adapter-only mark metadata.
Chart chrome styling and slot behavior
js/src/20_theme.ts, js/src/50_chartview.ts, js/src/53_interaction.ts, js/src/57_viewstate.ts, tests/test_*
Legend paint uses CSS variables, custom tooltips use stylesheet defaults, axis typography relies on cascade rules, and lasso paths and handles use the selection slot while preserving pointer behavior.
Live chrome rebuild and silent state restoration
python/reflex-xy/reflex_xy/assets/XYChart.jsx, js/src/53_interaction.ts, js/src/54_kernel.ts, js/src/57_viewstate.ts, tests/test_view_state_client.py
Mounted chrome changes trigger rebuilds that restore axis ranges and selection geometry without history, broadcasts, communications, or semantic selection callbacks.
Styling contracts and verification documentation
docs/*, spec/*, python/reflex-xy/README.md, python/reflex-xy/CHANGELOG.md, tests/test_docs_site.py, tests/test_tailwind_root_customization.py
Documentation and browser tests define selector-based dark mode, slot coverage, cascade ownership, Tailwind inventory rules, export behavior, and live-state restoration semantics.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReflexChart
  participant TailwindScanner
  participant XYChart
  participant ChartView
  participant SelectionMask
  ReflexChart->>TailwindScanner: provide normalized class inventory
  TailwindScanner->>XYChart: emit scan-only tailwind_class_tokens
  XYChart->>ChartView: rebuild mounted chrome when constructor inputs change
  ChartView->>ChartView: restore ranges and selection geometry silently
  ChartView->>SelectionMask: request selection mask
  SelectionMask-->>XYChart: return suppressed selection response
Loading

Possibly related PRs

  • reflex-dev/xy#238: Changes selection overlay and dispatch behavior in the same interaction and view-state paths.

Suggested reviewers: carlosabadia

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.77% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the PR’s main theme of improving Tailwind customization across Reflex charts.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/tailwind-live-chart-classes

Comment @coderabbitai help to get the list of available commands.

@codspeed-hq

codspeed-hq Bot commented Jul 26, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 103 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing agent/tailwind-live-chart-classes (36c55f3) with main (e4b119b)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@Alek99 Alek99 changed the title Fix Tailwind classes for live Reflex charts Harden Tailwind customization across Reflex charts Jul 27, 2026
@Alek99
Alek99 marked this pull request as ready for review July 27, 2026 01:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
tests/test_view_state_client.py (1)

634-640: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Tautological probe assertion in both new tests. assert result == {key: True for key in result} derives its expectation from result, so an empty or key-dropped probe payload passes silently.

  • tests/test_view_state_client.py#L634-L640: assert the explicit nine-key expectation for the Tailwind lasso slot probe.
  • tests/test_view_state_client.py#L978-L984: assert the explicit nine-key expectation for the republish durable-state probe.

Keep as-is if this matches the existing convention elsewhere in the suite.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_view_state_client.py` around lines 634 - 640, Replace the
tautological assertions in tests/test_view_state_client.py at lines 634-640 and
978-984 with explicit expectations containing all nine required probe keys, each
mapped to True. Update both the Tailwind lasso slot probe and republish
durable-state probe assertions; do not derive expected keys from result.
python/reflex-xy/reflex_xy/component.py (1)

209-214: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: extract the merge-and-inject pair.

Lines 209-214 and 312-317 are the same three-step sequence (discover, merge, inject). A tiny helper (e.g. _apply_tailwind_tokens(props, figure, tailwind_manifest)) would keep the two call sites from drifting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/reflex-xy/reflex_xy/component.py` around lines 209 - 214, Extract the
duplicated Tailwind token discovery, merge, and injection sequence into a helper
such as _apply_tailwind_tokens, preserving the existing conditional injection
behavior. Replace both sequences around the visible class_manifest logic and the
corresponding second call site with this helper so they remain consistent.
tests/test_tailwind_root_customization.py (1)

144-204: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

These two tests assert on JSX source text, not behavior.

Exact-substring checks like "const chromeChanged = Boolean(view && !sameMountedChromeSpec(view.spec, spec));" break on a formatter run or a local rename while the contract is unchanged. Since XYChart.jsx is a non-importable asset this is a reasonable stopgap, but consider narrowing to the identifiers that actually carry the contract (sameMountedChromeSpec, updatePayload, suppress_event) rather than whole formatted statements.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_tailwind_root_customization.py` around lines 144 - 204, Refactor
the source-text assertions in
test_live_wrapper_rebuilds_constructor_owned_chrome_only_when_needed and
test_live_wrapper_silently_hydrates_durable_selection_and_all_axis_ranges to
check stable contract identifiers such as sameMountedChromeSpec, updatePayload,
and suppress_event instead of complete formatted statements. Preserve the
existing ordering and semantic checks while making assertions resilient to
formatting changes and local variable renames.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@js/src/20_theme.ts`:
- Line 115: Update the custom-tooltip CSS rule for
[data-xy-slot="tooltip"][data-xy-custom-tooltip] to also remove the default
box-shadow, while preserving its existing transparent background, border, and
padding overrides.

In `@js/src/53_interaction.ts`:
- Around line 868-875: Update the selection restoration branches in
js/src/53_interaction.ts at lines 868-875 and 892-903: when dispatch is
suppressed, invoke _selectLocal(x0, x1, y0, y1) or _selectLocalPolygon(polygon)
respectively; only send through this.comm when dispatch is enabled, while
preserving the existing dispatch-enabled behavior.

In `@tests/test_tailwind_root_customization.py`:
- Around line 170-177: Update the assertion around the axisLayoutSpec and
mountedChromeSpec slice to first assert that the axisLayoutSpec declaration
index is before the mountedChromeSpec declaration index, then perform the
existing range-exclusion check. Keep the current substring assertion unchanged
for the valid ordering case.

---

Nitpick comments:
In `@python/reflex-xy/reflex_xy/component.py`:
- Around line 209-214: Extract the duplicated Tailwind token discovery, merge,
and injection sequence into a helper such as _apply_tailwind_tokens, preserving
the existing conditional injection behavior. Replace both sequences around the
visible class_manifest logic and the corresponding second call site with this
helper so they remain consistent.

In `@tests/test_tailwind_root_customization.py`:
- Around line 144-204: Refactor the source-text assertions in
test_live_wrapper_rebuilds_constructor_owned_chrome_only_when_needed and
test_live_wrapper_silently_hydrates_durable_selection_and_all_axis_ranges to
check stable contract identifiers such as sameMountedChromeSpec, updatePayload,
and suppress_event instead of complete formatted statements. Preserve the
existing ordering and semantic checks while making assertions resilient to
formatting changes and local variable renames.

In `@tests/test_view_state_client.py`:
- Around line 634-640: Replace the tautological assertions in
tests/test_view_state_client.py at lines 634-640 and 978-984 with explicit
expectations containing all nine required probe keys, each mapped to True.
Update both the Tailwind lasso slot probe and republish durable-state probe
assertions; do not derive expected keys from result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e07c20ff-2f4e-4c65-a59c-7b8cddb13f7c

📥 Commits

Reviewing files that changed from the base of the PR and between bb0059c and bf89be9.

⛔ Files ignored due to path filters (3)
  • spec/assets/tailwind-cascade-before-after.jpg is excluded by !**/*.jpg
  • spec/assets/tailwind-live-after.jpg is excluded by !**/*.jpg
  • spec/assets/tailwind-live-before.jpg is excluded by !**/*.jpg
📒 Files selected for processing (30)
  • docs/app/rxconfig.py
  • docs/app/tests/test_docs_site.py
  • docs/styling/chrome-slots.md
  • docs/styling/index.md
  • docs/styling/themes-and-tokens.md
  • js/src/20_theme.ts
  • js/src/50_chartview.ts
  • js/src/53_interaction.ts
  • js/src/54_kernel.ts
  • js/src/57_viewstate.ts
  • python/reflex-xy/CHANGELOG.md
  • python/reflex-xy/README.md
  • python/reflex-xy/reflex_xy/assets/XYChart.jsx
  • python/reflex-xy/reflex_xy/component.py
  • python/xy/_figure.py
  • python/xy/components.py
  • spec/README.md
  • spec/api/chart-roadmap.md
  • spec/api/export.md
  • spec/api/styling.md
  • spec/design/reflex-integration.md
  • spec/design/reflex-shaped-api.md
  • spec/process/tailwind-customizability-audit-2026-07-26.md
  • tests/reflex_adapter/test_component.py
  • tests/test_components.py
  • tests/test_export_style_survival.py
  • tests/test_legend_highlight.py
  • tests/test_static_client_security.py
  • tests/test_tailwind_root_customization.py
  • tests/test_view_state_client.py

Comment thread js/src/20_theme.ts Outdated
Comment thread js/src/53_interaction.ts
Comment thread tests/test_tailwind_root_customization.py Outdated
@Alek99
Alek99 merged commit a2ac10e into main Jul 27, 2026
23 of 24 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@js/src/53_interaction.ts`:
- Around line 958-962: Update the local restoration logic around _applySelMask
so traces excluded by the opts.localMask condition are explicitly cleared before
continuing. Ensure non-scatter and _legendHidden traces cannot retain stale
selection masks, while preserving the existing restoration behavior for eligible
visible scatter traces.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cb8dbfe6-168c-4593-956d-151ebd93bc2d

📥 Commits

Reviewing files that changed from the base of the PR and between bf89be9 and 36c55f3.

📒 Files selected for processing (12)
  • js/src/20_theme.ts
  • js/src/50_chartview.ts
  • js/src/53_interaction.ts
  • python/reflex-xy/reflex_xy/assets/XYChart.jsx
  • python/xy/_figure.py
  • python/xy/components.py
  • spec/api/styling.md
  • tests/test_components.py
  • tests/test_static_client_security.py
  • tests/test_tailwind_root_customization.py
  • tests/test_text_weight_defaults.py
  • tests/test_view_state_client.py
🚧 Files skipped from review as they are similar to previous changes (9)
  • python/xy/_figure.py
  • tests/test_static_client_security.py
  • python/xy/components.py
  • tests/test_tailwind_root_customization.py
  • spec/api/styling.md
  • tests/test_view_state_client.py
  • tests/test_components.py
  • python/reflex-xy/reflex_xy/assets/XYChart.jsx
  • js/src/50_chartview.ts

Comment thread js/src/53_interaction.ts
Comment on lines +958 to +962
// Restoration mirrors the kernel selection universe exactly. Otherwise
// a provisional line/hidden-series mask would survive forever because
// the authoritative reply only contains visible scatter trace ids.
if (opts.localMask === true
&& (g.trace.kind !== "scatter" || g._legendHidden)) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C 5 '_restoreSelectionLocalMask|_applySelMask|selection.*mask|mask.*selection' js/src tests

Repository: reflex-dev/xy

Length of output: 15455


Clear excluded-trace masks during local restoration.

These continue branches skip _applySelMask, so a non-scatter or hidden trace that already has a mask can stay highlighted. Clear excluded masks here, or reset every trace before this preview path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/src/53_interaction.ts` around lines 958 - 962, Update the local
restoration logic around _applySelMask so traces excluded by the opts.localMask
condition are explicitly cleared before continuing. Ensure non-scatter and
_legendHidden traces cannot retain stale selection masks, while preserving the
existing restoration behavior for eligible visible scatter traces.

Alek99 added a commit that referenced this pull request Jul 27, 2026
Only conflict was a comment in tests/test_export_style_survival.py that #328
updated from 23 to 29 slots. Kept this branch's wording, which reads the count
from CHART_DOM_SLOTS instead of writing it down, so the next slot addition
cannot leave it stale again.
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