Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
e6d3b70
refactor(css): use one parser and resolve each style once
mateo-m Aug 25, 2026
7dae1eb
feat(motion): animate height to auto
mateo-m Aug 25, 2026
196304b
fix(motion): measure auto height at the resolved width
mateo-m Aug 25, 2026
773027d
build: point the zed submodule at the mateo-m fork
mateo-m Aug 25, 2026
6a0eb3d
build: use ssh for the zed submodule url
mateo-m Aug 25, 2026
ab004c2
feat(style): read width and height as css lengths
mateo-m Aug 25, 2026
c0dde15
fix(motion): collapse a height from where auto left it
mateo-m Aug 25, 2026
d595afc
fix(react): accept key on a built-in tag
mateo-m Aug 25, 2026
9d09bca
feat(react): take root options in render and expose the renderer
mateo-m Aug 25, 2026
5b67080
feat(examples): add a demo of css values, classes and motion
mateo-m Aug 25, 2026
b410721
fix(examples): sign compiled apps and take the entry as an argument
mateo-m Aug 25, 2026
a21aa27
fix(native): copy the selection with cmd-c
mateo-m Aug 26, 2026
73b6534
fix(native): bend a growing height toward its new rows
mateo-m Aug 26, 2026
b673e71
feat(native): add linear gradients, corner-shape and cursor keywords
mateo-m Aug 26, 2026
11fe1ac
feat(native): ease the mix between two gradient stops
mateo-m Aug 26, 2026
673def3
feat(native): bump zed for effect layers and rounded scroll clipping
mateo-m Aug 26, 2026
623723c
feat(native): map filter, backdrop-filter, mask-image and blend modes…
mateo-m Aug 26, 2026
c40a567
feat(native): paint scrollbars and scroll an element into view
mateo-m Aug 26, 2026
b8caa85
chore: merge upstream main
mateo-m Aug 26, 2026
43baca4
chore: merge feat/css-values-and-classname
mateo-m Aug 26, 2026
43a92ec
fix(native): paint a gradient easing as its half-point hint
mateo-m Aug 26, 2026
6d05970
chore: merge feat/eased-gradients
mateo-m Aug 26, 2026
cd966db
chore: merge feat/layer-effects
mateo-m Aug 26, 2026
5aa4799
docs(react): explain resolveClassName routing and fix a doc comment
mateo-m Aug 26, 2026
3f61a02
chore: merge feat/css-values-and-classname
mateo-m Aug 26, 2026
fe9f050
fix(demo): move the blur header demo off the gradients branch
mateo-m Aug 26, 2026
b2c8450
chore: merge feat/eased-gradients
mateo-m Aug 26, 2026
86cfa6b
feat(demo): show the blur header on the effect layers branch
mateo-m Aug 26, 2026
bdc28a6
fix(styles): type overscrollBehavior and split its shorthand without …
mateo-m Aug 26, 2026
802d1dc
chore: merge feat/layer-effects
mateo-m Aug 26, 2026
bd58a5e
fix(scrollbars): take auto or exactly two colors for scrollbar-color
mateo-m Aug 26, 2026
140869a
fix(examples): set windows.icon only when the ico file exists
mateo-m Aug 26, 2026
67c924f
chore: merge feat/css-values-and-classname
mateo-m Aug 26, 2026
eb2f077
chore: merge feat/eased-gradients
mateo-m Aug 26, 2026
278d2d6
chore: merge feat/layer-effects
mateo-m Aug 26, 2026
42131c8
fix(ci): build before typecheck and give the alpha colour tests one 8…
mateo-m Aug 26, 2026
f1876fb
chore: merge feat/css-values-and-classname
mateo-m Aug 26, 2026
24c6ada
chore: merge feat/eased-gradients
mateo-m Aug 26, 2026
51a7264
chore: merge feat/layer-effects
mateo-m Aug 26, 2026
bff9e3a
fix(ci): run the react typecheck after the build and make it clean first
mateo-m Aug 26, 2026
dd72a5f
chore: merge feat/css-values-and-classname
mateo-m Aug 26, 2026
ced75d1
chore: merge feat/eased-gradients
mateo-m Aug 26, 2026
c544c83
chore: merge feat/layer-effects
mateo-m Aug 26, 2026
d6360d6
chore(zed): move the submodule to the corner shapes branch tip
mateo-m Aug 26, 2026
96a0006
chore: merge feat/css-values-and-classname
mateo-m Aug 26, 2026
acca1f9
chore: merge feat/eased-gradients
mateo-m Aug 26, 2026
aa1ceb5
chore: merge feat/layer-effects
mateo-m Aug 26, 2026
2993b61
chore(zed): move the submodule to the corner shapes branch tip
mateo-m Aug 26, 2026
9a12087
chore: merge feat/css-values-and-classname
mateo-m Aug 26, 2026
a78d9ac
chore: merge feat/eased-gradients
mateo-m Aug 26, 2026
09a7898
chore: merge feat/layer-effects
mateo-m Aug 26, 2026
1f105d0
feat(demo): add a scrollbars panel
mateo-m Aug 27, 2026
498d036
fix(demo): import the test renderer from the testing entry
mateo-m Aug 27, 2026
3b1a5ad
fix(demo): window the long list through virtual-list
mateo-m Aug 27, 2026
bda0751
chore: merge feat/layer-effects
mateo-m Aug 27, 2026
65b4cec
chore: merge feat/css-values-and-classname
mateo-m Aug 27, 2026
15360aa
chore: merge feat/eased-gradients
mateo-m Aug 27, 2026
4e6095f
fix(native): keep a scroll box's recorded bounds out of its own offset
mateo-m Aug 27, 2026
c752c27
docs(css): revision 4 puts structural variants and child utilities in…
mateo-m Aug 28, 2026
47dfc8b
feat(native): index and child selector conditions from a class
mateo-m Aug 28, 2026
3580b3d
feat(react): selectors field on StyleDesc, merged per rule
mateo-m Aug 28, 2026
bb33f83
feat(demo): selector variants and a selectors panel
mateo-m Aug 28, 2026
de9d657
docs(plan): child rules ride the walk context, not Inherited
mateo-m Aug 28, 2026
c5cd96b
chore: merge feat/css-values-and-classname
mateo-m Aug 28, 2026
3c04f9b
chore: merge feat/eased-gradients
mateo-m Aug 28, 2026
ed471d2
chore: merge feat/layer-effects
mateo-m Aug 28, 2026
770a777
fix(demo): write test screenshots into the OS temp directory
mateo-m Aug 29, 2026
bdb6350
chore: merge feat/css-values-and-classname
mateo-m Aug 29, 2026
ccb4b5f
chore: merge feat/eased-gradients
mateo-m Aug 29, 2026
a84b4ff
chore: merge feat/layer-effects
mateo-m Aug 29, 2026
6ecc558
fix(native): child rules skip raw text nodes, as selectors do on the web
mateo-m Aug 29, 2026
6eee50e
chore: merge feat/css-values-and-classname
mateo-m Aug 29, 2026
2fda17b
chore: merge feat/eased-gradients
mateo-m Aug 29, 2026
f9c65f8
chore: merge feat/layer-effects
mateo-m Aug 29, 2026
26d58a0
chore: merge upstream main
mateo-m Sep 4, 2026
3ecdb80
chore: merge feat/css-values-and-classname
mateo-m Sep 4, 2026
a91ef51
chore: merge feat/eased-gradients
mateo-m Sep 4, 2026
8581321
chore: merge feat/layer-effects
mateo-m Sep 4, 2026
5170690
chore: merge upstream main
mateo-m Sep 4, 2026
cb7f139
chore: merge feat/css-values-and-classname
mateo-m Sep 4, 2026
d81353d
chore: merge feat/eased-gradients
mateo-m Sep 4, 2026
58174d7
chore: merge feat/layer-effects
mateo-m Sep 4, 2026
01cf29c
feat(css): read drop-shadow() into a layer shadow
mateo-m Sep 4, 2026
33d9ea1
chore: merge feat/layer-effects
mateo-m Sep 4, 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
15 changes: 15 additions & 0 deletions .changeset/animate-height-to-auto.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@gpuix/native": patch
"@gpuix/react": patch
---

Animate `height` to `auto`

A motion `height` now takes `"auto"` at either end of the animation. `auto` is
the height the content takes, and only layout knows that number, so the element
measures its content every frame and interpolates against the measurement. An
animation that opens a panel follows content that changes while it runs.

The measurement runs at the width the element really gets, whether that width
comes from a declared length, from `flex`, from a percentage or from a stretched
cross axis. Text wraps the way it will on screen.
14 changes: 14 additions & 0 deletions .changeset/bend-toward-content-that-grows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@gpuix/native": patch
"@gpuix/react": patch
---

Keep a `height` animation smooth when the content changes while it runs.

A `motion` height with `auto` at an end resolves against the content every
frame. Content that grew part way through moved the height with it, so a box
opening toward two rows jumped when two more arrived. The animation now keeps
the height that was on screen and bends the rest of the curve toward the new
content, ending when it would have ended.

At rest, `auto` still follows the content at once.
33 changes: 33 additions & 0 deletions .changeset/class-name-prop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
"@gpuix/react": minor
---

Add a `className` prop and a resolver seam for it.

`className` is `string | undefined` on every element, so `clsx` and `cn` need no
special handling. GPUIX ships no resolver. A root registers one:

```ts
createRoot(renderer, { resolveClassName })
```

The resolver reads one class token, such as `p-4`, and returns the `StyleDesc`
it declares, or `null` for a token it does not know. `@gpuix/tailwind` will be
one. Without a resolver a `className` does nothing and the root warns once.

A declaration in `style` beats one from a class, key by key, and in the hover
and active states too. [CSS Style Attributes][spec] gives the attribute "a
specificity higher than any selector", so an element with
`style={{ backgroundColor: "red" }}` stays red under a `hover:bg-blue-500`
class, the way a browser keeps it red.

Caching is per token. `clsx("p-4", a && "bg-blue-500", b && "text-lg")` writes up
to eight strings from three tokens, and the resolver sees three. A bounded cache
over whole strings sits in front of it, so a repeated string skips both the split
and the merge.

`hideInstance` and `unhideInstance` now send the class-derived style as well.
React drives that pair for Suspense, and before this an element that suspended
came back with only its inline style.

[spec]: https://www.w3.org/TR/css-style-attr/#cascading
14 changes: 14 additions & 0 deletions .changeset/clip-rounded-scroll-boxes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@gpuix/native": patch
---

Clip a rounded scroll box at its corner arcs, and end the scroll range at the far edge of the content.

A box that hides or scrolls its overflow used to clip its children to a
rectangle, so a child could poke out over a rounded corner by a few
pixels. Such a box now clips on the GPU with the same rounded shape it
paints. The scroll range used to count the start-side padding twice,
which let a padded scroll box scroll sideways past its content and show
a bar it did not need. The range now ends at the far edge of the
children plus the end-side padding, as in CSS, and an absolutely
positioned child ends the range at its own edge.
16 changes: 16 additions & 0 deletions .changeset/close-a-height-from-where-it-is.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@gpuix/native": patch
"@gpuix/react": patch
---

Collapse a `height: auto` animation from the height on screen.

Opening a panel animated to the height the content takes, but closing it snapped
shut in one frame. Every frame of the closing animation measured zero.

A motion height now carries a number of pixels and a share of the height the
content takes. `"auto"` is the whole share, a length is none of it, and a frame
between the two is part of each, so `"auto"` and a length are the same kind of
value. A collapse starts from the height that is on screen, and pressing the
button again part way through turns back from the frame it reached instead of
jumping.
14 changes: 14 additions & 0 deletions .changeset/corner-shape.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@gpuix/native": minor
"@gpuix/react": minor
---

Add `corner-shape` from CSS Borders 4.

`cornerShape` takes `round`, `squircle`, `square`, `bevel`, `scoop`, `notch`
or `superellipse(K)`, one to four values like `borderRadius`. The per-corner
and per-side longhands, their logical names, and the `corner*` shorthands
that pair a radius with a shape are read too. The quad shader draws each
corner as a superellipse, so a squircle costs the same as a circle. Motion
interpolates `cornerShape` in the half-corner space the spec names. Shadows
and image corners still follow the round shape.
17 changes: 17 additions & 0 deletions .changeset/css-lengths-and-calc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@gpuix/native": minor
"@gpuix/react": minor
---

Read a `lineHeight` string the way CSS reads it, and accept `calc()` in any
length.

A JS number keeps the old meaning, so `lineHeight: 20` is still 20 px. A string
follows CSS: `"2.5"` is a multiple of the font size, `"250%"` is the same
multiple, and `"24px"` is 24 px. Anything at or below zero declares nothing.

Every length also takes `calc()`, `min()`, `max()` and `clamp()`, folded by
lightningcss while the value parses. `rem` becomes pixels first, against the
window rem size, so `calc(1rem + 4px)` reaches a single number. This is what
makes the Tailwind spacing scale work, because every step in it is
`calc(var(--spacing) * n)`.
11 changes: 11 additions & 0 deletions .changeset/cursor-keywords.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@gpuix/native": patch
"@gpuix/react": patch
---

Read every CSS `cursor` keyword and show the I-beam over selectable text.

`cursor` only knew `pointer` and `default`. It now maps `text`, `grab`,
`not-allowed`, the resize directions and the other keywords to their native
cursors. Selectable text shows the I-beam when no ancestor sets a cursor,
which is what `cursor: auto` does on the web.
18 changes: 18 additions & 0 deletions .changeset/custom-properties-and-var.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@gpuix/native": minor
"@gpuix/react": minor
---

Support CSS custom properties in the `style` prop. A `--name` key declares a value for the element and everything below it, and `var(--name)` reads it, with fallbacks including the empty one Tailwind writes as `var(--tw-ring-inset,)`. A missing variable with no fallback drops the declaration, which is what CSS calls invalid at computed-value time.

Support `currentColor`. It reads the computed `color`, whether the element declares it or an ancestor does.

Take text in every numeric style field. `padding`, `borderWidth`, `fontSize` and the other 33 numeric fields now accept `number | string`, so `8`, `"8px"` and `"var(--pad)"` all mean the same thing. A unit the renderer cannot read, such as `2rem`, drops the declaration instead of painting the number as pixels.

Type custom properties with a pattern index signature, so `"-pad"` is a type error rather than a name that silently never resolves. `hover` and `active` reject them, because a state has no cascade of its own to declare into.

Keep the resolved-style cache correct without giving up on elements that use no variable. A resolution that reads nothing inherited holds under every cascade, so only a `var()` or `currentColor` reader is ever invalidated by an ancestor.

Stop re-resolving an element that has no `style` prop. The reconciler skips the call for an empty style at mount but sends `{}` on every update, so the first update on every unstyled element read as a change.

Typecheck the `style` prop rules. `src/__tests__` was excluded from every tsconfig, so nothing checked the type assertions. `bun run typecheck` covers them now, and CI runs it.
15 changes: 15 additions & 0 deletions .changeset/eased-gradients.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@gpuix/native": minor
"@gpuix/react": minor
---

Ease the mix between two gradient stops.

An `<easing-function>` between two colour stops bends the mix, following the
CSSWG proposal in csswg-drafts issue 1332: `linear-gradient(to top, black,
ease-in-out, transparent)`. `ease`, `ease-in`, `ease-out`, `ease-in-out` and
`cubic-bezier()` are read. The easing paints as the GPUI colour hint whose
curve crosses one half at the same place, so the paint agrees with the easing
at both ends and at the half-way point. A straight fade to transparent looks
dense near the solid stop and thin near the clear one. An eased one reads as
one smooth fall-off.
27 changes: 27 additions & 0 deletions .changeset/index-and-child-selectors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"@gpuix/native": patch
"@gpuix/react": patch
---

Index and child selector conditions from a class

A class resolver can now return `selectors`, a list of `{ on, style }` rules.
The spellings form a closed set. `:first-child`, `:last-child`,
`:nth-child(odd)`, `:nth-child(even)` and `:only-child` read the position of
the element among its siblings. The walk knows that position at build time, so
they cost no event and no measurement. `& > *` and `& > :not(:last-child)` sit
on the parent and style its direct children, which is what `space-y-*` and
`divide-*` compile to. `& *` reaches the whole subtree. An unknown spelling
warns once and drops.

Every rule applies with specificity zero, as `:where()` does on the web. A
declaration the child makes itself wins over a rule from the parent, and the
`style` prop wins over an index rule key by key. Two tokens on the same
selector merge into one rule, the later token winning. The `style` prop type
excludes `selectors`, because a style attribute holds declarations, not
selectors.

Two places sit outside the tree walk. A virtual-list row builds on its own, so
it has no child position and the index conditions do not apply to it. A custom
element resolves its own style, so the rules of a parent stop at its border.
Verified on macOS. The Windows and Linux paths did not run here.
15 changes: 15 additions & 0 deletions .changeset/key-on-a-built-in-tag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@gpuix/react": patch
---

Accept `key` on a built-in tag under `jsxImportSource: "@gpuix/react"`.

`<text key={id} />` failed to typecheck. TypeScript reads
`JSX.IntrinsicAttributes` for a component tag but not for a built-in one, so
`key` has to sit in the props of each tag. React does the same for every DOM
tag. The props of a tag stay closed, so a name that is not a prop is still an
error.

`jsx-runtime.d.ts` also imported its types with no file extension, which
`moduleResolution: "nodenext"` cannot resolve. Under `skipLibCheck` that import
became `any` and every tag took any prop at all. The import now names the file.
23 changes: 23 additions & 0 deletions .changeset/layer-effects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@gpuix/native": minor
"@gpuix/react": minor
---

Add `filter`, `backdrop-filter`, `mask-image`, `mix-blend-mode`, `background-blend-mode` and `overscroll-behavior`.

An element with any of the first four paints itself and its children into a
texture of its own, and the GPU paints that texture over the frame with the
effect. `blur()` is a two-pass Gaussian on a shrunk texture, so a wide blur
costs the same as a narrow one. `drop-shadow()` blurs the alpha of the
element's picture and paints it under the element in its colour. The other
filter functions fold into one colour matrix. `url()` is the one function
not painted. `mask-image` takes a gradient and keeps each pixel by its
alpha. Over a `backdrop-filter: blur()` the alpha scales the blur radius
instead, so an eased mask gives a progressive blur like the iOS 26
navigation bar. CSS would fade a sharp copy over the blur there. All sixteen blend modes of Compositing and Blending 1 plus
`plus-lighter` are painted. `background-image` now paints over
`background-color` instead of replacing it. A scroll box keeps a wheel event
it can scroll with, and `overscroll-behavior: contain` keeps it at the end
of the box too. The Metal, DirectX and wgpu renderers all paint the
effects. Only WebGL, which has no storage buffers, paints the content of
an effect layer with no effect.
13 changes: 13 additions & 0 deletions .changeset/linear-gradient-backgrounds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@gpuix/native": minor
"@gpuix/react": minor
---

Paint `linear-gradient()` backgrounds.

`background` and the new `backgroundImage` take a `linear-gradient()` with
an angle, a side or a corner, up to eight colour stops, percentage positions
and colour hints. The engine fixes the stops up the way CSS Images 3 says and
the quad shader paints them, so a gradient costs one quad like a flat colour.
`backgroundImage` wins over `backgroundColor`, since a box paints one fill.
Stop lengths, radial and conic gradients are not read yet.
19 changes: 19 additions & 0 deletions .changeset/motion-keeps-the-style-cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@gpuix/native": patch
"@gpuix/react": patch
---

Keep the resolved-style cache for an element that animates.

A motion frame drives eight numbers: `width`, `height`, `top`, `right`,
`bottom`, `left`, `borderRadius` and `opacity`. It used to drive them by
copying the element's whole style, writing the numbers into the copy and
resolving that copy. Every declaration the element made was reparsed on every
frame of the animation to change one value.

None of the eight reads a custom property, `currentColor` or the font size, so
each one now lands on the element after the cached resolution does. An animated
element resolves its style once, the same as any other element.

Custom elements resolve a style themselves, so a motion frame still reaches
them folded into one.
35 changes: 35 additions & 0 deletions .changeset/read-colours-with-lightningcss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
"@gpuix/native": minor
---

Read every colour with the same CSS parser the rest of the engine uses.

GPUIX held two CSS parsers. Colour went through csscolorparser, and every other
value went through lightningcss. The two agreed on most colours and disagreed at
the edges, which is where the specifications matter most. Colour now goes through
lightningcss as well, and csscolorparser is gone.

Three colour syntaxes work that did not before:

- `color-mix()`, which Tailwind writes for every opacity modifier such as
`bg-red-500/50`
- `light-dark()`, which reads the appearance of the window
- `color()`, such as `color(display-p3 1 0 0)`

`currentColor` now resolves wherever it sits, including inside `light-dark()`.
Before, only a value that was exactly `currentColor` resolved, and a nested one
made the whole declaration invalid.

Four syntaxes no longer parse, because no CSS specification defines any of them:

- `hsv()` and `hsva()`
- `hwba()`
- hex with no leading `#`, such as `ff0000ff`

A declaration that uses one of these is invalid, so the property keeps the value
it would have had with no declaration at all. Write `hwb()` instead of `hwba()`,
and add the `#` to a bare hex colour. There is no CSS replacement for `hsv()`,
so convert the value to `hsl()` or `hwb()`.

Alpha on `rgb()`, `hsl()` and `hwb()` now rounds to 8 bits, because that is how
lightningcss holds an sRGB colour. The wider colour spaces keep the exact value.
26 changes: 26 additions & 0 deletions .changeset/read-styles-without-buffering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
"@gpuix/native": patch
---

Read a `style` prop without buffering it, and stop carrying it by value.

`StyleDesc` used `#[serde(flatten)]` to collect custom properties, and `Numeric`
and `FontWeightValue` used `#[serde(untagged)]`. Each of those makes serde read
the whole value into an intermediate tree before it looks at one field, and
every `setStyle` call paid for it. All three now have a hand written
`Deserialize`. A macro declares `StyleDesc` and its reader from one field list,
so the name JS writes and the name Rust reads come from the same literal.

The struct is 1,728 bytes, so the read now writes straight into a box rather
than building on the stack and copying. Measured over 200,000 parses:

| shape | before | after | no flatten, no untagged |
| --- | --- | --- | --- |
| two fields | 320 ns | 84 ns | 74 ns |
| eleven fields | 531 ns | 399 ns | 341 ns |

A retained element holds that box instead of the struct, which takes it from
2,000 bytes to 280. A tree of 10,000 elements was carrying 17 MB of mostly
empty styles. Each op in a batched mutation shrank the same way.

The wire format is unchanged.
17 changes: 17 additions & 0 deletions .changeset/render-takes-a-class-resolver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@gpuix/react": minor
---

Pass a class resolver to `render()`.

`createRoot` took `resolveClassName`, but `render()` did not, so an application
that wanted `className` had to open the window and build the root by hand.
`render()` now takes the same root options:

```ts
render(<App />, { resolveClassName, title: "Demo" })
```

A `Root` also carries the `renderer` it draws on, so an application that lets
`render()` open the window can still reach the handle afterwards. Inside the
tree, `useGpuixRequired()` gives the same renderer.
10 changes: 10 additions & 0 deletions .changeset/resolve-styles-once.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@gpuix/native": minor
"@gpuix/react": patch
---

Resolve each element style once instead of on every frame. GPUI rebuilds its element tree on every frame, so the renderer used to run all 52 style branches again for styles that had not changed since the last update from React. The resolved style is now kept on the retained element and dropped when the style changes.

Apply the `visibility` style. It reached the native side but nothing read it, so `visibility: "hidden"` did nothing.

Keep the element style when React hides an element. `hideInstance` replaced the whole style with `visibility: "hidden"`, which dropped the layout box and every other style on the element. The `hover` and `active` styles are dropped while an element is hidden, so neither can paint an element React asked to hide.
Loading