Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9e29139
fix(extractor): let a later css() part replace an earlier one's decla…
owjs3901 Oct 1, 2026
337ddeb
test(extractor): cover known style composition branches
owjs3901 Oct 1, 2026
024a24c
chore: satisfy clippy assert_is_empty lint on Rust 1.99
owjs3901 Oct 1, 2026
71585a9
test(extractor): cover false ?? composition branch
owjs3901 Oct 1, 2026
52f4518
refactor(extractor): split coalesce check so composition lines are me…
owjs3901 Oct 1, 2026
1cedf9f
fix(extractor): compose styled extensions, merge attrs and let later …
owjs3901 Oct 1, 2026
01adab9
test(extractor): restore runtime-value composition test and cover sty…
owjs3901 Oct 1, 2026
53f740b
fix(extractor): render styled as targets and compile withComponent
owjs3901 Oct 1, 2026
b4b42cf
fix(extractor): keep read, $ and theme props off styled tags and eval…
owjs3901 Oct 1, 2026
e2cfa67
test(extractor): cover shouldForwardProp forms and element prop filte…
owjs3901 Oct 1, 2026
af1177d
merge: update PR #706 with main ESLint checks
owjs3901 Oct 8, 2026
944e723
merge: update PR #706 with 9a953607
owjs3901 Oct 8, 2026
5a70d2a
merge: update PR #706 with db631c6c
owjs3901 Oct 8, 2026
30247ba
merge: update PR #706 with main
owjs3901 Oct 8, 2026
c7ea90a
merge: update PR #707 with main
owjs3901 Oct 8, 2026
4669b17
merge: update PR #710 with PR #707
owjs3901 Oct 8, 2026
9e4d28a
merge: update PR #711 with PR #710
owjs3901 Oct 8, 2026
c597abb
fix(extractor): filter every transient prop from styled rest spreads
owjs3901 Oct 12, 2026
b77cc5d
docs(migration): document transient props and compile-time forwarding
owjs3901 Oct 12, 2026
e23acb6
merge: update PR #707 with main and preserve inherited captures
owjs3901 Oct 14, 2026
fa11c07
merge: update PR #707 with latest main and exact test hotfixes
owjs3901 Oct 14, 2026
bcd240e
merge: refresh PR #707 with main and cover inherited render paths
owjs3901 Oct 14, 2026
53011cf
fix(extractor): lift capture visitor out of generic method
owjs3901 Oct 14, 2026
a6b4188
merge: update PR #710 with PR #707 and preserve typed retargeting
owjs3901 Oct 14, 2026
497aba0
merge: update PR #711 with PR #710 and preserve captured prop filtering
owjs3901 Oct 14, 2026
7afb427
merge: update PR #774 with PR #711 and bound ordered captures
owjs3901 Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_compose_known_styles.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "css(a, b) composing classes whose styles the build knows (a const bound to css() in the file, or a css() result another module exports with a rule object every value of which is known) merges their styles: a later part's declaration replaces an earlier one's for the same property, selector, breakpoint and layer, also under conditions (css(base, cond && danger)) and for ||/?? parts, where the classes used to be joined and the stylesheet order picked the winner. vanilla-extract style([a, b]) passes each composed style as its own argument, so the later one wins, and a style composed again later (style([a, b, a])) is kept. Classes the build does not know (strings, props, CSS Modules) are kept as they are",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_compose_styled.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "styled(Base) extending a styled component the file binds to a const renders Base's tag directly with Base's styles composed under its own, so the extension's declarations replace Base's for the same property, selector, breakpoint and layer instead of losing to them by stylesheet order; Base's attrs apply before the extension's. attrs merge as styled-components merges them: className is joined with the caller's, style is merged and other props replace earlier ones. On an element, a className or style spread after the explicit prop now wins, as React merges props in the order they are written",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_styled_props.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Styled components no longer pass every prop to the tag they render: `$` props, `theme` and the props their style functions or attrs read are kept away from a tag unless it takes them as attributes, and `shouldForwardProp` (Emotion options or styled-components withConfig) is evaluated at build time. A `shouldForwardProp` the build cannot evaluate is a build error naming the file, line and code, with the forms it accepts. An element using a styled component the file defines drops props the component neither reads nor passes on; spread props are passed as written",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_styled_targets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Styled components render what their `as` prop names, defaulting to the tag or component they were defined with, and pass `forwardedAs` on as `as`, as styled-components and Emotion do; `as` used to reach the rendered tag as an attribute. `Component.withComponent(target)` on a styled component the file binds to a const builds a component rendering the same styles and attrs as `target` (a tag name or a component JSX can name), where it used to call a method the generated function does not have and throw",
"date": "2026-10-01T00:00:00.000Z"
}
9 changes: 9 additions & 0 deletions .changepacks/changepack_log_transient_props.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"changes": {
"apps/landing/package.json": "Patch",
"bindings/devup-ui-wasm/package.json": "Patch",
"packages/react/package.json": "Patch"
},
"note": "Styled components now remove every runtime prop whose name starts with `$` before forwarding a rest spread to a tag or component, including props supplied by callers or attrs. Style and attrs functions can still read transient props, and all other build-time forwarding policies remain unchanged. The styled-components migration guide documents transient prop support. No new build errors",
"date": "2026-10-01T00:00:00.000Z"
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@
const G = styled(motion.div)`
color: red;
`
const H = styled(forwardRef(Field))({ bg: 'red' })

Check warning on line 30 in apps/landing/src/app/(detail)/docs/migration/styled-components/page.mdx

View workflow job for this annotation

GitHub Actions / publish

In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead
```

Any base renders: a tag, a component, a member such as `motion.div`, or any other value, read once where the component is declared. `null`, numbers, booleans and `undefined` never render, so they are build errors, as is a `styled` left uncompiled (`styled('div')` never given styles, `styled.div` exported bare): its import is removed, and that code would throw at runtime.

A `keyframes` or `css` result held in a `const` of the same file is a build-time value, so `` css`animation: ${fadeIn} 1s;` `` and `styled.div({ ['.' + card + ':hover &']: { margin: 0 } })` work. In CSS text a class standing where a declaration would is a mixin, composed with the component's class. One imported from another module is not: its name is only known once that file is built, so declare it where it is used.

Object styles follow the libraries: several style arguments, or an array of them, merge into one rule set where a later declaration replaces an earlier one and nested rules merge; Emotion's options argument (`styled('div', { shouldForwardProp })(...)`) is accepted, and every prop is forwarded. Numbers mean pixels as they do in Emotion and styled-components — `padding: 8` is `8px`, unitless properties such as `lineHeight` stay as written — while Devup UI shorthands such as `p` keep the spacing scale.
Object styles follow the libraries: several style arguments, or an array of them, merge into one rule set where a later declaration replaces an earlier one and nested rules merge; Emotion's options argument (`styled('div', { shouldForwardProp })(...)`) is accepted, and its `shouldForwardProp` is evaluated at build time. Numbers mean pixels as they do in Emotion and styled-components — `padding: 8` is `8px`, unitless properties such as `lineHeight` stay as written — while Devup UI shorthands such as `p` keep the spacing scale.

A style argument can be conditional — `cond && { ... }`, `cond ? { ... } : { ... }`, `value || { ... }` or `value ?? { ... }`, in `styled` as in `css`. Each property it sets becomes a per-property ternary over what came before (`color: cond ? 'blue' : 'red'`), so the result does not depend on the order atomic classes land in the stylesheet; a conditional class joins as `cond ? cls : ''`, and the left side of `||` / `??` joins only when it is a string, as the libraries skip other values. Classes passed beside rule objects compose in `styled` as in `css`.

Expand All @@ -44,7 +44,7 @@

Inside a template, an interpolation standing where a declaration would is a mixin: `${mixin}` and `${(p) => p.active && mixin}` compose their class with the component's. A literal or constant is written into the CSS wherever it stands — a value, a CSS snippet, a selector. Any other interpolation in a selector or a property name is a build error, as are runtime values in `css`, `keyframes` and global styles, which have no element to set a CSS variable on.

`.attrs()` and `.withConfig()` chain onto any factory. Attrs are merged over the props in order, as styled-components does — an object is spread, a function is called with the props; `.withConfig()` only tunes runtime behavior, so it is dropped:
`.attrs()` and `.withConfig()` chain onto any factory. Attrs are merged over the props in order, as styled-components does — an object is spread, a function is called with the props. In `.withConfig()`, `shouldForwardProp` is evaluated at build time; `displayName`, `componentId` and the other options are ignored and add no runtime behavior:

```tsx
const Button = styled.button
Expand All @@ -66,6 +66,23 @@
const Card = Emotion.div`color: red;`
```

## Transient props

Props whose names start with `$` are transient, as in styled-components: style functions read them, but a styled component never passes them on to what it renders — a tag, a component or what `as` names. That includes `$` props a runtime spread brings and those attrs create, even when `shouldForwardProp` allows them:

```tsx
const Title = styled.h1`
color: ${(p) => p.$tone};
`

function Heading(props) {
return <Title {...props} />
}

// `$tone` sets the color; the <h1> gets `id` but neither `$tone` nor `$track`
const heading = <Heading $tone="teal" $track="hero" id="intro" />
```

## Theme interpolation

`ThemeProvider` redefines CSS variables on a `display: contents` wrapper, so it scopes the theme to its subtree without touching layout. Interpolations that read the theme are resolved at build time — no runtime lookup:
Expand Down Expand Up @@ -128,7 +145,7 @@
| `css` prop, `jsx`, JSX runtime pragmas | compiled to `className` and `style` |
| `ClassNames` | replaced by what its child renders, each `css` / `cx` call compiled to classes |
| Component selectors (`${Child}`) | a marker class on the selected component |
| `.attrs()`, `.withConfig()` | attrs merged over props; `withConfig` dropped |
| `.attrs()`, `.withConfig()` | attrs merged over props; build-time `shouldForwardProp`, other options ignored |

## Limitations

Expand Down
Loading
Loading