From 1c044b089baff988cbd49da971e485f3f848ccc4 Mon Sep 17 00:00:00 2001
From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date: Sat, 27 Jun 2026 23:13:36 -0700
Subject: [PATCH 1/6] fix: resolve inline style= declarations in
getComputedStyle
---
src/browser/tests/element/styles.html | 19 +++++++++++++++++++
.../webapi/css/CSSStyleDeclaration.zig | 16 ++++++++++++++++
2 files changed, 35 insertions(+)
diff --git a/src/browser/tests/element/styles.html b/src/browser/tests/element/styles.html
index 05dfa34f0..42250d168 100644
--- a/src/browser/tests/element/styles.html
+++ b/src/browser/tests/element/styles.html
@@ -198,3 +198,22 @@
testing.expectEqual('1', divStyle.opacity);
}
+
+
diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig
index f1c915506..a106731fd 100644
--- a/src/browser/webapi/css/CSSStyleDeclaration.zig
+++ b/src/browser/webapi/css/CSSStyleDeclaration.zig
@@ -96,6 +96,9 @@ pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const
const prop = self.findProperty(wrapped) orelse {
// Only return default values for computed styles
if (self._is_computed) {
+ if (self._element) |element| {
+ if (findInlineValue(element, normalized, frame)) |value| return value;
+ }
return getDefaultPropertyValue(self, wrapped);
}
return "";
@@ -236,6 +239,19 @@ pub fn findProperty(self: *const CSSStyleDeclaration, name: String) ?*Property {
return null;
}
+fn findInlineValue(element: *const Element, normalized_name: []const u8, frame: *Frame) ?[]const u8 {
+ const attr_value = element.getAttributeSafe(comptime .wrap("style")) orelse return null;
+ var it = CssParser.parseDeclarationsList(attr_value);
+ var result: ?[]const u8 = null;
+ while (it.next()) |declaration| {
+ if (std.ascii.eqlIgnoreCase(declaration.name, normalized_name)) {
+ // Keep the last matching declaration, matching CSS declaration order.
+ result = normalizePropertyValue(frame.call_arena, normalized_name, declaration.value) catch declaration.value;
+ }
+ }
+ return result;
+}
+
fn normalizePropertyName(name: []const u8, buf: []u8) []const u8 {
if (name.len > buf.len) {
log.info(.dom, "css.long.name", .{ .name = name });
From 16b16564d535b14f191ca9133c2a83837d275b34 Mon Sep 17 00:00:00 2001
From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date: Sat, 27 Jun 2026 23:29:41 -0700
Subject: [PATCH 2/6] css: honor !important for duplicate inline declarations
in computed style
---
src/browser/tests/element/styles.html | 6 ++++++
src/browser/webapi/css/CSSStyleDeclaration.zig | 13 ++++++++++---
2 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/src/browser/tests/element/styles.html b/src/browser/tests/element/styles.html
index 42250d168..adc9d395f 100644
--- a/src/browser/tests/element/styles.html
+++ b/src/browser/tests/element/styles.html
@@ -215,5 +215,11 @@
const plainDiv = document.createElement('div');
document.body.appendChild(plainDiv);
testing.expectEqual('block', window.getComputedStyle(plainDiv).getPropertyValue('display'));
+
+ // A normal declaration must not override an earlier !important one.
+ const impDiv = document.createElement('div');
+ impDiv.setAttribute('style', 'color: red !important; color: blue');
+ document.body.appendChild(impDiv);
+ testing.expectEqual('red', window.getComputedStyle(impDiv).getPropertyValue('color'));
}
diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig
index a106731fd..f58b7fd74 100644
--- a/src/browser/webapi/css/CSSStyleDeclaration.zig
+++ b/src/browser/webapi/css/CSSStyleDeclaration.zig
@@ -243,11 +243,18 @@ fn findInlineValue(element: *const Element, normalized_name: []const u8, frame:
const attr_value = element.getAttributeSafe(comptime .wrap("style")) orelse return null;
var it = CssParser.parseDeclarationsList(attr_value);
var result: ?[]const u8 = null;
+ var result_important = false;
while (it.next()) |declaration| {
- if (std.ascii.eqlIgnoreCase(declaration.name, normalized_name)) {
- // Keep the last matching declaration, matching CSS declaration order.
- result = normalizePropertyValue(frame.call_arena, normalized_name, declaration.value) catch declaration.value;
+ if (!std.ascii.eqlIgnoreCase(declaration.name, normalized_name)) {
+ continue;
}
+ // A later declaration wins, except a normal one never overrides an
+ // earlier !important declaration (CSS cascade precedence).
+ if (result_important and !declaration.important) {
+ continue;
+ }
+ result = normalizePropertyValue(frame.call_arena, normalized_name, declaration.value) catch declaration.value;
+ result_important = declaration.important;
}
return result;
}
From 0614795fc5b54213c7f3559aa5a7ab524aa30a5a Mon Sep 17 00:00:00 2001
From: Pierre Tachoire
Date: Mon, 29 Jun 2026 14:37:54 +0200
Subject: [PATCH 3/6] css: resolve computed inline styles via StyleManager
getComputedStyle now reads inline values from the element's parsed
el.style through StyleManager.inlineStyleValue instead of re-parsing the
style= attribute, so computed and inline values share one source of
truth. Move the !important cascade precedence to the shared parse path
(applyParsedDeclaration) so el.style resolves duplicate declarations
correctly too.
---
src/browser/StyleManager.zig | 9 ++++
src/browser/tests/element/styles.html | 4 +-
.../webapi/css/CSSStyleDeclaration.zig | 41 ++++++++-----------
3 files changed, 30 insertions(+), 24 deletions(-)
diff --git a/src/browser/StyleManager.zig b/src/browser/StyleManager.zig
index 79936e1d3..d96eba0b0 100644
--- a/src/browser/StyleManager.zig
+++ b/src/browser/StyleManager.zig
@@ -871,6 +871,15 @@ fn getInlineStyleProperty(el: *Element, property_name: String, frame: *Frame) ?*
return style.asCSSStyleDeclaration().findProperty(property_name);
}
+/// Resolved value of an element's inline `style=` declaration for `property_name`,
+/// or null when the element has no such declaration. Reads the element's parsed
+/// inline style (the same source `el.style` exposes), so `getComputedStyle` and
+/// `el.style` agree on inline values instead of resolving them independently.
+pub fn inlineStyleValue(self: *StyleManager, el: *Element, property_name: String) ?[]const u8 {
+ const property = getInlineStyleProperty(el, property_name, self.frame) orelse return null;
+ return property._value.str();
+}
+
const testing = @import("../testing.zig");
test "StyleManager: computeSpecificity: element selector" {
// div -> (0, 0, 1)
diff --git a/src/browser/tests/element/styles.html b/src/browser/tests/element/styles.html
index adc9d395f..b4e81aa2b 100644
--- a/src/browser/tests/element/styles.html
+++ b/src/browser/tests/element/styles.html
@@ -216,10 +216,12 @@
document.body.appendChild(plainDiv);
testing.expectEqual('block', window.getComputedStyle(plainDiv).getPropertyValue('display'));
- // A normal declaration must not override an earlier !important one.
+ // A normal declaration must not override an earlier !important one, and the
+ // computed and inline (el.style) paths must agree on the resolved value.
const impDiv = document.createElement('div');
impDiv.setAttribute('style', 'color: red !important; color: blue');
document.body.appendChild(impDiv);
testing.expectEqual('red', window.getComputedStyle(impDiv).getPropertyValue('color'));
+ testing.expectEqual('red', impDiv.style.getPropertyValue('color'));
}
diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig
index f58b7fd74..5cf1c21b2 100644
--- a/src/browser/webapi/css/CSSStyleDeclaration.zig
+++ b/src/browser/webapi/css/CSSStyleDeclaration.zig
@@ -49,7 +49,7 @@ pub fn init(element: ?*Element, is_computed: bool, frame: *Frame) !*CSSStyleDecl
if (el.getAttributeSafe(comptime .wrap("style"))) |attr_value| {
var it = CssParser.parseDeclarationsList(attr_value);
while (it.next()) |declaration| {
- try self.setPropertyImpl(declaration.name, declaration.value, declaration.important, frame);
+ try self.applyParsedDeclaration(declaration, frame);
}
}
}
@@ -97,7 +97,9 @@ pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const
// Only return default values for computed styles
if (self._is_computed) {
if (self._element) |element| {
- if (findInlineValue(element, normalized, frame)) |value| return value;
+ // Resolve inline `style=` declarations through the element's
+ // parsed inline style, so computed values match `el.style`.
+ if (frame._style_manager.inlineStyleValue(element, wrapped)) |value| return value;
}
return getDefaultPropertyValue(self, wrapped);
}
@@ -127,6 +129,19 @@ pub fn setProperty(self: *CSSStyleDeclaration, property_name: []const u8, value:
try self.syncStyleAttribute(frame);
}
+// Apply one declaration parsed from a `style=` block. Unlike the imperative
+// setProperty path, within a single declaration block a normal declaration must
+// not override an earlier !important one (CSS cascade precedence).
+fn applyParsedDeclaration(self: *CSSStyleDeclaration, declaration: CssParser.Declaration, frame: *Frame) !void {
+ if (!declaration.important) {
+ const normalized = normalizePropertyName(declaration.name, &frame.buf);
+ if (self.findProperty(.wrap(normalized))) |existing| {
+ if (existing._important) return;
+ }
+ }
+ try self.setPropertyImpl(declaration.name, declaration.value, declaration.important, frame);
+}
+
fn setPropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, value: []const u8, important: bool, frame: *Frame) !void {
if (value.len == 0) {
_ = try self.removePropertyImpl(property_name, frame);
@@ -210,7 +225,7 @@ pub fn setCssText(self: *CSSStyleDeclaration, text: []const u8, frame: *Frame) !
// Parse and set new properties
var it = CssParser.parseDeclarationsList(text);
while (it.next()) |declaration| {
- try self.setPropertyImpl(declaration.name, declaration.value, declaration.important, frame);
+ try self.applyParsedDeclaration(declaration, frame);
}
try self.syncStyleAttribute(frame);
}
@@ -239,26 +254,6 @@ pub fn findProperty(self: *const CSSStyleDeclaration, name: String) ?*Property {
return null;
}
-fn findInlineValue(element: *const Element, normalized_name: []const u8, frame: *Frame) ?[]const u8 {
- const attr_value = element.getAttributeSafe(comptime .wrap("style")) orelse return null;
- var it = CssParser.parseDeclarationsList(attr_value);
- var result: ?[]const u8 = null;
- var result_important = false;
- while (it.next()) |declaration| {
- if (!std.ascii.eqlIgnoreCase(declaration.name, normalized_name)) {
- continue;
- }
- // A later declaration wins, except a normal one never overrides an
- // earlier !important declaration (CSS cascade precedence).
- if (result_important and !declaration.important) {
- continue;
- }
- result = normalizePropertyValue(frame.call_arena, normalized_name, declaration.value) catch declaration.value;
- result_important = declaration.important;
- }
- return result;
-}
-
fn normalizePropertyName(name: []const u8, buf: []u8) []const u8 {
if (name.len > buf.len) {
log.info(.dom, "css.long.name", .{ .name = name });
From 96e23f78e8151ce2cc3f894c9dfd001865ac4c89 Mon Sep 17 00:00:00 2001
From: Karl Seguin
Date: Tue, 30 Jun 2026 12:50:50 +0800
Subject: [PATCH 4/6] perf: Skip CSSStyleProperty creation if there is no style
attribute
Was reviewing https://github.com/lightpanda-io/browser/pull/2836 and realized
the StyleManager's getInlineStyleProperty could be optimized to avoid creating
the CSSStyleProperties in the case where there's no style attribute.
---
src/browser/StyleManager.zig | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/browser/StyleManager.zig b/src/browser/StyleManager.zig
index 79936e1d3..92d3a32e7 100644
--- a/src/browser/StyleManager.zig
+++ b/src/browser/StyleManager.zig
@@ -864,6 +864,10 @@ const CheckVisibilityOptions = struct {
const INLINE_PRIORITY: u64 = std.math.maxInt(u64);
fn getInlineStyleProperty(el: *Element, property_name: String, frame: *Frame) ?*CSSStyleProperty {
+ if (!el.hasAttributeSafe(comptime .wrap("style"))) {
+ // cheap guard to avoid creating the CSSStyleProperty if there is no style attribute
+ return null;
+ }
const style = el.getOrCreateStyle(frame) catch |err| {
log.err(.browser, "StyleManager getOrCreateStyle", .{ .err = err });
return null;
From 87b93faf913259408a597c7d7977c68e1d50446a Mon Sep 17 00:00:00 2001
From: Karl Seguin
Date: Tue, 30 Jun 2026 20:40:36 +0800
Subject: [PATCH 5/6] minor: ignore test server error when writing internal
server error
---
src/TestHTTPServer.zig | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/TestHTTPServer.zig b/src/TestHTTPServer.zig
index d2d00a077..73ee07907 100644
--- a/src/TestHTTPServer.zig
+++ b/src/TestHTTPServer.zig
@@ -93,7 +93,7 @@ fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !voi
error.BrokenPipe => {},
else => {
std.debug.print("test http error '{s}': {}\n", .{ req.head.target, err });
- try req.respond("server error", .{ .status = .internal_server_error });
+ req.respond("server error", .{ .status = .internal_server_error }) catch {};
},
}
return;
From 056cd3261fef1670a9cac90bc770d70c75c25f87 Mon Sep 17 00:00:00 2001
From: Pierre Tachoire
Date: Tue, 30 Jun 2026 15:01:27 +0200
Subject: [PATCH 6/6] Update src/browser/webapi/css/CSSStyleDeclaration.zig
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: AdriĆ Arrufat <1671644+arrufat@users.noreply.github.com>
---
src/browser/webapi/css/CSSStyleDeclaration.zig | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig
index 5cf1c21b2..393243187 100644
--- a/src/browser/webapi/css/CSSStyleDeclaration.zig
+++ b/src/browser/webapi/css/CSSStyleDeclaration.zig
@@ -129,9 +129,9 @@ pub fn setProperty(self: *CSSStyleDeclaration, property_name: []const u8, value:
try self.syncStyleAttribute(frame);
}
-// Apply one declaration parsed from a `style=` block. Unlike the imperative
-// setProperty path, within a single declaration block a normal declaration must
-// not override an earlier !important one (CSS cascade precedence).
+/// Apply one declaration parsed from a `style=` block. Unlike the imperative
+/// setProperty path, within a single declaration block a normal declaration must
+/// not override an earlier !important one (CSS cascade precedence).
fn applyParsedDeclaration(self: *CSSStyleDeclaration, declaration: CssParser.Declaration, frame: *Frame) !void {
if (!declaration.important) {
const normalized = normalizePropertyName(declaration.name, &frame.buf);