Skip to content

Render the styles carrier label as the part attribute - #401

Closed
davesnx wants to merge 1 commit into
mainfrom
styles-part-attribute
Closed

davesnx wants to merge 1 commit into
mainfrom
styles-part-attribute

Conversation

@davesnx

@davesnx davesnx commented Sep 23, 2026

Copy link
Copy Markdown
Member

styled-ppx's next snapshot (davesnx/styled-ppx#614 and the stack below it) turns the CSS.styles carrier into (className, style, label), where the label is the binding name, and renders that label as the element's part attribute so styles stay findable in DevTools ([part~="Button"]) now that class names carry no label suffix. Styled components do that themselves; lowercase elements with styles= are expanded here, so the ppx has to emit it too.

Styles_attribute.expand_attributes now adds ?part next to className and style:

<div styles=x />
/* becomes */
div ~className:(CSS.className x) ~style:(CSS.styles x)
  ?part:(match CSS.label x with "" -> None | part -> Some part) ...
  • Only a non-empty label produces the attribute; unlabeled carriers render exactly as before.
  • ?styles keeps part optional through the None case.
  • An explicit part prop merges with the label the way className merges (part="Button host"); merge_className became merge_string_attribute name so both share one implementation.
  • Native and Melange mode share the expansion. On the Melange side the emitted ?part reaches ReactDOM.domProps, which does not accept ~part in reason-react 0.17 yet, so that side needs a reason-react change before this can be released. Draft until that lands or until we decide to gate the Melange emission.

Requires a styled-ppx runtime that defines CSS.label.

Tests

At c3eae9a, OCaml 5.4.0, quickjs 0.5.1, melange 7.0.1, ocamlformat 0.28.1:

  • dune build @packages/styles-attribute/runtest: 7 tests, including the new expand_styles_with_previous_part.
  • dune build @packages/server-reason-react-ppx/runtest: 72 tests; the CSS stub is a triple now, with styles_attribute_with_label, styles_attribute_with_label_and_part and styles_attribute_optional_some_with_label asserting part="Button", part="Button host" and part="Card" in the rendered HTML. Cram expectations for styles, styles-native, styles-js and optional-prop-shadowed-none were promoted and reviewed; the only change is the added part branch.
  • dune build @runtest: everything passes except packages/router/test/browser, which needs npm install in that checkout.
  • dune build @fmt on the touched directories: clean.

Risk: two-way door. Reverting restores the className/style-only expansion. Merging as is breaks Melange builds that use styles= on lowercase elements until reason-react's domProps accepts ~part.

styled-ppx now carries the binding name as the third element of the
CSS.styles carrier. When the ppx expands styles= on a lowercase element it
also emits ?part from CSS.label, only when the label is non-empty, and an
explicit part prop merges with it the way className does. Native and
Melange mode share the expansion; the Melange side needs ReactDOM.domProps
to accept ~part.
@vercel

vercel Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
server-reason-react Ready Ready Preview Sep 23, 2026 3:06pm UTC

Request Review

@github-actions

Copy link
Copy Markdown

Benchmarks

No baseline from main available yet, showing absolute values only.

Benchmark ops/sec
trivial/renderToStaticMarkup 2,498,245
trivial/renderToString 2,259,862
depth/10 62,602
depth/25 24,662
depth/50 12,579
depth/100 6,350
width/10 38,015
width/100 3,913
width/500 713.14
width/1000 359.02
table/10 25,105
table/50 5,410
table/100 2,318
table/500 505.58
props/small 10,033
props/medium 3,469
props/large 1,129
realworld/ecommerce24 6,295
realworld/ecommerce48 3,374
realworld/dashboard 26,500
realworld/blog50 4,104
realworld/form 13,897
primitive/React.string 17,637,948
primitive/React.int 8,495,653
primitive/React.null 26,749,388
primitive/createElement_empty 11,765,229
primitive/createElement_children 854,185
primitive/React.array_10 797,578
primitive/React.array_100 87,312
primitive/React.list_10 848,740
primitive/React.list_100 94,989
rsc/trivial 312,007
rsc/depth/50 4,818
rsc/width/100 520.18
rsc/width/500 95.75
rsc/width/1000 45.47
rsc/table/100 267.6
rsc/table/500 50.81
streaming/renderToStream/wide100 3,552
streaming/renderToStream/suspense-drained 36,986
rsc/render_html/wide100 522.8
rsc/render_html/suspense 15,374
rsc/render_model/wide100 1,055
router/match/static-small-first 18,779,677
router/match/static-small-last 27,877,377
router/match/static-large-first 27,405,232
router/match/static-large-last 26,727,559
router/match/static-large-miss 2,565,807
router/match/dynamic-large-first 1,725,625
router/match/dynamic-large-last 1,956,616
router/endpoint/static-large-last 15,204,466
router/endpoint/dynamic-large-last 1,798,458
router/parse/path-plain 1,846,274
router/parse/path-encoded 1,217,019
router/parse/search 2,422,913

@davesnx
davesnx marked this pull request as ready for review September 23, 2026 16:44
@davesnx davesnx closed this Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview — c3eae9a1 Deployed Sep 23, 2026 by vercel[bot]
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