The value pipeline

Introducing Ladybird: A Browser Built from Scratch

Bruce introduces Jelle Reijmagas from the Ladybird Browser Initiative, and Jelle provides an overview of the project. He explains that Ladybird is a genuinely new browser engine — not a fork of Gecko, Chromium, or WebKit — built entirely from scratch in C++ and Rust, funded solely by donations and sponsorships. Jelle emphasizes the community excitement around having a more diverse web ecosystem, drawing parallels to the early days of Firefox.

Web Standards as a Foundation and the SerenityOS Origin Story

Jelle explains how Ladybird's development is tightly coupled to web specifications, with source code annotated by spec references, and how implementing specs from scratch exposed numerous bugs and gaps that the team fed back to working groups. He traces Ladybird's origins to SerenityOS, a from-scratch operating system project by Andreas Kling, where a basic CSS engine was first built to style documentation, eventually growing into a recognizable browser.

Where Ladybird Stands Today: WPT Scores and JavaScript Engine

Jelle presents current performance data from the Web Platform Test suite, showing Ladybird trailing Firefox by about 70,000 tests while actually leading Safari by 30,000 tests. He also highlights Libjs, Ladybird's custom JavaScript engine, which currently outperforms stock V8 on the ECMAScript conformance suite. Jelle then demonstrates Ladybird live, loading his own slide deck and the CSS Day website to show the browser in real-world action.

Ladybird's Architecture: Processes, IPC, and the Rendering Pipeline

Jelle walks through Ladybird's multi-process architecture, where sandboxed processes communicate via IPC to isolate sensitive tasks like image decoding, networking, and script execution. He describes the core libweb library containing DOM parsing, CSS parsing, layout, painting, and JavaScript, then outlines the full rendering pipeline from resource fetching through HTML/CSS/JS parsing, styling, layout, display list generation, and GPU compositing.

CSS Value Processing: Declared and Cascaded Stages

Jelle introduces the six CSS value processing stages — declared, cascaded, specified, computed, used, and actual — using `width: 50%` as a running example. He explains how Ladybird uses a JSON-driven property definition file and Python code generators to handle parsing and validation, and demonstrates the CSS property validation API rejecting invalid values like `rebeccapurple` for `width`. The cascading stage is then covered, including specificity, source order, and the `!important` keyword.

Specified, Computed, and Used Values: Resolving Styles Through Layout

Jelle covers how Ladybird handles specified values — reverting to initial values when declarations are absent — and how animations complicate every stage. He explains computed values as an early resolution pass that avoids layout, showing how `2em` can be resolved to pixels immediately while percentages must wait. The used value stage is then introduced as the post-layout phase where percentage widths finally resolve to concrete pixel values, demonstrated with a live interactive slider example.

Actual Values, getComputedStyle Quirks, and Complex Cascading Examples

Jelle explains how actual values account for device pixel ratios and rounding, and critiques the historical inconsistency of `getComputedStyle`, which returns either computed or used values depending on the property. He then walks through a more complex example involving CSS layers, `revert-layer`, `calc()`, CSS variables, and percentages, tracing the value through all six stages to show how each dependency is resolved in turn.

Efficiency Challenges: Memory, Invalidation, and Custom Properties

Jelle addresses the performance and memory challenges of CSS processing at scale, describing how Ladybird reduced memory usage on a typical Twitter page from 1.5GB to a few hundred megabytes. He explains the trade-offs between sparse maps (used during cascading) and dense maps (used for computed styles), how style invalidation is scoped to avoid full-page recalculations, and the complexities of custom properties with copy-on-write inheritance chains. A particularly surprising bug involving `font-family: monospace` causing font-size time-travel is also described.

Container Queries, Animations, and the CSS Implementation Summary

Jelle covers how container queries feed layout information back into the top of the value processing pipeline, requiring recursive style re-evaluation. He summarizes the full complexity of CSS implementation — layout dependencies, font metrics, `currentColor` lookups, variable substitution, and animation interactions — and maps each pipeline stage to its corresponding Ladybird code component. He closes by reflecting on the depth of work required to go from authored CSS to actual rendered pixels.

Q&A: Motivation, Open Source Model, and Technical Choices

Bruce hosts an audience Q&A with Jelle covering a range of topics: why Ladybird was built (love of programming and desire for a more diverse web ecosystem), its open-source but currently contribution-restricted model due to AI-generated code security risks, use of FFmpeg for audio/video rather than from-scratch decoders, frame rate targets and the deliberate absence of a JIT JavaScript engine for security, DevTools support via the Firefox DevTools protocol, the planned alpha release timeline (Q3–Q4 of the current year for macOS and Linux), and why the nonprofit is US-based despite having no staff in California.

A lot of people say there's not enough browser diversity these days. And whereas that's true, I think, at the moment, There's only really three engines, Chromium, Blink, Gecko, and WebKit. But there are new contenders on the block out of Spain.

The Igalia is stewarding the Servo engine. And our next speaker is from a new engine, which is gonna be released, I think, sometime later this year, hopefully. Fingers crossed. So without further ado, I'm gonna butcher his name. I just know it.

I've been practicing. From Ladybird browser, Jelle Reijmarcus. Nice.

Amazing. That was not butchered at all. Thank you, Bruce. So good morning, everyone. I'm here on behalf of Lady Bird, the Lady Bird Browser Initiative. And indeed, as Bruce introduced us, we're a new browser engine. And I think for today, what would be nice to look at is the other side of spectrum, as it were, or the CSS wall. We have this side where you author all your CSS rules, your style sheets. You do things with the DOM tree and try to get things in order.

The other side of that is there needs to be this engine that actually takes in all the text that you typed and turn that into pixels. Somewhere between the text and the pixels, something needs to happen. And turns out, that's a lot. And I'd like to give you just a tiny peek into what the engine needs to do and what we get from the specs, what the CSS spec tells us about how to deal with these rules and how we did that in Lady Bird.

So first, the thing about me. I'm Jel Reijmagas. I'm the COO of the Lady Bird Browser Initiative. We're a nonprofit based in San Francisco. We have no people in California whatsoever. We live all over the world. But we do have this team of amazing people that we've gathered around in a couple of past years to build a new browser.

And that's one of my kids. I have three. And they're very happy that their daddy is now able to work on this full time instead of during the evenings and in the weekends. So some of you I know already are familiar with Lady Bird. But for those of you who aren't, I just want to give a primer. We are a new browser and a new web engine.

We're not a fork. We didn't fork Gecko or Servo or Chromium. We built something from scratch. So in the truest sense of that sentence, it's a new engine. And we try to build everything that we do that has to do with the web based on web standards.

And I know some of you are involved in some of the working groups. I thank you very much because your work has been invaluable. And I'll show you in a couple of slides what that means. And we are written mostly in C plus plus and Rust. We historically started off in C plus plus because that was a language that we were very familiar with.

We were comfortable writing a new web engine, all the things that we needed to have in the browser in C plus plus And recently, we started working in Rust as well in order to get a couple of very sensitive parts of the browser in a memory safe part of our binary. Then all the work we do, everything that we put out, is supported by donations and sponsorships.

Not only that, it's 100% supported. We don't have any other forms of revenue. We are never going to create new sources of revenue. This is everything that we're going to do. And that means that we also need to have a good discussion on what does that mean, someone donates or someone sponsors us.

And we decided way back at the beginning of our foundation that, no, we're not going to do anything for the money. Well, we're going to put up your logo in our sponsor section, and that's it. And it's truly amazing. So many individuals and companies reached out because they wanted to see a more diverse web ecosystem. They wanted to have something new.

They wanted to have that feeling that some of us had back in the nineties when Mozilla came out with Firebird and Firefox eventually. That was amazing. And a lot of people have the same feeling with Lady Bird right now that this is something new. It can work. How is this possible? It's supposed to be impossible to build a new browser.

Right? And somehow, we just did that. So that's interesting. One thing I want to touch on is web specs. As I said earlier, we're very dependent on that. And how we do that is that inside of Ladybird, have a lot of CSS codes, CSS parsing codes. We have DOM code. We have layout code. And for a lot of these pieces of logic, web specs were written. And what we tried to do is to actually refer to the web spec, the draft web spec that is out there, and we just copy over all the steps that people have written and then start implementing those.

And turns out if you do that, a lot of things break because web specs were mainly written by looking at what browsers do right now. And these are algorithms and you can at a certain level, you can try them out, but at a certain point, you stop being able to actually run them. You need the actual browser for that.

So when we started implementing these web specs, we noticed a lot of bugs, lot of missing pieces, moving parts that weren't there. And we started giving back to the working groups mostly in form of issues, pull requests, changes that we could make to the specs to improve them. We're not at the point where we are able to spend our time writing new specs, but I'm sure that as soon as we're done building the browser, we might be able to start with new specs as well.

But basically, our source code is like this. We have all the moving parts that have to do with web specs littered with comments that describe what we should do. So where Lady Bird came from? Well, of course, if you want a new browser and you have a lot of free time, what you do is you build a new operating system first.

You deal with the graphics graphics driver. Driver. You You need need a a kernel to interact with the file system. You need to do all those things. This was a project called SerenityOS started by Andreas Kling. And when he got to a point where he was interested in doing some more styling, I believe it was for the documentation inside of the operating system, he was like, I think there is this language that people use to style things.

Maybe we should have an implementation of that. So what you see here is one of the first attempts to get selectors working, properties, values working, match that to the right element in the DOM tree. It's very simple, very basic, but this is one of the very first screenshots where you can see this working. And consider that everything that you see here, this is written from scratch. Every pixel, every font, it's all made from scratch.

That was the premise of SerenityOS. And at a certain point, we were like, oh wow. More people started working on the project. We were expanding this browser, and suddenly we could run old Reddit, old.reddit.com, and it started to look somewhat legible, somewhat interpretable. It started to work.

And around this time, people started noticing that, wait, are we building a new browser? Is this actually something that you can do? Mind you, this is before AI came around and people were able to just prompt browsers into existence. This was amazing to us and to a lot of people as well. So at some point, we got asset three compliance, 100%, 100 points out of 100.

And it was not long after that that we considered, well, maybe we should take this browser outside the OS and make it standalone. As you can see, it was still called browser at this point. The operating system was pretty simple. Everything had a very clear name. We pulled it out. We called it Ladybird.

And we continued working on Ladybird. And where we are right now So these are two graphs that I pulled up yesterday. This is the web platform test suite. Who of you is familiar with WPT? Quite some hands go up. So for the people who are not familiar with that, the web platform test suite is basically this humongous test suite of both regression tests and unit tests of all kinds of web features.

So CSS, DOM, all the extensions that are out there, or rather the specs, the web specs. There are over 2,000,000 tests in there, and all the browsers run them, and you get a score. And what we do is we track that score. And at this point, we're about 70 ks tests behind on Firefox.

And for some weird reason, we're 30 ks tests in front of Safari right now, meaning that according to this huge test suite that people all over the world have built in order to test interoperability between browsers, we are doing pretty well. Servo and Flow are out there as well, and I'm very happy to have noticed the last two years that especially Servo has ramped up the development speed and working on getting the WPT score up as well. But these are all synthetic tests.

This is an example. This is test two sixty two. This is JavaScript or ECMA two sixty two test suite that exposes how much of the standards you implement correctly when running a JavaScript engine. I did mention that we wrote everything from scratch. So JavaScript as well, of course. We wrote a new JavaScript engine. It's called Lipsch. It's the red one over there.

Currently, we're in front of the stock V eight that comes out with Chromium. We're behind on the experimental Spider Monkey and V eight and behind on the regular Spider Monkey. We used to be the number one and then a number of new features came out. A couple of specs were stage forth and then at that point, we needed to catch up.

And that's cool because we love catching up to the big browsers and doing our thing and proving that we can do this. But again, this is very synthetic because what good is having Libya a JavaScript engine if it isn't able to run the real web? So about that. What you're currently looking at is, in fact, Lady Bird. There we go.

So this slide deck was built not specifically for Lady Bird. It's just a regular slide deck. It works in all browsers. And it does work in Lady Bird. But I can go to CSS Day, for example. Let's take a look at that. And there we go. Let's see if we can find ourselves. There we go. So this is Lady Bird.

It's an actual browser. It works. Are we bug free? Absolutely not. Are we feature complete? No, sir. We're not. And that's Okay because what we're currently doing is ramping up for alpha release this year, and we need to be at the spot that we can actually render most websites correctly.

The websites that you and I use on a daily basis, that needs to work. But do we have bugs? Yes, we do. And that's the fun thing. This is a new browser. We're going to need to solve all these issues, and that's Okay. That's what we like doing. So let's dive into the CSS part of this, because what we have with Lady Bird is a set of cooperating processes that interact through an IPC. Basically, we have the sandbox processes that just have a protocol to talk to each other.

And we do that because we want to separate some sensitive things like image decoding or the scripts that run-in the background or the network requests that we do into a separate part of memory that we can protect and doesn't have access to all the things that your browser UI has. But in the middle there is web content, and web content is this huge library where we make sure or it's a process running on libweb that contains all these things like the entire DOM parsing, CSS parsing, etcetera. Inside of libweb or web content, we have these moving parts to make your website work.

We have the DOM tree. We need to be able to parse that, to construct that, to be efficient in querying everything that is inside the DOM tree. You can imagine we have all these spec authors that do things like, oh, you know, if somewhere in the dump tree this element lives, great. Now we need to have a map of all elements, or do we not do that?

That's a huge part. And then we have the CSS parsing and style computer, which are respectively the parsing of the style sheets and the things that look at the DOM tree and the rule sets that you have and combine that into the actual values that you see on screen eventually. We have a layout engine to make sure that all your flex and grids and tables are working correctly.

We have painting, which is separate for us from compositing, where we basically turn the entire layout tree into something that we can paint on an arbitrary service. We have the JavaScript engine, of course, that needs to be able to interact with all the things that happen with CSS styling elements. And then we have all these parts dictated, supported by other browsers in web specs and stuff like IndexedDB, the editing spec.

Those are all things that we need to support. This is huge. And CSS is just a part of that. And that's huge as well. So the pipeline looks a bit like this going from top left to bottom right. We fetch all the resources that we need to build up the dump tree to understand which resources are out there.

With CSS, you have links that you couldn't put in HTML. Those can be added dynamically using JavaScript as well. They could be conditional. You could have at import rules. There are all these ways that styles can come into your page. And we need to parse everything. So we have these processes dedicated to parsing all the HTML, CSS, JavaScript. Then we need to go into styling.

We need to go into layout. Again, the paintable tree gets turned into a sort of a display list, And the display list is basically this long list of commands that tell us how to draw a page or a piece of that page. And then the compositor takes a display list and does things like efficient overlaying, using your GPU to render it efficiently.

If you are scrolling, we don't need to paint everything again. We just need to move the right layers and we need to take all the styling into account. So what does CSS say about how to deal with properties and their values? There are a couple of stages that are involved. So I'd I'd like to take the width 50% as an example just to see what happens with that.

There are six value stages or value processing stage that are defined. Declared, cascaded, specified, computed, used, and actual values. I'm very certain that almost all of you have heard of these terms before, at least one of them. I think at least cascading should ring a bell, computed probably as well, depending on whether you're in the working group for CSS spec.

You probably know this by heart. But we start off with having the actual text for the style, and we need to be able to understand what that is. Is that a valid declaration? This means we need to be able to parse it. We need to be able to understand what is written. So which property is this?

Is this a shorthand or a longhand property? What is to the right of the colon? Is this something that can potentially match something? If anything is invalid in that regard, we refuse it and we log something to the console and we're done. But if it's valid, it goes to the next stage. But for these declarations, what we do in Lady Bird is we actually have this huge JSON file with all the CSS properties defined.

And this is a relative simple one, but what you can already see is that we need to put in some behavioral things in there to automatically deal with the properties that come along with something like with. How this works is that we take this JSON file and we have a number of Python code generators that generate all the code for us that we need to support the width property, for example. And we can do fun things like copy in the infinity symbol and it will work.

It will understand what it does, taking into account all the limits that exist for CSS numbers, for example. You can specify identifiers, but there's also a couple of interesting things in there that we need to do things efficiently or correctly. So for example, the needs layout for GetComputerStyle below there, that of course is a hint to what do we need to do with the style in order to give a value back in GetComputerStyle.

Some styles we can just return without doing any layout. That means we can do it very efficiently. For width, we do need a layout. So there you go. And then you end up with something that looks like this. You have these properties and values. So for example, I can switch to width, which will accept the two EM right there.

But I can put in something like Rebecca Purple. And that should probably not work. It gets rejected. So the mechanism behind this is a JavaScript API that essentially runs a part of our CSS password and validation logic that looks up the property name.

It tries to pass the value that you put in. And if that works, Okay, great, we return true. And otherwise, we return false. When we have found all the valid declarations, we have another stages that cascading, basically taking all the rules into account in order and making sure that we understand which one wins.

And to do so, there's a number of things that come into play, like selective specificity, changing the amount of things that you type in a rule apparently determines if something a rule can get selected. But the order is also very important because if you put things with the same specificity later in the style sheet, it will get priority. And of course, you can force it with important, which I believe a lot of you use.

These are all things that have an effect on the cascading logic, the cascading stage. This is a piece of code that comes from Lady Bird, where we look at all the context and the matching rule sets that we found, and then we run over all the author rules. We do this in forwards and backwards ordering in order to get priority straight.

And of course, we also need to take into account the user and user agent style sheets because those exist as well, even though a lot of people tend to reset them for some reason. Then when we go to the specified values, this is a very simple stage, really, where we consider all the declared values in cascading order, and we take a look at whether or not they were set. And if they were not set, we need to revert back to their initial value.

Fun thing, and this will pop up, everything that we do has to take animations into account. So when you look through a style computer, for example, you'll see these computer properties with animations applied, no or yes, enum values being passed in, just to make sure that we do the right thing at the right stage, because animations mess up everything.

And of course, we can influence the specified value with certain CSS keywords, like inherit, initial, unset, revert, revert layer. This is a directive that you can use to basically halt the engine at that point to say, no, we're not going to deal with the initial value or the actual value that you're seeing right there. We're going to take something else, and you need to go up the cascading tree.

For us, this means in Lady Bird, we need to be aware of everything we do up until the point where we decide what that specified value is. Because if we forget about the cascading order, if we forget about whether something was valid or not, which style sheet it came from even, we might be unable to honor some of these words.

Then the next stage is computed. Computed values are basically the same CSS values, but we try to resolve anything that we can at that point. We don't have a layout yet. We haven't put the boxes in all the right places. But what we can do at that point is look at, Okay, so maybe we have this EM in there and a constant, two pixels.

We are able to resolve that without doing anything in terms of layout. So we don't need to spin up our layout engine just yet. We can just take a look at the rule. And we can simplify that because we know what this value is going to be. And that tends to look a little bit like this. We absolutize the value, make sure that there's as few variables in there as possible, and then try to compute the value based on the context, where it's coming from.

And this is a very large part of a lot of difficulties that we've had with implementing CSS from scratch is understanding which context does a value come from. Depending on whether you use If, for example, you use a percentage and you use this in the context of a length, a box size, that should result in pixels.

But if you use it in the context of a scalar value, that should result in the scalar value. And then things tend to get very interesting when you can have either lengths or percentages in the value, and it all becomes a mess. Please join me in browser development. It's a lot of fun. So for example, this is a continuation of the last slide of where you can modify something.

For example, this slider modifies the font size of this box and there's this rule, the border left with, that is scaled based on that font size. And every time I drag this little slider, what Lady Bird needs to do is to understand, oh, wait, we changed something and this was a dependency for this rule.

So we need to invalidate these styles and do the whole thing again. We need to layout again. We need to run the display list again. You can imagine that if you have a big website and you plop in a widget like this, you don't want to deal with complete style invalidation across your entire page. If your browser needs to recalculate all your styles, all the layout, all the display commands again, all the compositing layers, that will be awful. So honestly, a large part of what we've been doing with CSS and with rendering is making sure we can invalidate efficiently, and we understand what parts to draw again and which ones we don't necessarily need to do again.

Because the best optimization will be the work that you never do. And then we get into use values. So use values come after computed. Why? This is the part where we have run the layout engine. We now have enough information based on all the computed styles that we can do a preliminary pass of all the layouts that is on the page. And this gives us interesting information because when we have a style like with 50, well, we have to have this containing block in which the element resides and then we need to know what the dimensions of this containing block are.

And once we know that, once we have run the layout for that block, we can then infer the actual pixels that we could fill in for that width value. So the use value is basically transformed at that point into actual pixels. So for example, if I scale down this container, and I have this element in here with width 50, this is the point where the use value actually comes in.

And you can see here, the use value for a width of 50% would be two fifty three. For example, when I put the container at five zero six, just a random value, but I hope it drives home the point. So then there's the concept of an actual value.

And this is where we do all the stuff that matters to get things on your screen. Basically, if you have a high DPI screen, for example, you have 2x or 3x display, every CSS pixel that we've talked about so far will be multiplied by two or three. And that is a actual value.

That's the actual value that we'll be using to instruct the compositor, the display engine, to do the right things and turn the styles into the right pixels. We also need to do some rounding, not only because the spec dictates it or because someone likes it. It's also because some browsers just do random rounding sometimes.

Someone decided that that's the way it should be done, so we need to do it as well. And that changes the actual value. And substitutions come into play, other constraints. Everything that ends up all the way at the end, and that's basically, in some cases, the value that you would like to see, That's the actual value.

And then getComputerStyle comes in. GetComputerStyle is awesome. It's like a relic. Why? Because depending on which property you provide as an argument to getComputerStyle, you'll get either the computed value or the used value. This is just historical.

It sucks. But for example, if you have a width and you're like, Okay, let me do get computer style on the width, the browser will happily tell you, this is 200 pixels. Great. Done. And then you're like, Okay, so now I'd like to know the actual line height. And the browser tells you, oh, fine. Here you go. It's normal.

What happened? This is a relic. There are better APIs out there, luckily, but this is something that we need to support. So we have to think about which properties we need to pull from which buckets of the value processing pipeline. And here you can see that in action. So this is a piece of code that GetComputerStyle uses, where we determine whether or not we need to actually do a layout pass.

This is also something that's very interesting. We have JavaScript that can run-in parallel to rendering the page. And that means that maybe we haven't run layout yet at that point. So if someone requests the computer style for an element for a certain property, we might not have run layout yet and we would either crash or give back an invalid or intermediate value.

So we need to understand which ones actually need to layout and do the layout and make sure that the styles are up to date. And then it's very easy. We can just do style value for computer property and return that value. So how does this look like for width? Just to give a summary. At the top, we start off with the style sheets input.

We go to declared, which is, okay, this is a valid width value. Then with cascading, we determine which declaration ends up being the winner. Then with specified values, we decide is this an actual specified value or do we need to revert to something else, like the initial value. With computed, we try to resolve things as far as possible. So for 50% with width, we cannot yet resolve anything because we don't have a layout.

We go into the used part where we do have a layout, and now we can determine the actual pixels, the CSS pixels. For example, if you have a six forty px container, that results in a three twenty px width. And then with actual values, we do some adjustments. For example, if I were to display this on my screen right here, I would get six forty again because that three twenty gets multiplied by two. And there we go.

But you can also do things like this. We could have layers. And we could do things like revert layer. So what happens right now is we have this declaration of the style sheet with layers. We do a padding left with a calc with a percentage in there, and we use a variable because why not? Let's make the life of browser developers easier, and said nobody.

So we go into the declared stage. We tend to check which one wins are valid, I'm sorry. We have the cascading where we decide, Okay, we end up with the revert layer keywords. Sure. Then we go into specified where we actually do that rollback to the right value, and we end up with the calc.

Then into the computed parts, we can determine what that gap is because we've defined the gap variable as a EM value. That means we can look at the font size. We do the calculation. We cannot do the calculation just yet because we have a percentage in there. We go into the layout stage where we can calculate use values. We take the 50%, change that into actual pixels.

We can do the calculation. Amazing. And then we do the actual stage where we map things to the device, and we end up with the actual value that we can use for drawing. Mind you, this is all things that we have to do just for CSS. So again, just a quick summary of things that could happen. We have layout dependent things.

We can have fun metrics come in play. We have things like current color that can actually look at other properties. Oh dear. So now we need to take that into account as well. We can't just pass things from top to bottom. We actually need to have proper stages. We can't cut corners here. And of course, with variables, we need cascading substitution.

Sometimes we can't do that, and we need to wait for the right moment to be able to do the substitutions. And then with Custom Properties, yes, as I said, we really need to wait to the point where we know the actual value because the variable can be anything. Again, we need to substitute that and recursively do these iterations of all values until we understand what the actual value is going to be. And then we also need to do this efficiently because it's not enough to be able to do all that.

You also need to not crash the machine that the user is running the browser on. So just a couple of weeks ago, a random X page, for example, would take us 1.5 gigs. Nowadays, we brought that back to a couple of 100 megabytes. Most of that had to do with styling and style and validation and how we store things.

And we do a couple of things in Lady Bird that try to do things more efficiently in terms of memory. So we need to have the authored styles, of course. We pass all things like the user agent and user styles, then we get the authored styles, the things that websites actually send to us. And we need to have those in their original version because at any point, we need to be able to look at them again. They can be changed, and we want to do that efficiently, so we need to have those.

Then during a couple of stages like cascading, we try to do things as sparse as possible. So the thing with dense and sparse maps is when you do things sparsely, you tend to save a lot of memory. But you tend to exchange that for performance because you need to know what is in there and how to find it efficiently.

And maybe you need to do hashing. And then with dense maps, you tend to use more memory, but it's usually very quick because you can index directly to the right property. So after a lot of performance benchmarks that we've run, we found that during cascading, we need the sparse map. Computer styles, we go into a dense map where we increase memory usage, but we need this because we need to go into layout.

Layout needs to be able to very quickly get those values. And when we get to the layout stage, we don't actually store those used values as such. They tend to end up in the layout tree that we have. So as much as possible, we store things associated with layout tree, So we no longer are working with all the individual properties, but we try to map things to our world as browser web developers.

And that works incredibly well. And eventually, with custom properties, properties are a pain in the ass because they can be inherited, they can overlap. So what we do there is to have a bit of a copy on write system where we tend to add new properties as we see them and we keep this chain of different lists of custom properties that are defined in the auto style sheet and try to traverse that. This is probably something that we still need to optimize a lot.

So for example, there's a piece of code that goes into one of the optimization that we have for a dense map of long end properties, where we just define this array with all the long end properties that we know of. All the short end properties get mapped into these long end properties, and then we do things like having bitmaps, like just toggling single bits to understand which properties we've set or not.

And font size. Font size is amazing. Did you know font size can time travel? This is one very fun bug that at some point we had at Lady Bird that we discovered that basically what happens is you can define font size going all the way to the roots and then nest that with some elements.

If somewhere in your page you decide to use font family monospace, we then need to go back into all those processing stages and change the sizes. Because once you use font family monospace very deeply in the dump tree or in a layer somewhere, it doesn't matter, we now need to go back and change the usual inheritance to different values because monospace apparently requires a default of 13 pixels instead of 16 pixels. It's amazing.

So now we need to have all these pieces of code that deal with that as well. It's so much fun. A short mapping of what we do in Ladybirth for all these stages. So for declared values, have the the parser declaration, or basically the parser that creates parser declarations and style properties.

We have cascaded properties. We have a style computer for the specified properties. So this is where a lot of work happens to the style values in order to resolve them, to absolutize them. And when we move to computer properties, we have this immense part of logic where we need to resolve try to resolve at least functions and their intended values, the types of values being used.

Then once we have the initial layout passes done, we can go to Use Values, and this is where our layout engine comes in and interacts with the styles the most. And then finally, we push all of that data to our painting. So there's an example of how that looks like for the style computer. So even though we're not dealing with device pixels just yet, even our specified values need to be aware of device pixels per CSS pixel, as you can see right here.

And not only that, we also need to understand, is this being animated or not? Because that all messes with your cascading, with the inheritance of other values. If you try to think about what happens with animations and how different properties inherit those values and deal with that, and what you can and cannot use in calculations, that becomes a very big mess as a CSS author.

It becomes an even bigger mess if you try to support that. And then you have things like container queries. Amazing. So now the CSS people have thought of something that we can do to query parts of the layout and let that result in new styles that we need to push back into the top of the entire value processing pipeline.

Amazing. I love it. So yeah, if you have a style like this, you can just say, well, if this thing, this container has the right container type and it has an in life size larger than 40 RAM, let's just do this title class and change the font size. Oh, I love This could only get better if instead of font size it would say font family.

So yeah, that's also something that we need to do and we need to recursively look at these container queries and match them, make sure that they're valid, that they're satisfying all requirements. And if they do, we need to evaluate all the styles that are in there and then push them back in. Amazing. So as a concluding takeaway, I hope I've been able to show you that a lot of work not only goes into writing a CSS in the front end, but also it takes a lot of work to understand how to interpret it, how to push it through the entire pipeline, how to deal with functions, with recursive things that happen in those pipelines. And that is not as easy as just defining PX in your style sheet, and you get PX out.

There are so many things that can happen. And I'm really glad that my experimental browser didn't crash today. So that's a good thing. And thank you very much.

Jella, you win the competition for the most questions asked by the audience.

Amazing.

Let's Come sit do on the chair of interrogation and feel free to munch on a flower if Lovely. You're So there are literally loads of questions. I'm going to try and synthesize them the best I can. Why why? What problem were you trying to solve by making a brand new browser engine?

So originally we weren't solving a problem. We were doing what we like to do, and that is program. We were just coding along. We were enjoying ourselves. And we wanted to see how far we could get in showing the real web in a brand new web browser. And when funding came in for Lady Bird, when we decided to create a nonprofit, we noticed that not only was it a lot of fun for us, it was also a lot of fun for a lot of other people that were either very excited about having a more diverse web ecosystem, as I said, this is like This is the new Firefox, the new Chrome.

This has potential. There were also a lot of people concerned with privacy, with the fact that, well, I think Google is the best example of a very fast browser, a very huge browser that supports a lot of things. But it has become a bit of a monopoly. And people try to change that.

They want to try out new browsers, and they are excited by anything that isn't based in something that Google does, even though Google has, I think, objectively been one of the most driving forces behind all the new web specs. So that's, I think, the two things that I see the most.

It's we do our thing because we like doing what we do. And because a lot of other people are excited by the more diverse web ecosystem. And people really want something else than what's already out there.

Other monopolies are available, by the way, folks. It's not only Google.

Absolutely. Are they sponsoring this?

No. Is it all open source?

Yes. Everything that we do is open source. We recently changed our contribution model just last week. This had to do with the decreasing cost of AI, and the fact that we were getting security advisories for very subtle parts of the browser, which is amazing.

I love it when people do that. But we considered the fact that we were spending most of our time reviewing tens of thousands of lines of codes from people that we don't know that could easily slip in anything. So we're basically more of a source available browser at the moment. And I won't say that we're never going back to the open contribution model. But at this point, yeah, we're open source, source available.

So somebody couldn't take it and fork it?

Absolutely, they can. People have done so before. And I would love that.

But you're not necessarily accepting contributions from outside?

Not in code at the moment. No.

No. That's great. Jake Archibald of this parish asks, are your audio, video, and image decoders all written from scratch as well? Or do you use the existing libraries that are out there?

Yeah. So we had to draw a line somewhere. So originally, we wrote everything from scratch. We had a VP8, VP9 decoders written from scratch. Amazing. That tends to be not a lot of fun when you actually want to build a working web browser. So what we decided on with Lady Bird is that everything that is grounded in a web spec, we try to write from scratch.

And I think for the larger parts, minus maybe the PNG spec, we've done so. So for things like audio and video, we use FFmpeg, LibAV codec, LibAV format, things like that. Trio?

Life's too short to be writing VPA decoders.

We might be able to do that at one point. But at this time, I don't think it's that efficient to start writing something like FFmpeg again.

Got it. Got it. You mentioned efficiency. You've got to be able to do all of that computation, but you've got to do it fast enough that people can actually use the browser. Do you have any frame per second targets? What's your optimization strategy like?

I guess the best target would be the native refresh rate of the device, of course. So if you have a 120 FPS display, then we should be able to do that. And the way that we try to do that is to separate out all the things that we can do in parallel. So this is what all the major browsers do, by the way.

But for example, if you're scrolling, you should not have hiccups in your scrolling because a style needs updating, or a box is being moved. The scrolling is just in parallel. We move that smoothly across your screen. And then everything that happens in terms of updates, we try to do as fast as possible. And one of the major bottlenecks for us has been JavaScript and Wasm. We've been optimizing that, of course.

But one thing we'd rather not do is have a JIT JavaScript engine. We don't do that yet. We did have it before, and we threw it out. Because once you start to compile JavaScript to native instructions, you tend to open up the system for all kinds of vulnerabilities. So for now, we've gone for basically the Apple lockdown mode, where we would just have the non JIT JavaScript version, and try to optimize that as much as possible that you, as an end user, will not notice any performance penalty.

On the subject of performance and things that people in this room, like developers, are interested in, are there, or will there be, DevTools for Lady Bird?

Currently, we implemented the Firefox DevTools protocol. So you can just take Firefox and go to About Debugging, and just connect to Lady Bird. And it will just show you the entire DOM tree, the elements, styles, all that stuff. We haven't implemented everything from the protocol yet, but we're getting there very quickly. We used to have our own DevTools, because of course, we implemented everything from scratch originally.

And we threw that out as well for now, because we weren't feeling that building the entirety of a DevTools that is useful to people got into the way of actually building the browser. So that's the current goal. I'm very excited for the new DevTools protocol specs that might help us to, in the future, maybe connect to any DevTools out there.

Wicked. I'm aware, by the way, that I'm not acknowledging who answered the questions. That's not because I'm being rude. It's just I'm trying to amalgamate them to get through as many as possible. When will we get a Ladybird alpha release? And related, is Ladybird the engine, or is Ladybird going to be a browser that contains a Ladybird engine?

When can we get our hands on it, really?

Right. So what we're building is Ladybird the browser, which is the Chrome, the thing you see on your screen, together with the web view, which is libweb view with web content, and all the stuff that we do to render things on your screen. And that entirety is something that we call the Ladybird browser. That's Ladybird. And if you want to give the internal engine the name, it's currently called Libweb.

It's a name. But this year is the year that we're coming out with the alpha version. I suppose that's going to be Q3, maybe early Q4, where we will put out binaries for macOS and Linux initially. And we'll move to a beta and production ready releases in the next two years.

Woody asks, like Firebird to Firefox, will you eventually rename to Ladyfox?

Maybe, I don't know. Yeah, it's a good suggestion. But no, I think for the time being, it's Ladybird.

Gotcha. On the subject of names, and libweb is a good name but quite boring, Bruce is a really good name, I think, for a rendering engine. The Bruce The Brewster. The Brewster, yes.

There we go.

Make it happen. What do you do what was the most interesting CSS bug that you found, as in it was inadequately spec'd or was hardest to actually implement?

Oh, that's a hard question.

Because it's all of them, right?

Yeah. It's all of them and none of them at the same time. I have to think hard about this because we did so many things. We found so many bugs. I couldn't, off the top of my mind, say which one was the hardest. Basically, we do is we tend to implement spec to a point where you say, Okay, so now we follow these rules.

Amazing. And then you discover that they don't work, and we need to talk to spec authors, and we need to look at browsers, what they do. And then you discover that, oh, so it's not just us. Also, Chrome and Firefox disagree on what this property should do. And that has happened for a lot of properties. And these change over time as well, of course, because browsers don't stand still. They keep developing.

I think for me, the most interesting bugs have been in CSS transformations. So to truly understand how to do proper perspective transformations, how all these values that go into it resolve to the right type, to understand how we need to modify our metadata to automatically generate code that deals with that as well. That's been a mess.

How much do you wish that the CSS working group would just like take a bit of a break and just stop influencing new stuff? I noticed you looking at Josh on the boat yesterday, I wondered if you could push him over overboard or something, just to slow down the pace of change.

No, I really like it because we need new specs. And I think we are getting into a spot where we can push back more. Where we can be one of the first that implements a new spec, even if it's just in a very draft status. And at that point, we might be able to point out all the difficulties that come along with it.

Historically, this has always been done by the larger browser vendors. And I hope that at one point we get a seat at the table.

So you're okay, Josh. You're not going to be mown down by a guy on a bicycle in the streets anytime soon. Last question. Why is the company US based? Wouldn't all the changes in the world make it good to have another European browser engine?

So I agree. We basically could be found anywhere. But the donation and sponsorship model works amazingly well over there. And originally, we started with a board founded by my colleague Andreas Kling and Chris Wanstrath. And Chris is one of the founders of GitHub.

I've heard of that.

Yeah. And he brought a lot of experience with him in terms of setting up the organization, nonprofit, bringing us into contact with both donors, with the companies that help us with legal and risk, with accountancy, all those things. So we've set that up. But as I said, we don't have anyone in California.

We could just as well have a company, a nonprofit over in Switzerland. Doesn't matter. So it doesn't matter where we're seated, because every country, every state has its own legal framework. And what we try to do is we want to do what we like doing. And as soon as we're hindered by changes in that legal framework, or by public perception of that legal framework, we could just pick up our things and go sit in another country.

I tend to not look as much at country where companies are founded in. I tend to look at what are the people doing and what's their intention.

Gotcha. If I didn't ask your question, I'm terribly sorry. There were absolutely loads. But I'm sure that you could pounce on Jela in in the break or something and and ask him. Give it up, folks, Jela Reimarkus. Amazing.

CSS Day

.css-day {
  date: 'June 11 & 12 2026';
  location: 'Amsterdam'
}

The value pipeline

Jelle Raaijmakers

#identifier-sponsors {
  Google AG Grid
}
selector-sponsors {
  9ELEMENTS Polypane
}

A stylized logo forming the text 'CSS DAY'.

Headshot of Jelle Raaijmakers, a smiling man with a beard and dark hair, wearing a white shirt and a grey vest.

LESS IS MORE

The words 'LESS IS MORE' are displayed using a minimalist, blocky typeface, with each letter formed by a combination of black rectangular segments on a light background.

.now {

11:50 | Jelle Raaijmakers

.now {

11:50 | Jelle Raaijmakers

Logo of CSS Day in the top right corner.

.now {

11:50 | Jelle Raaijmakers

CSS Day 2026 - Zuiderkerk, Amsterdam

Ladybird

CSS Day 2026 - Zuiderkerk, Amsterdam

Abstract logo featuring intertwined purple and white oval shapes.

CSS, From Text to Pixels in Ladybird

A tour through the machinery that turns CSS values into something a browser can use.

Jelle Raaijmakers

COO of the Ladybird Browser Initiative, a 501(c)(3) non-profit based in San Francisco.

Building a new browser with a team of amazing people.

A photo of Jelle Raaijmakers, a man with a beard, smiling and carrying a young child on his shoulders. The child is also smiling and wearing a patterned jacket and pink boots.

Jelle Raaijmakers

COO of the Ladybird Browser Initiative, a 501(c)(3) non-profit based in San Francisco.

Building a new browser with a team of amazing people.

An image of Jelle Raaijmakers smiling, with a young child on his shoulders.

Ladybird: new engine, not a fork

  • Built from scratch.
  • Based on web standards.
  • Written mostly in C++ and Rust.
  • Backed by a non-profit, 100% supported by donations and sponsorships.

Logo for Ladybird, consisting of two intertwined rings forming an abstract shape, in purple against a dark background.

Ladybird: new engine, not a fork

  • Built from scratch.
  • Based on web standards.
  • Written mostly in C++ and Rust.
  • Backed by a non-profit, 100% supported by donations and sponsorships.
A logo consisting of two intertwined oval shapes, one purple and one light blue/white.

Ladybird: new engine, not a fork

  • Built from scratch.
  • Based on web standards.
  • Written mostly in C++ and Rust.
  • Backed by a non-profit, 100% supported by donations and sponsorships.
An abstract logo featuring two intertwined or orbiting lines forming a symmetrical, somewhat hourglass-like shape.

Ladybird: new engine, not a fork

  • Built from scratch.
  • Based on web standards.
  • Written mostly in C++ and Rust.
  • Backed by a non-profit, 100% supported by donations and sponsorships.
Logo for Ladybird, an abstract design of two interwoven shapes.

Ladybird: new engine, not a fork

  • Built from scratch.
  • Based on web standards.
  • Written mostly in C++ and Rust.
  • Backed by a non-profit, 100% supported by donations and sponsorships.
An abstract, geometric logo is displayed on the right side of the slide.

Ladybird: new engine, not a fork

  • Built from scratch.
  • Based on web standards.
  • Written mostly in C++ and Rust.
  • Backed by a non-profit, 100% supported by donations and sponsorships.
Logo for Ladybird, consisting of two overlapping, stylized purple ellipses.

Ladybird: new engine, not a fork

  • Built from scratch.
  • Based on web standards.
  • Written mostly in C++ and Rust.
  • Backed by a non-profit, 100% supported by donations and sponsorships.
Abstract logo consisting of two intertwined, stylized oval loops in shades of purple and blue.

Ladybird: new engine, not a fork

  • Built from scratch.
  • Based on web standards.
  • Written mostly in C++ and Rust.
  • Backed by a non-profit, 100% supported by donations and sponsorships.
Logo for Ladybird browser, a stylized abstract purple symbol.

Ladybird: new engine, not a fork

  • Built from scratch.
  • Based on web standards.
  • Written mostly in C++ and Rust.
  • Backed by a non-profit, 100% supported by donations and sponsorships.
An abstract logo for Ladybird, resembling intertwined rings or orbits, in a purple hue.

Ladybird: new engine, not a fork

  • Built from scratch.
  • Based on web standards.
  • Written mostly in C++ and Rust.
  • Backed by a non-profit, 100% supported by donations and sponsorships.
An abstract purple and white intertwined circular logo, representing Ladybird, is shown on the right side of the slide.

Specs become code

A lot of browser work looks like this: keep the spec close, then make the algorithm executable.

// CSS Syntax Level 3: consume a number
// <a href="https://drafts.csswg.org/css-syntax-3/#consume-number">https://drafts.csswg.org/css-syntax-3/#consume-number</a>

// 1. Initially set repr to "integer". Let repr be the
// Stringbuilder repr.
Number::Type type = Number::Type::Integer;
// 2. If the next input code point is + or -, consume it
bool has_explicit_sign = false;
auto next_input = peek_code_point();
if (is_plus_sign(next_input) || is_hyphen_minus(next_input)) {
	has_explicit_sign = true;
	repr.append_code_point(next_code_point());
}
// 6. Convert repr to a number
auto value = convert_a_string_to_a_number(repr.string_vi

Specs become code

A lot of browser work looks like this: keep the spec close, then make the algorithm executable.

// CSS Syntax Level 3: consume a number
// <a href="https://drafts.csswg.org/css-syntax-3/#consume-number">https://drafts.csswg.org/css-syntax-3/#consume-number</a>

// 1. Initially set type to "integer". Let repr be the empty
StringBuilder repr;
Number::Type type = Number::Type::Integer;

// 2. If the next input code point is + or -, consume it
bool has_explicit_sign = false;
auto next_input = peek_code_point();
if (is_plus_sign(next_input) || is_hyphen_minus(next_input)) {
    has_explicit_sign = true;
    repr.append_code_point(next_code_point());
}

// 6. Convert repr to a number.
auto value = convert_a_string_to_a_number(repr.string_view);

CSS Syntax Level 3: consume a number

https://drafts.csswg.org/css-syntax-3/#consume-number

// 1. Initially set type to "integer". Let repr be the empty string.
StringBuilder repr;
Number::Type type = Number::Type::Integer;

// 2. If the next input code point is + or -, consume it and append it.
bool has_explicit_sign = false;
auto next_input = peek_code_point();
if (is_plus_sign(next_input) || is_hyphen_minus(next_input)) {
  has_explicit_sign = true;
  repr.append_code_point(next_code_point());
}

// 6. Convert repr to a number.
auto value = convert_a_string_to_a_number(repr.string_view());
// CSS Syntax Level 3: consume a number
// https://drafts.csswg.org/css-syntax-3/#consume-number

// 1. Initially set type to "integer". Let repr be the empty string.
StringBuilder repr;
Number::Type type = Number::Type::Integer;

// 2. If the next input code point is + or -, consume it and append it.
bool has_explicit_sign = false;
auto next_input = peek_code_point();
if (is_plus_sign(next_input) || is_hyphen_minus(next_input)) {
  has_explicit_sign = true;
  repr.append_code_point(next_code_point());
}

// 6. Convert repr to a number.
auto value = convert_a_string_to_a_number(repr.string_view());

Specs become code

A lot of browser work looks like this: keep the spec close, then make the algorithm executable.

// CSS Syntax Level 3: consume a number
// https://drafts.csswg.org/css-syntax-3/#consume-number

// 1. Initially set type to "integer". Let repr be the empty
StringBuilder repr;
Number::Type type = Number::Type::Integer;

// 2. If the next input code point is + or -, consume it
bool has_explicit_sign = false;
auto next_input = peek_code_point();
if (is_plus_sign(next_input) || is_hyphen_minus(next_input)) {
	has_explicit_sign = true;
	repr.append_code_point(next_code_point());
}

// 6. Convert repr to a number.
auto value = convert_a_string_to_a_number(repr.string_vie...
Screenshot of a code editor showing C++ code for consuming a number based on CSS Syntax Level 3 specifications, with a mouse cursor visible.
<html>
<head>
    <title>CSS test</title>
    <style type="text/css">
        #foo {
            background-color: #ff0000;
            color: #00ffff;
        }

        #bar {
            background-color: #00ff00;
            color: #ffff00;
        }

        #baz {
            background-color: #0000ff;
            color: #ffff00;
        }
    </style>
</head>
<body>
    <div id="foo">This is foo</div>
    <div id="bar">This is bar</div>
    <div id="baz">This is baz</div>
</body>
</html>
Screenshot of the SerenityOS desktop environment. On the left, a web browser displays the rendered output of HTML and CSS: three horizontal bars labeled 'This is foo' (red background, cyan text), 'This is bar' (green background, yellow text), and 'This is baz' (blue background, yellow text). On the right, a text editor window displays the corresponding HTML and inline CSS code that generates these elements.

Browser experiments inside SerenityOS

Screenshot of the SerenityOS desktop environment showing two application windows: a browser rendering an HTML page with CSS styling and a text editor displaying the corresponding HTML and CSS code.

Browser experiments Inside SerenityOS

<!DOCTYPE html>
<html>
<head>
	<title>CSS test</title>
	<style type="text/css">
		#foo {
			background-color: #ff0000;
			color: #ffffff;
		}

		#bar {
			background-color: #00ff00;
			color: #ffffff;
		}

		#baz {
			background-color: #0000ff;
			color: #ffffff;
		}
	</style>
</head>
<body>
	<div id="foo">This is foo</div>
	<div id="bar">This is bar</div>
	<div id="baz">This is baz</div>
</body>
</html>
Screenshot of the SerenityOS desktop environment showing two open windows: a web browser titled 'CSS test - HTML' displaying three colored bars (red, green, blue) with text 'This is foo', 'This is bar', 'This is baz', and a text editor titled 'Text Editor: /home/anon/css.html' showing the HTML and CSS code that generates the content in the browser.

CSS test - HTML

<html>
<head><title>CSS test</title>
<style type="text/css">
#foo {
	background-color: #ff0000;
	color: #ffffff;
}
#bar {
	background-color: #00ff00;
	color: #00ff00;
}
#baz {
	background-color: #0000ff;
	color: #ffff00;
}
</style>
</head>
<body>
<div id="foo">This is foo</div>
<div id="bar">This is bar</div>
<div id="baz">This is baz</div>
</body>
</html>
A screenshot of a desktop environment showing two application windows. On the left, a browser window titled "CSS test - HTML" displays a webpage with three horizontal colored blocks: a red block with white text "This is foo", a green block with green text "This is bar", and a blue block with yellow text "This is baz". On the right, a text editor window titled "Text Editor: /home/anon/css.html" displays the HTML and CSS code that renders the content in the browser window. The code defines CSS rules for `#foo`, `#bar`, and `#baz` setting their background and text colors, and three corresponding `div` elements in the HTML body.

Humble beginnings

Browser experiments inside SerenityOS

The real web starts showing up

Acid3 as an early milestone

A slide illustrating the "humble beginnings" of browser development in SerenityOS through three screenshots. The first screenshot shows a SerenityOS window with a code editor displaying HTML and CSS, next to a small browser window rendering simple colored bars (red, green, blue) based on that code, demonstrating early browser experiments. The second screenshot shows a SerenityOS browser window successfully rendering the old Reddit website (old.reddit.com), indicating initial real web functionality. The third screenshot displays a SerenityOS browser window running the Acid3 test, showing a "100/100" score and a progress bar of colored blocks, signifying an early milestone in web standards compliance.

SerenityOS - Browser

A screenshot of the SerenityOS web browser displaying the old Reddit website, specifically the r/SerenityOS subreddit.

Screenshot of old.reddit.com in the SerenityOS Browser

A screenshot of the SerenityOS web browser displaying the old Reddit website.

The Acid3 Test

Screenshot of a web browser displaying the Acid3 test page, showing "Acid3" with a score of "100/100" and a series of colored blocks. The browser address bar shows the URL http://wpt.live/acid/acid3/test.html.

Humble beginnings

  • Browser experiments inside SerenityOS
  • The real web starts showing up
  • Acid3 as an early milestone

Three panels are displayed, each showing a screenshot of a computer interface from the SerenityOS environment.

Left panel: A screenshot depicting two application windows within SerenityOS. One window, titled 'CSS test', displays simple colored bars (red, green, blue). The adjacent 'Text Editor' window shows HTML and CSS code, which defines the appearance of these bars.

Middle panel: A screenshot of a browser window running inside SerenityOS, actively displaying the Reddit website. The browser shows the site's typical layout, including a header, navigation, and a feed of posts.

Right panel: A screenshot of a browser window running inside SerenityOS, displaying the Acid3 web standards compliance test. The test shows a score of '100/100' and a series of five fully filled, colored progress bars (red, yellow, green, blue, purple), indicating full compliance with the test.

Web Platform Tests, relative suite scores

Scores measured June 10, 2026.

Subtest passes relative to Ladybird

Engine/Browser Relative Passes
chrome 123962 tests
edge 120762 tests
firefox 70418 tests
safari -29486 tests
servo -104476 tests
flow -153565 tests

Test262, percent passing

Engine Percent Passing
LibJS 97.6%
V8 97.5%
Boa 95.4%
Kiesel 93.4%
JavaScriptCore 92.0%
JavaScriptCore 85.2%
QuickJS NG 83.1%
QuickJS 82.5%
Nova 77.4%
Porffor 56.1%
A horizontal bar chart titled "Subtest passes relative to Ladybird" compares the performance of several web engines and browsers. Chrome has 123,962 more passing tests than Ladybird, Edge has 120,762 more, Firefox has 70,418 more, Safari has 29,486 fewer, Servo has 104,476 fewer, and Flow has 153,565 fewer. A second bar chart lists "Test262, percent passing" for various JavaScript engines: LibJS at 97.6%, V8 at 97.5%, Boa at 95.4%, Kiesel at 93.4%, JavaScriptCore (with a wrench and gear icon) at 92.0%, JavaScriptCore at 85.2%, QuickJS NG at 83.1%, QuickJS at 82.5%, Nova at 77.4%, and Porffor at 56.1%.

Web Platform Tests, relative suite scores

  • firefox: 70418 tests
  • safari: -29486 tests
  • servo: -104476 tests
  • flow: -153565 tests
A horizontal bar chart displays relative suite scores for different web engines: Firefox, Safari, Servo, and Flow. Firefox shows a positive score, while Safari, Servo, and Flow show negative scores, indicating a deficit in tests.

Web Platform Tests, relative suite scores

A horizontal bar chart displaying relative suite scores for Web Platform Tests. The x-axis represents test counts, ranging from -150,000 to 150,000 in increments of 50,000. A green bar labeled "flow" indicates a score of -153,565 tests.

Subtest passes relative to Ladybird

  • chrome: 123962 tests
  • edge: 120762 tests
  • firefox: 70418 tests
  • safari: -29486 tests
  • servo: -104478 tests
  • flow: -153565 tests

Web Platform Tests, relative suite scores

A horizontal bar chart comparing the number of subtest passes for different web browsers/engines (Chrome, Edge, Firefox, Safari, Servo, Flow) relative to Ladybird. The x-axis indicates the number of tests, ranging from -150,000 to 150,000. Bars for Chrome, Edge, and Firefox extend to the right (positive passes), while bars for Safari, Servo, and Flow extend to the left (negative passes).

Subtest passes relative to Ladybird

  • chrome: 123962 tests
  • edge: 120762 tests
  • firefox: 70418 tests
  • safari: -29486 tests
  • servo: -104478 tests
  • flow: -153565 tests
Web Platform Tests, relative suite scores
A horizontal bar chart comparing the number of web platform subtest passes for different browser engines (Chrome, Edge, Firefox, Safari, Servo, Flow) relative to Ladybird. The x-axis ranges from -150,000 to 150,000 tests. Positive values indicate more passes than Ladybird, while negative values indicate fewer passes.

Subtest passes relative to Ladybird

  • Chrome: 123962 tests
  • Edge: 120762 tests
  • Firefox: 70418 tests
  • Safari: -29486 tests
  • Servo: -104478 tests
  • Flow: -153565 tests

Web Platform Tests, relative suite scores

A horizontal bar chart comparing the number of passing subtests for various web engines (Chrome, Edge, Firefox, Safari, Servo, Flow) relative to Ladybird. Positive values indicate more passes than Ladybird, negative values indicate fewer passes. The X-axis represents test counts from -150,000 to 150,000.

Subtest passes relative to Ladybird

  • chrome: 123962 tests
  • edge: 120762 tests
  • firefox: 70418 tests
  • safari: -29486 tests
  • servo: -104476 tests
  • flow: -153565 tests

Web Platform Tests, relative suite scores

A horizontal bar chart depicting subtest passes for various web browsers relative to Ladybird. The x-axis indicates the number of tests, ranging from -150,000 to 150,000, with 0 as the baseline. Bars extending to the right represent more passes than Ladybird, while bars extending to the left indicate fewer passes.

Subtest passes relative to Ladybird

  • chrome: 123962 tests
  • edge: 120762 tests
  • firefox: 70418 tests
  • safari: -29486 tests
  • servo: -104478 tests
  • flow: -153565 tests

Web Platform Tests, relative suite scores

A horizontal bar chart showing subtest passes relative to Ladybird for various browsers. Chrome, Edge, and Firefox show positive test passes, while Safari, Servo, and Flow show negative test passes.

Subtest passes relative to Ladybird

  • chrome: 123962 tests
  • edge: 120762 tests
  • firefox: 70418 tests
  • safari: -29486 tests
  • servo: -104478 tests
  • flow: -153565 tests

Web Platform Tests, relative suite scores

A horizontal bar chart comparing subtest passes for different browsers (Chrome, Edge, Firefox, Safari, Servo, Flow) relative to Ladybird. The x-axis ranges from -150,000 tests to 150,000 tests. Chrome, Edge, and Firefox show positive scores, while Safari, Servo, and Flow show negative scores.

Synthetic tests & the real web

Web Platform tests, relative suite scores

Scores measured June 18, 2026.

Test262, percent passing

  • SpiderMonkey 99.6%
  • V8 / 98.3%
  • SpiderMonkey 98.2%
  • LibJS 97.6%
  • V8 97.5%
  • Boa 95.4%
  • Kiesel 93.4%
  • JavaScriptCore / 92.0%
  • JavaScriptCore 85.2%
  • QuickJS 83.1%
  • QuickJS 82.5%
  • Nova 77.4%
  • Portfor 56.1%
A slide featuring two horizontal bar charts. The left chart displays "Web Platform tests, relative suite scores" with unreadable labels. The right chart lists various JavaScript engines and their "Test262, percent passing" scores.
  • SpiderMonkey ✓ 98.6%
  • V8 ✓ 98.3%
  • SpiderMonkey 98.2%
  • LibJS 97.6%
  • V8 97.5%
  • Boa 95.4%
  • Kiesel 93.4%
  • JavaScriptCore ✓ 92.0%
  • JavaScriptCore 85.2%
  • QuickJS NG 83.1%
  • QuickJS 82.5%
  • Nova 77.4%
  • Porffor 56.1%

Test262, percent passing

A horizontal bar chart comparing the Test262 compliance scores of various JavaScript engines. The chart displays the percentage passing rates for different engines and versions, including SpiderMonkey, V8, LibJS, Boa, Kiesel, JavaScriptCore, QuickJS NG, QuickJS, Nova, and Porffor.

Test262, percent passing

  • SpiderMonkey ⚡⚙️ 98.6%
  • V8 ⚡⚙️ 98.3%
  • SpiderMonkey 98.2%
  • LibJS 97.6%
  • V8 97.5%
  • Boa 95.4%
  • Kiesel 93.4%
  • JavaScriptCore ⚡⚙️ 92.0%
  • JavaScriptCore 85.2%
  • QuickJS NG 83.1%
  • QuickJS 82.5%
  • Nova 77.4%
  • Porffor 56.1%
Bar chart showing the Test262 passing percentage for various JavaScript engines.

Test262, percent passing

  • SpiderMonkey / 98.6%
  • V8 / 98.3%
  • SpiderMonkey 98.2%
  • LibJS 97.6%
  • V8 97.5%
  • Boa 95.4%
  • Kiesel 93.4%
  • JavaScriptCore / 92.0%
  • JavaScriptCore 85.2%
  • QuickJS NG 83.1%
  • QuickJS 82.5%
  • Nova 77.4%
  • Porffor 56.1%
A horizontal bar chart comparing the Test262 compliance percentages of various JavaScript engines, from SpiderMonkey at 98.6% down to Porffor at 56.1%.

Test262, percent passing

  • SpiderMonkey 👁️‍🗨️ 98.6%
  • V8 👁️‍🗨️ 98.3%
  • SpiderMonkey 98.2%
  • LibJS 97.6%
  • V8 97.5%
  • Boa 95.4%
  • Kiesel 93.4%
  • JavaScriptCore 👁️‍🗨️ 92.0%
  • JavaScriptCore 85.2%
  • QuickJS NG 83.1%
  • QuickJS 82.5%
  • Nova 77.4%
  • Porffor 56.1%

Bar chart comparing the percentage of Test262 compliance for different JavaScript engines.

Ladybird - Browsing a Slideshow

<!DOCTYPE html>
<html>
  <head>
    <title>Ladybird - Browsing a Slideshow</title>
    <link rel="stylesheet" href="main.css">
    <link rel="icon" href="favicon.svg">
  </head>
  <body>
    <div class="slideshow">
      <section data-slide>
        <h3>Ladybird</h3>
        <p>We need your help!</p>
        <p>
          We're a team of volunteers building a new browser
          engine from scratch.
        </p>
        <ul>
          <li>Written in C++ with <a href="https://serenityos.org/">SerenityOS</a> influence.</li>
          <li>Runs on Linux, macOS, Windows, Android, and <a href="https://serenityos.org/">SerenityOS</a>.</li>
          <li>Focus on speed, memory safety, and ...</li>
        </ul>
      </section>
      <section data-slide>
        <figure>
          <img src="slides/what-is-ladybird.png" alt="">
          <figcaption>Ladybird, the browser.</figcaption>
        </figure>
      </section>
Screenshot of the Lady Bird application, functioning as a code editor, displaying the HTML source code for a presentation slide deck. The visible tab shows 'index.html'.

Well, in fact...

Well, in fact...

Screenshot of the Ladybird browser displaying a local HTML file at file:///Users/joshua/Projects/ladybird/Tests/slides/index.html.

Well, in fact...

Screenshot of the Ladybird web browser displaying the current presentation slide.

Well, in fact...

Screenshot of the Ladybird web browser displaying a slide.

Ladybird Browser: Search History for cssday.nl

Screenshot of the Ladybird web browser. The address bar displays 'cssday.nl', with a dropdown showing a list of related history links for cssday.nl, the first item being highlighted.

CSS Day 2026, 11th & 12th of June, Amsterdam

Screenshot of the CSS Day website, cssday.nl, showing event details and navigation.
:css-day: 2026 {
  date: '11th and 12th of June, 2026';
  location: 'Amsterdam = NL';
}

Get your tickets

Our speakers {

The 2026 line-up is now complete:

Thursday 11th of June

Bruce Lawson: MC

A veteran of the browser wars, many a standards skirmish and an accessibility apocalypse or two; Bruce now leverages synergies for Vivaldi browser:

When web standards finally makes him a billionaire, Bruce has no plans to go to Mars; but will continue making music with the cruellest months:

Abstract logo spelling out CSS DAY 2026. Semi-transparent headshot of Bruce Lawson looking upwards, against a blurred background.

Jelle is the COO of the Ladybird Browser Initiative, a San Francisco based non-profit operating on donations and sponsorships. He has been involved with the SerenityOS project since 2021, which gave birth to the Ladybird browser. He oversees operations and finances while remaining actively involved in the browser's development and day-to-day coding.

Putting the C in CSS Crimes

We all know that CSS is a programming language, so let's see what happens if we truly use it as one.

In this talk I'll show you how to to make interactive art and games: a CSS x86 CPU that runs C code, and some classic Cohost CSS crimes.

Lyra Rebane

Lyra Rebane is a security researcher and former Cohoster from Estonia with a bunch of browser CVEs and a love for the web. She loves writing CSS, be it as a styling language for web design, or as a programming language for making games. Check out her cool website at lyra.horse.

Headshot of Jelle, a man with a beard and short hair, smiling.

Headshot of Lyra Rebane, a woman with long hair looking forward.

We'll show you how we pick a winner in the cascade, how we resolve relative units and calc() into absolute values, where layout sneaks back in, and why getComputedStyle() doesn't always return what its name suggests. A peek behind the curtain at the machinery your CSS quietly relies on every day.

Jelle Raaijmakers

Jelle is the COO of the Ladybird Browser Initiative, a San Francisco based non-profit operating on donations and sponsorships. He has been involved with the SerenityOS project since 2021, which gave birth to the Ladybird browser. He oversees operations and finances while remaining actively involved in the browser's development and day-to-day coding.

Putting the C in CSS Crimes

We all know that CSS is a programming language, so let's see what happens if we truly use it as one.

In this talk I'll show you how to to make interactive art and games: a CSS x86 CPU that runs C code, and some classic Cohost CSS crimes.

Lyra Rebane

Headshot of Jelle Raaijmakers.

Ladybird, the independent browser engine we're building from scratch against the web standards.

We'll show you how we pick a winner in the cascade, how we resolve relative units and `calc()` into absolute values, where layout sneaks back in, and why `getComputedStyle()` doesn't always return what its name suggests. A peek behind the curtain at the machinery your `CSS` quietly relies on every day.

Jelle Raaijmakers

Jelle is the COO of the Ladybird Browser Initiative, a San Francisco based non-profit operating on donations and sponsorships. He has been involved with the SerenityOS project since 2021, which gave birth to the Ladybird browser. He oversees operations and finances while remaining actively involved in the browser's development and day-to-day coding.

A headshot of Jelle Raaijmakers, a man with a beard, smiling and wearing a vest over a shirt.

Our speakers {

The 2026 line-up is now complete.

Thursday 11th of June

Bruce Lawson: MC

A veteran of the browser wars, many a standards skirmish and an accessibility apocalypse or two, Bruce now leverages synergies for Vivaldi browser.

When web standards finally makes him a billionaire, Bruce has no plans to go to Mars, but will continue making music with the cruellest months.

A black and white photo of Bruce Lawson, a man with spiky hair and glasses, looking upwards and to the left, with tree branches in the background.

QuirksMode.org and Peter-Paul Koch

Screenshot of the QuirksMode.org website, displaying the 'About' page with navigation on the left and a profile picture of Peter-Paul Koch on the right.

About QuirksMode.org and Peter-Paul Koch

Screenshot of the QuirksMode.org website's "About" page, displayed in a browser.

About QuirksMode.org and Peter-Paul Koch

Screenshot of the QuirksMode.org "About" page, showing a navigation sidebar, a search bar, the main content area, and a photo of Peter-Paul Koch.

About QuirksMode.org and Peter-P

Screenshot of the QuirksMode.org website's about page.

About QuirksMode.org and Peter-Paul Koch

Screenshot of the Ladybird web browser displaying the 'About' page of QuirksMode.org.

Cooperating processes

USER-FACING

  • Browser UI

    Windows, tabs, address bar, input, platform frontend

  • WebView

    Embeds web content and tasks to sandboxed processes

WEB PLATFORM

  • WebContent

    LibWeb, DOM, CSS, Javascript, style, layout, paint

SANDBOXED PROCESSES

  • RequestServer

    Network loading

  • ImageDecoder

    Image decoding

  • WebWorker

    Worker execution

  • Compositor

    Async scrolling and composition

Connected through IPC

A conceptual diagram illustrating different types of cooperating processes within a system, categorized into user-facing, web platform, and sandboxed processes. These processes include Browser UI, WebView, WebContent, RequestServer, ImageDecoder, WebWorker, and Compositor, all connected through Inter-Process Communication (IPC).

Cooperating processes

USER-FACING

  • Browser UI: Windows, tabs, address bar, input, platform frontend
  • WebView: Embeds web content and talks to sandboxed processes

WEB PLATFORM

  • WebContent: LibWeb, DOM, CSS, Javascript, style, layout, paint

SANDBOXED PROCESSES

  • RequestServer: Network loading
  • ImageDecoder: Image decoding
  • WebWorker: Worker execution
  • Compositor: Async scrolling and composition

Connected through IPC

A diagram illustrating the cooperating processes within a web browser, categorized into User-Facing, Web Platform, and Sandboxed Processes, with descriptions for each component and an indication that they are connected through Inter-Process Communication (IPC).

Cooperating processes

USER-FACING

  • Browser UI
    • Windows, tabs, address bar,
    • input, platform frontend
  • WebView
    • Embeds web content and talks
    • to sandboxed processes

WEB PLATFORM

  • WebContent
    • LibWeb, DOM, CSS, Javascript, style, layout, paint

SANDBOXED PROCESSES

  • RequestServer
    • Network loading
  • ImageDecoder
    • Image decoding
  • WebWorker
    • Worker execution
  • Compositor
    • Async scrolling and composition

Connected through IPC

A diagram illustrating browser architecture with cooperating processes divided into three columns: USER-FACING, WEB PLATFORM, and SANDBOXED PROCESSES. All processes are indicated as being connected through IPC (Inter-Process Communication).

Cooperating processes

USER-FACING

Browser UI
Windows, tabs, address bar, input, platform frontend
WebView
Embeds web content and talks to sandboxed processes

WEB PLATFORM

WebContent
LibWeb, DOM, CSS, Javascript, style, layout, paint

SANDBOXED PROCESSES

RequestServer
Network loading
ImageDecoder
Image decoding
WebWorker
Worker execution
Compositor
Async scrolling and composition

Connected through IPC

Cooperating processes

USER-FACING

Browser UI
  • Windows, tabs, address bar, input, platform frontend
WebView
  • Embeds web content and talks to sandboxed processes

WEB PLATFORM

WebContent
  • LibWeb, DOM, CSS, JavaScript, style, layout, paint

SANDBOXED PROCESSES

RequestServer
  • Network loading
ImageDecoder
  • Image decoding
WebWorker
  • Worker execution
Compositor
  • Async scrolling and composition

Connected through IPC

A three-column diagram illustrating cooperating processes within a web browser architecture. The columns are titled 'USER-FACING', 'WEB PLATFORM', and 'SANDBOXED PROCESSES'. Each column lists multiple process boxes with their names and descriptions. The 'WebContent' process box under 'WEB PLATFORM' is highlighted. A line at the bottom indicates that these processes are 'Connected through IPC'.

Cooperating processes

USER-FACING

  • Browser UI
    Windows, tabs, address bar, input, platform frontend
  • WebView
    Embeds web content and talks to sandboxed processes

WEB PLATFORM

  • WebContent
    LibWeb, DOM, CSS, JavaScript, style, layout, paint

SANDBOXED PROCESSES

  • RequestServer
    Network loading
  • ImageDecoder
    Image decoding
  • WebWorker
    Worker execution
  • Compositor
    Async scrolling and composition

Connected through IPC

A diagram illustrating the cooperation of different browser processes, categorized into User-Facing, Web Platform, and Sandboxed Processes, which communicate through Inter-Process Communication (IPC).

Cooperating processes

USER-FACING

  • Browser UI
    Windows, tabs, address bar, input, platform frontend
  • WebView
    Embeds web content and talks to sandboxed processes

WEB PLATFORM

  • WebContent
    LibWeb, DOM, CSS, Javascript, style, layout, paint

SANDBOXED PROCESSES

  • RequestServer
    Network loading
  • ImageDecoder
    Image decoding
  • WebWorker
    Worker execution
  • Compositor
    Async rendering and composition

Connected through IPC

A diagram listing browser processes categorized into User-Facing, Web Platform, and Sandboxed Processes. The "WebContent" item under "WEB PLATFORM" is highlighted.

Cooperating processes

USER-FACING

  • Browser UI
    Windows, tabs, address bar, input, platform frontend
  • WebView
    Embeds web content and talks to sandboxed processes

WEB PLATFORM

  • WebContent
    LibWeb, DOM, CSS, JavaScript, style, layout, paint

SANDBOXED PROCESSES

  • RequestServer
    Network loading
  • ImageDecoder
    Image decoding
  • WebWorker
    Worker execution
  • Compositor
    Async scrolling and composition

Connected through IPC

A diagram illustrating cooperating browser processes, categorized into three columns: User-Facing, Web Platform, and Sandboxed Processes, connected through IPC.

Cooperating processes

  • USER-FACING

    • Browser UI

      Windows, tabs, address bar, input, platform frontend

    • WebView

      Embeds web content and talks to sandboxed processes

  • WEB PLATFORM

    • WebContent

      LibWeb, DOM, CSS, JavaScript, style, layout, paint

  • SANDBOXED PROCESSES

    • RequestServer

      Network loading

    • ImageDecoder

      Image decoding

    • WebWorker

      Worker execution

    • Compositor

      Async scrolling and composition

Connected through IPC

Cooperating processes

USER-FACING

  • Browser UI

    Windows, tabs, address bar, input, platform frontend

  • WebView

    Embeds web content and talks to sandboxed processes

WEB PLATFORM

  • WebContent

    LibWeb, DOM, CSS, JavaScript, style, layout, paint

SANDBOXED PROCESSES

  • RequestServer

    Network loading

  • ImageDecoder

    Image decoding

  • WebWorker

    Worker execution

  • Compositor

    Async scrolling and composition

Connected through IPC

Diagram illustrating three categories of browser processes: User-Facing, Web Platform, and Sandboxed Processes, each with a list of components and their functions. The WebContent component in the Web Platform category is highlighted.

CSS inside LibWeb

WebContent / LibWeb

DOM

Elements, attributes, tree mutation

CSS Parser

Tokens become rules, selectors, and values

StyleComputer

Matches selectors, cascades, defaults, computes

Layout

Boxes become sizes and positions

Painting

Display commands are generated

JavaScript

CSSOM reads and mutations enter the same machinery

Web specs

Editing, Fetch, IndexedDB, HTML, Web IDL, Storage, and more

A conceptual diagram illustrating the components and processes within LibWeb for rendering web content. It shows a grid of seven boxes, each representing a distinct component with a title and a brief description. The 'CSS Parser' and 'StyleComputer' boxes are highlighted.

CSS inside LibWeb

WebContent / LibWeb

  • DOM

    Elements, attributes, tree mutation

  • CSS Parser

    Tokens become rules, selectors, and values

  • StyleComputer

    Matches selectors, cascades, defaults, computes

  • Layout

    Boxes become sizes and positions

  • Painting

    Display commands are generated

  • JavaScript

    CSSOM reads and mutations enter the same machinery

  • Web specs

    Editing, Fetch, IndexedDB, HTML, Web IDL, Storage, and more

A block diagram illustrating the various components and processes within WebContent / LibWeb that contribute to rendering web content, including DOM, CSS Parser, StyleComputer, Layout, Painting, JavaScript, and Web specs.

CSS inside LibWeb

WebContent / LibWeb

  • DOM: Elements, attributes, tree mutation
  • Layout: Boxes become sizes and positions
  • Web specs: Editing, Fetch, IndexedDB, HTML, Web IDL, Storage, and more
  • CSS Parser: Tokens become rules, selectors, and values
  • StyleComputer: Matches selectors, cascades, defaults, computes
  • Painting: Display commands are generated
  • JavaScript: C/DOM reads and mutations enter the same machinery
A diagram titled 'CSS inside LibWeb' showing interconnected components: DOM, CSS Parser, StyleComputer, Layout, Painting, JavaScript, and Web specs.

CSS inside LibWeb

WebContent / LibWeb

DOM

Elements, attributes, tree mutation

Layout

Boxes become sizes and positions

Web specs

Editing, Fetch, IndexedDB, HTML, Web IDL, Storage, and more

CSS Parser

Tokens become rules, selectors, and values

Painting

Display commands are generated

StyleComputer

Matches selectors, cascades, inherits, computes

JavaScript

DOM APIs and mutations enter the same machinery

A diagram illustrating the components within LibWeb, grouped into three columns. The components include DOM, Layout, Web specs, CSS Parser, StyleComputer, Painting, and JavaScript, each with a brief description. The CSS Parser and StyleComputer components are highlighted.

CSS inside LibWeb

WebContent / LibWeb

  • DOM

    Elements, attributes, tree mutation

  • Layout

    Boxes become sizes and positions

  • Web specs

    Editing, Fetch, IndexedDB, HTML, Web IDL, Storage, and more

  • CSS Parser

    Tokens become rules, selectors, and values

  • StyleComputer

    Matches selectors, cascades, defaults, computes

  • Painting

    Display commands are generated

  • JavaScript

    CSISOM state and mutations alter the same machinery

A conceptual diagram illustrating the components and processes within LibWeb related to CSS, including DOM, layout, web specifications, CSS parsing, style computation, painting, and JavaScript interaction.

CSS inside LibWeb

WebContent / LibWeb

DOM
Elements, attributes, tree mutation
CSS Parser
Styles become rules, selectors, and values
StyleComputer
Matches selectors, cascades, defaults, computes
Layout
Boxes become sizes and positions
Painting
Display commands are generated
JavaScript
CSSTOM trees and mutations enter the same machinery
Web specs
Editing, Fetch, IndexedDB, HTML, Web IDL, Storage, and more
A diagram illustrating the components involved in CSS rendering within LibWeb. It lists DOM, CSS Parser, StyleComputer, Layout, Painting, JavaScript, and Web specs. CSS Parser and StyleComputer are highlighted within blue boxes.

CSS inside LibWeb

WebContent / LibWeb

DOM

Elements, attributes, tree mutation

CSS Parser

Tokens become rules, selectors, and values

StyleComputer

Matches selectors, cascades, defaults, computes

Layout

Boxes become sizes and positions

Painting

Display commands are generated

JavaScript

CSSOM reads and mutations enter the same machinery

Web specs

Editing, Fetch, IndexedDB, HTML, Web IDL, Storage, and more

A diagram illustrating components within LibWeb, organized into a grid. The components include DOM, CSS Parser, StyleComputer, Layout, Painting, JavaScript, and Web specs. The CSS Parser and StyleComputer boxes are highlighted, indicating they are the current focus.

The page pipeline around CSS

  • Load

    RequestServer fetches HTML, CSS, images, scripts

  • Parse

    HTML becomes DOM. CSS becomes CSSOM and style values

  • Style

    Selectors match. Values cascade, default, and compute

  • Layout

    Boxes get sizes and positions

  • Paintable tree

    Layout boxes get paintable counterparts

  • Display list

    Paintables record drawing commands

  • Composite

    Output reaches the screen

A diagram illustrates the page pipeline around CSS, showing seven sequential steps represented as connected boxes. The "Style" box is highlighted.

The page pipeline around CSS

Load

RequestServer fetches HTML, CSS, images, scripts

Parse

HTML becomes DOM. CSS becomes CSSOM and style values

Style

Selectors match. Values cascade, default, and compute

Layout

Boxes get sizes and positions

Paintable tree

Layout boxes get paintable counterparts

Display list

Paintables record drawing commands

Composite

Output reaches the screen

A diagram illustrates the browser rendering pipeline around CSS, showing seven distinct steps arranged in a grid. The steps are Load, Parse, Style, Layout, Paintable tree, Display list, and Composite. The "Style" step is highlighted with a lighter background.

The page pipeline around CSS

  • Load

    RequestServer fetches HTML, CSS, images, scripts

  • Parse

    HTML becomes DOM, CSS becomes CSSOM and style values

  • Style

    Selections match. Values cascade, default, and compute

  • Layout

    Boxes get sizes and positions

  • Paintable tree

    Layout boxes get paintable counterparts

  • Display list

    Paintable record drawing commands

  • Composite

    Output reaches the screen

A diagram illustrating the page rendering pipeline around CSS, showing seven distinct stages: Load, Parse, Style, Layout, Paintable tree, Display list, and Composite. The "Style" stage is highlighted.

The page pipeline around CSS

  • Load: Request/Server fetches HTML, CSS, images, scripts
  • Parse: HTML becomes DOM, CSS becomes CSSOM and style values
  • Style: Selections match, Values cascade, default, and compute
  • Layout: Boxes get sizes and positions
  • Paintable tree: Layout boxes get paintable counterparts
  • Display list: Paintable record drawing commands
  • Composite: Output reaches the screen
A diagram illustrates "The page pipeline around CSS," showing seven stages in a flow: Load, Parse, Style, Layout, Paintable tree, Display list, and Composite. The "Style" stage is highlighted with a blue background.

The page pipeline around CSS

Load

Request/Server fetches HTML, CSS, images, scripts

Parse

HTML becomes DOM, CSS becomes CSSOM and style values

Style

Selections match, Values cascade, default, and compute

Layout

Boxes get sizes and positions

Paintable tree

Layout boxes get paintable counterparts

Display list

Paintable record drawing commands

Composite

Output reaches the screen

A diagram outlining the page rendering pipeline around CSS, with the 'Style' stage emphasized by a surrounding box.

The page pipeline around CSS

Load
RequestServer fetches HTML, CSS, images, fonts
Parse
HTML becomes DOM, CSS becomes CSSOM and style values
Style
Selectors match, Values cascade, default, and compute
Layout
Boxes get sizes and positions
Paintable tree
Layout boxes got paintable counterparts
Display list
Paintables record drawing commands
Composite
Output matches the screen

A diagram illustrating the seven stages of the page pipeline around CSS: Load, Parse, Style, Layout, Paintable tree, Display list, and Composite. The 'Style' stage is visually highlighted.

One CSS value, many stages

width: 50%;

Simple enough, right?

Six CSS value stages

  • Stylesheet text
  • Declared
  • Cascaded
  • Specified
  • Computed
  • Used
  • Actual
A vertical flow diagram illustrating the six CSS value stages: Stylesheet text, Declared, Cascaded, Specified, Computed, Used, and Actual.

Stylesheet text

.card {
  width: 50%;
}
  • Stylesheet text
  • Declared
  • Cascaded
  • Specified
  • Computed
  • Used
  • Actual
A vertical flowchart shows the progression from 'Stylesheet text' to 'Declared', 'Cascaded', 'Specified', 'Computed', 'Used', and 'Actual' values.

Declared values: valid declarations

If the property is unknown, the selector does not match, or the value is invalid, it does not enter the pipeline for this element.

Stylesheet text

  • Declared
  • Cascaded
  • Specified
  • Computed
  • Used
  • Actual
A vertical diagram illustrates the CSS value processing pipeline, showing a flow from "Stylesheet text" through steps: Declared, Cascaded, Specified, Computed, Used, and Actual. The "Declared" step is highlighted.

Declared values: valid declarations

If the property is unknown, the selector does not match, or the value is invalid, it does not enter the pipeline for this element.

  • Stylesheet text
  • Declared
  • Cascaded
  • Specified
  • Computed
  • Used
  • Actual
A flow diagram illustrates the CSS cascade process, depicting the stages from "Stylesheet text" through "Declared", "Cascaded", "Specified", "Computed", "Used", to "Actual", with arrows indicating the sequential flow.

Declared values: valid declarations

If the property is unknown, the selector does not match, or the value is invalid, it does not enter the pipeline for this element.

Stylesheet text

  • Declared
  • Cascaded
  • Specified
  • Computed
  • Used
  • Actual

A diagram illustrates the CSS value processing pipeline. It starts with a label "Stylesheet text", followed by a series of connected boxes arranged vertically with downward arrows, representing the stages: Declared, Cascaded, Specified, Computed, Used, and Actual.

Property metadata

// Libraries/LibWeb/CSS/Properties.json

"width": {
	"animation-type": "by-computed-value",
	"inherited": false,
	"initial": "auto",
	"requires-computation": "cascaded-value",
	"valid-types": [
		"length [0,-]",
		"percentage [0,-]"
	],
	"valid-identifiers": [
		"auto", "max-content", "min-content"
	],
	"needs-layout-for-getcomputedstyle": true
}

Property metadata

// Libraries/LibWeb/CSS/Properties.json
"width": {
  "animation-type": "by-computed-value",
  "inherited": false,
  "initial": "auto",
  "requires-computation": "cascaded-value",
  "valid-types": [
    "length [0,∞]",
    "percentage [0,∞]"
  ],
  "valid-identifiers": [
    "auto", "max-content", "min-content"
  ],
  "needs-layout-for-getcomputedstyle": true
}

Property metadata

// Libraries/LibWeb/CSS/Properties.json
"width": {
	"animation-type": "by-computed-value",
	"inherited": false,
	"initial": "auto",
	"requires-computation": "cascaded-value",
	"valid-types": [
		{
			"length": "[0,-]",
			"percentage": "[0,-]"
		}
	],
	"valid-identifiers": [
		"auto", "max-content", "min-content"
	],
	"needs-layout-for-getcomputedstyle": true
}

Property metadata

// Libraries/LibWeb/CSS/Properties.json
"width": {
    "animation-type": "by-computed-value",
    "inherited": false,
    "initial": "auto",
    "requires-computation": "cascaded-value",
    "valid-types": [
        "length [0,∞]",
        "percentage [0,∞]"
    ],
    "valid-identifiers": [
        "auto", "max-content", "min-content"
    ],
    "needs-layout-for-getcomputedstyle": true
}

Parser as filter

Only recognized properties with values matching that property's syntax become declared values.

Grammar demo

A screenshot of a user interface demonstrating a grammar parser. It displays fields for 'property' and 'value'. The 'property' dropdown is open, showing options like 'font-size', 'width', 'color', and 'display', with 'color' currently highlighted. Below the input fields, a message states "accepted: font-size: 2em".

Parser as filter

Only recognized properties with values matching that property's syntax become declared values.

Grammar demo

Property: width

Value: 2em

Accepted: width: 2em

A screenshot of an interactive user interface labeled 'Grammar demo', demonstrating property parsing. It shows input fields for 'property' (set to 'width') and 'value' (set to '2em'), with an output line indicating 'accepted: width: 2em'.

Grammar demo

Screenshot of a 'Grammar demo' user interface with input fields for 'property' and 'value'. The 'property' field is set to 'width', and the 'value' field contains '2em'. A message below confirms 'accepted: width: 2em'.

Grammar demo

Property: width

Value: 2em

Accepted: width: 2em

Screenshot of a dark-themed user interface titled 'Grammar demo', showing input fields for 'property' (set to 'width') and 'value' (set to '2em'), with a confirmation message 'accepted: width: 2em'.

Grammar demo

Property: width

Value: 2em

accepted: width: 2em

Screenshot of a "Grammar demo" user interface showing input fields. The 'property' field is set to 'width', and the 'value' field is set to '2em'. Below these fields, a message states "accepted: width: 2em".

Grammar demo

Screenshot of a 'Grammar demo' user interface. It contains two input fields labeled 'property' and 'value'. The 'property' field shows 'width', and the 'value' field shows 'rebeccap'. Below these, a message 'rejected: width: rebeccap' is displayed in red.

Grammar demo

Property: width

Value: rebeccapurple

rejected: width: rebeccapurple

Screenshot of a 'Grammar demo' application interface with two input fields. The first is labeled 'property' and contains the text 'width'. The second is labeled 'value' and contains the text 'rebeccapurple'. Below these fields, a message in red text reads 'rejected: width: rebeccapurple'.

Grammar demo

Property selected: width. Dropdown options visible: font-size, width (checked), color (hovered), display.

Value input field.

Error message: rejected: width: rebeccapurple

Screenshot of a user interface demonstrating a "Grammar demo". It features input fields for a CSS-like property and value. The property "width" is selected from a dropdown, and an error message "rejected: width: rebeccapurple" is displayed, indicating an invalid value was attempted.

Parser as filter

Only recognized properties with values matching that property's syntax become declared values.

Grammar demo

Property: color

Value: rebeccapurple

Output: accepted: color: rebeccapurple

A user interface element titled 'Grammar demo' shows input fields. The 'property' field displays 'color', and the 'value' field displays 'rebeccapurple'. Below these inputs, a green text label indicates 'accepted: color: rebeccapurple'.

The parser gate in LibWeb

The grammar demo is calling into the same kind of question a browser has to answer before a value can become declared.

Demo API: CSS.supports(property, value)

// Libraries/LibWeb/CSS/CSS.cpp
bool supports(JS::VM&, Utf16FlyString const& p)
{
	if (auto property = PropertyNameAndID::from(...)) {
		if (parse_css_value(Parser::ParsingParameters(...)))
			return true;
	}

	return false;
}

A code snippet from a C++ file showing a boolean function named supports.

// Libraries/LibWeb/CSS/CSS.cpp
bool supports(JS::VM&, Utf16FlyString const& property_name, StringView value)
{
    if (auto property = PropertyNameAndID::from_name(property_name); property.has_value()) {
        if (parse_css_value(Parser::ParsingParams {}, value, property->id()))
            return true;
    }
    return false;
}

Cascaded value: one winner

There can be many declared values for one property. The cascade chooses at most one.

  • Stylesheet text
  • Declared
  • Cascaded
  • Specified
  • Computed
  • Used
  • Actual
A vertical flow diagram on the right shows stages of CSS value resolution, represented by connected rectangular boxes. The "Cascaded" box is highlighted.

One property, one winner

LibWeb applies declarations in cascade order. Later passes can replace earlier winners.


// Libraries/LibWeb/CSS/StyleComputer.cpp
// Normal author declarations, with inner contexts first so outer contexts win.
for (auto const& context : matching_rule_set.author_contexts.in_reverse()) {
	for (auto const& layer : context.author_rules) {
		for (auto const& rule : layer.rules) {
			cascade_declarations(cascaded_properties, abstract_element, layer.rules, Cascade);
		}
	}
}
if (context.shadow_root = element_context.shadow_root) {
	cascade_inline_style(Important::No);
}

Cascade demo

  • candidate selector: .action
  • later in CSS: no
  • important: !important

.action {
	color: rebeccapurple;
}

button.action {
	color: slate;
}

winner: button.action -> color: slate

button.action

The slide presents a C++ code snippet on the left related to CSS style computation. On the right, there's a "Cascade demo" UI panel with controls including a slider for "candidate selector" set to '.action', an unchecked checkbox for "later in CSS" showing 'no', and a checked checkbox for "important" showing '!important'. Below the controls are two CSS code blocks. The first defines `color: rebeccapurple;` for `.action`. The second, highlighted, defines `color: slate;` for `button.action`. A text output states "winner: button.action -> color: slate", followed by the text "button.action".

One property, one winner

LibWeb applies declarations in cascade order. Later passes can replace earlier winners.

// Libraries/LibWeb/CSS/StyleComputer.cpp
// Normal author declarations, with inner contexts first so outer contexts win.
for (auto const& context : matching_rule_set.author_contexts.in_reverse()) {
    for (auto const& layer : context.author_rules) {
        cascade_declarations(cascaded_properties, abstract_element, layer.rules, Cascadi...
        if (context.shadow_root = element_context.shadow_root)
            cascade_inline_style(Important::No);
    }
}

Cascade demo

Candidate selector: main button.action

  • Later in CSS: no
  • Important: !important
main button.action {
    color: rebeccapurple;
}
button.action {
    color: slate;
}

winner: main button.action -> color: rebeccapurple

button.action

An interactive demo illustrating CSS cascade rules, featuring controls for a candidate selector, 'later in CSS' and 'important' flags. Below the controls are two CSS code blocks showing competing style rules, a displayed winner declaration, and a representation of the styled button labeled 'button.action'.

One property, one winner

LibWeb applies declarations in cascade order. Later passes can replace earlier winners.

// Libraries/LibWeb/CSS/StyleComputer.cpp
// Normal author declarations, with inner contexts first so outer contexts win.
for (auto const& context : matching_rule_set.author_contexts.in_reverse()) {
    for (auto const& layer : context.author_rules) {
        cascade_declarations(cascaded_properties, abstract_element, layer.rules, Cascadi
        if (context.shadow_root == element_context_shadow_root)
            cascade_inline_style(Important::No);
    }
}

Cascade demo

Candidate selector: button.action

Later in CSS: Yes

Important: !important (unchecked)

button.action {
  color: slate;
}
button.action {
  color: rebeccapurple;
}

winner: button.action -> color: rebeccapurple

A UI demonstrating CSS cascade rules with interactive controls. It shows two CSS code blocks, a result indicating the winning rule, and a preview button labeled "button.action" styled by the winning rule. The demo controls include a slider for "candidate selector" set to "button.action", a checkbox for "later in CSS" which is checked (Yes), and a checkbox for "important" which is unchecked (!important).

One property, one winner

LibWeb applies declarations in cascade order. Later passes can replace earlier winners.

// Libraries/LibWeb/CSS/StyleComputer.cpp
// Normal author declarations, with inner contexts first so outer contexts win.
for (auto const& context : matching_rule_set.author_contexts.in_reverse()) {
	for (auto const& layer : context.author_rules) {
		cascade_declarations(cascaded_properties, abstract_element, layer.rules, Cascade
		if (context.shadow_root = element_context_shadow_root)
		cascade_inline_style(Important::No);
	}
}

Cascade demo

candidate selector: button.action

later in CSS: no

important: !important

button.action {
    color: rebeccapurple;
}
button.action {
    color: slate;
}

winner: button.action -> color: slate

button.action

A demonstration panel illustrating CSS cascade rules, featuring a slider for "candidate selector", checkboxes for "later in CSS" and "important", two distinct CSS code blocks, a text display indicating the winning rule, and a preview of a button labeled "button.action" reflecting the applied style.

One property, one winner

LibWeb applies declarations in cascade order. Later passes can replace earlier winners.

// Libraries/LibWeb/CSS/StyleComputer.cpp
// Normal author declarations, with inner contexts first so outer contexts win.
for (auto const& context : matching_rule_set.author_contexts.in_reverse()) {
    for (auto const& layer : context.author_rules) {
        cascade_declarations(cascaded_properties, abstract_element, layer.rules, Cascade);
        if (context.shadow_root = element_context_shadow_root)
            cascade_inline_style(Important::No);
    }
}

Cascade demo

button.action

no

!important

button.action {
    color: rebeccapurple;
}
button.action {
    color: slate;
}

winner: button.action -> color: slate

A code editor screenshot showing a C++ file `StyleComputer.cpp` with a loop iterating through author contexts and layers to cascade CSS declarations. Below that, a UI demonstrating CSS cascade rules with controls for candidate selector (currently 'button.action'), whether it's later in CSS (currently 'no'), and if it's important (currently '!important'). It displays two competing CSS rules for `button.action` (one setting `color: rebeccapurple;` and another setting `color: slate;`), and the resulting winner, `color: slate;`. A button element labeled 'button.action' is shown below the winner statement.

One property, one winner

LibWeb applies declarations in cascade order. Later passes can replace earlier winners.

// LibWeb/css/cascade.cpp
// Normal author declarations, with author contexts first in inner contexts win.
for (auto const& context : matching_rule_set.author_contexts.in_reverse()) {
	for (auto const& layer : context.author_rules) {
		cascade_declarations(cascaded_properties, abstract_element, layer.rules, CascadeOrigin::Author, Important::No, layer.qualified_layer_name, false);
		if (context.shadow_root == element.context.shadow_root) {
			cascade_inline_style(Important::No);
		}
	}
}

// Important author declarations, with author contexts first in inner contexts win.
for (auto const& context : matching_rule_set.author_contexts()) {
	for (auto const& layer : context.author_rules) {
		cascade_declarations(cascaded_properties, abstract_element, layer.rules, CascadeOrigin::Author, Important::Yes, {}, false);
		if (context.shadow_root == element.context.shadow_root) {
			cascade_inline_style(Important::Yes);
		}
	}
}

cascade_declarations(cascaded_properties, abstract_element, matching_rule_set.user_rules, CascadeOrigin::User, Important::Yes, {}, false);
cascade_declarations(cascaded_properties, abstract_element, matching_rule_set.user_agent_rules, CascadeOrigin::UserAgent, Important::Yes, {}, false);

Cascade demo

candidate selector

later in CSS

Important

Button.action {
  color: rebeccapurple;
}
A UI showing interactive controls for a CSS cascade demo, including an input field for a candidate selector, a slider labeled "later in CSS," and a checkbox labeled "Important."

One property, one winner

LibWeb applies declarations in cascade order. Later passes can replace earlier winners.

// Libraries/LibWeb/CSS/StyleComputer.cpp
// Normal author declarations, with inner contexts first so outer contexts win.
for (auto const& context : matching_rule_set.author_contexts.in_reverse()) {
    for (auto const& layer : context.author_rules) {
        cascade_declarations(cascaded_properties, abstract_element, layer.rules, CascadeOrigin::Author, Important::No, layer.qualified_layer_name, false);
        if (context.shadow_root == element_context_shadow_root)
            cascade_inline_style(Important::No);
    }
}
// Important author declarations, with outer contexts first so inner contexts win.
for (auto const& context : matching_rule_set.author_contexts()) {
    for (auto const& layer : context.author_rules.in_reverse()) {
        cascade_declarations(cascaded_properties, abstract_element, layer.rules, CascadeOrigin::Author, Important::Yes, {}, false);
        if (context.shadow_root == element_context_shadow_root)
            cascade_inline_style(Important::Yes);
    }
}
cascade_declarations(cascaded_properties, abstract_element, matching_rule_set.user_rules, CascadeOrigin::User, Important::Yes, {}, false);
cascade_declarations(cascaded_properties, abstract_element, matching_rule_set.user_agent_rules, CascadeOrigin::UserAgent, Important::Yes, {}, false);

CSS snippet from Cascade demo

button.action {
    color: rebeccapurple;
}
A user interface element titled "Cascade demo" features interactive controls (a slider and checkboxes) to adjust cascade parameters for a displayed CSS code snippet. Below this, a multi-part code block illustrates C++ functions related to CSS cascade rule processing for normal author, important author, user, and user agent declarations.

One property, one winner

LibWeb applies declarations in cascade order. Later passes can replace earlier winners.

// Libraries/LibWeb/CSS/StyleComputer.cpp
// Normal author declarations, with inner contexts first so outer contexts win.
for (auto const& context : matching_rule_set.author_contexts.in_reverse()) {
	for (auto const& layer : context.author_rules, Cascade)
		cascade_declarations(cascaded_properties, abstract_element, layer.rules, Cascade
	if (context.shadow_root = element_context.shadow_root)
		cascade_inline_style(Important::No);
}

Cascade demo

Candidate selector: button.action

Later in CSS: no

Important: !important

button.action {
	color: rebeccapurple;
}
button.action {
	color: slate;
}

winner: button.action -> color: slate

Screenshot of a UI demonstrating CSS cascade rules. The UI includes input controls (slider, checkboxes), CSS code snippets, and a preview of a button with the text "button.action" reflecting the winning style.

Specified values: blanks filled

For width, the specified value is the cascaded value if one wins. Otherwise it is the initial value: auto.

// Libraries/LibWeb/CSS/StyleComputer.cpp
if (should_inherit && computed_properties_to_inherit_from) {
	computed_style->set_property_inherited(property_id, ComputedProperties::Inherited::Yes);
	value = computed_properties_to_inherit_from->property(
		inherited_property_id,
		ComputedProperties::WithAnimationsApplied::No);
	requires_computation = property_requires_computation_with_inherited_value(property_id);
}

if (!value || value->is_initial() || value->is_unset()
	|| (should_inherit && !computed_properties_to_inherit_from)) {
	value = property_initial_value(property_id);
	requires_computation = property_requires_computation_with_initial_value(property_id);
}
  • Stylesheet text
  • Declared
  • Cascaded
A screenshot of a code editor displaying C++ code. To the right of the main text, there is a vertical stack of three rectangular buttons, labeled "Stylesheet text", "Declared", and "Cascaded". The "Cascaded" button is highlighted.

CSS-wide keywords

  • inherit
  • initial
  • unset
  • revert
  • revert-layer

They tell the browser which part of inheritance, initial values, or cascade history to consult.

Computed values before layout

For width: 50%, the computed value can still be 50%. For border-left-width: `calc(0.8em + 2px)`, font-size is enough to compute pixels.

// Libraries/LibWeb/CSS/StyleComputer.cpp
auto const& absolutized_value = specified_value->absolutized(computation_context);

switch (property_id) {
case PropertyID::FontSize:
    return compute_font_size(
        absolutized_value,
        get_property_specified_value(PropertyID::MathDepth)->as_integer().integer());
}
  • Stylesheet text
  • Declared
  • Cascaded
  • Specified
  • Computed
  • Used
  • Actual
A diagram illustrates the stages of CSS value resolution, depicted as a vertical flow of interconnected rectangular boxes. The stages are: Stylesheet text, Declared, Cascaded, Specified, Computed, Used, and Actual. The 'Computed' stage is currently highlighted.

Computed values before layout

width: 50%, the computed value can still be 50%. For

border-left-width: calc(0.8em + 2px), font-size is enough to

compute pixels.

libraries/LibWeb/CSS/StyleComputer.cpp

const& absolutized_value = specified_value->absolutized(computation_context);

switch (property_id) {
PropertyID::FontSize:
	return compute_font_size(
absolutized_value,
get propertv specified value(PropertyID::MathDepth)->as integer().integer().

Computed values before layout

For width: 50%, the computed value can still be 50%. For border-left-width: calc(0.8em + 2px), font-size is enough to compute pixels.

// Libraries/LibWeb/CSS/StyleComputer.cpp
auto const& absolutized_value = specified_value->absolutized(computation_context);

switch (property_id) {
case PropertyID::FontSize:
	return compute_font_size(
		absolutized_value,
		get property specified_value(PropertyID::MathDepth)->as_integer().integer().
}

Computed values before layout

For width: 50%, the computed value can still be 50%. For border-left-width: calc(0.8em + 2px), font-size is enough to compute pixels.

// Libraries/LibWeb/CSS/StyleComputer.cpp
auto const& absolutized_value = specified_value->absolutized(computation_context);

switch (property_id) {
case PropertyID::FontSize:
    return compute_font_size(
        absolutized_value,
        get_property_specified_value(PropertyID::MathDepth)->as_integer().integer()
    );
}
A vertical flow diagram on the right shows stages of CSS value resolution: Stylesheet text, Declared, Cascaded, Specified, Computed (highlighted), Used, and Actual.

Computed values before layout

For width: 50%, the computed value can still be 50%. For border-left-width: calc(0.8em + 2px), font-size is enough to compute pixels.

// Libraries/LibWeb/CSS/StyleComputer.cpp
auto const& absolutized_value = specified_value->absolutized(computation_context);

switch (property_id) {
	case PropertyID::FontSize:
		return compute_font_size(
			absolutized_value,
			get_property_specified_value(PropertyID::MathDepth)->as_integer().integer(),
			inheritance_parent());
	case PropertyID::LineHeight:
		return compute_line_height(absolutized_value, computation_context.length_resolution_context.font_metrics.font_size);
	default:
		return absolutized_value;
}
  • Stylesheet text
  • Declared
  • Cascaded
  • Specified
A vertical flow diagram on the right shows four stages of CSS value processing: Stylesheet text, Declared, Cascaded, and Specified, connected by vertical lines.

Computed values before

A computed value can still be 50%. For instance, if you write width: calc(0.8em + 2px), font-size is enough to...


/StyleComputer.cpp
_value = specified_value->absolutized(computation_context);

size:
    _size(
        value,
        specified_value(PropertyID::MathDepth)->as_integer().integer(),
        font());
height:
    _height(absolutized_value, computation_context.length_resolution_context.font_metrics.font_size);
value;

Screenshot of a code editor displaying C++ code related to style computation, showing how values are absolutized and properties like size and height are calculated.

Computed values before layout

For width: 50%, the computed value can still be 50%. For border-left-width: `calc(0.8em + 2px)`, font-size is enough to compute pixels.

// Libraries/LibWeb/CSS/StyleComputer.cpp
auto const& absolutized_value = specified_value->absolutized(computation_context);

switch (property_id) {
case PropertyID::FontSize:
    return compute_font_size(
        absolutized_value,
        get_property_specified_value(PropertyID::MathDepth)->as_integer().integer(),
...
A vertical flow diagram shows stages of CSS value processing. The stages are: Stylesheet text, Declared, Cascaded, Specified, Computed, Used, and Actual. The 'Computed' stage is highlighted, indicating it is the current focus.

calc() and its available inputs

Change font-size and the absolute part of calc(0.8em + 2px) changes with it. Percentages still need layout.

calc() demo

font-size

border-left-width: calc(0.8em + 2px)
font-size: 24px; calc(0.8em + 2px) -> 21.2px
A demo interface showing a slider for font-size, a CSS code snippet `border-left-width: calc(0.8em + 2px)`, and a visual representation of a purple border. Below, the current font-size and the calculated `border-left-width` of 21.2px are displayed.

calc() and its available inputs

Change font-size and the absolute part of calc(0.8em + 2px) changes with it. Percentages still need layout.

calc() demo

font-size

border-left-width:
calc(0.8em + 2px)

font-size: 33px; calc(0.8em + 2px) -> 28.4px

A demo interface on the right side with a slider controlling 'font-size'. Below the slider, a code block shows a CSS rule `border-left-width: calc(0.8em + 2px)`. At the bottom, a line displays the computed values: `font-size: 33px; calc(0.8em + 2px) -> 28.4px`.

calc() and its available inputs

Change font-size and the absolute part of calc(0.8em + 2px) changes with it. Percentages still need layout.

calc() demo

font-size px

border-left-width: calc(0.8em + 2px)

font-size: 28px; calc(0.8em + 2px) -> 24.4px

A screenshot of a user interface demo for CSS calc(). The demo features a slider to adjust font-size, an editable CSS property input box displaying 'border-left-width: calc(0.8em + 2px)' with a grid background, and a display showing the calculated result: 'font-size: 28px; calc(0.8em + 2px) -> 24.4px'. A mouse cursor is positioned on the font-size slider.

border-left-width: calc(0.8em + 2px)

When font-size is 26px, calc(0.8em + 2px) evaluates to 22.8px.

A screenshot of an interactive demonstration. At the top, there is a slider control labeled "font-size" with units "px". Below, a dark gray box with a purple left border displays the CSS property `border-left-width: calc(0.8em + 2px)`. Further below, another dark gray box shows the calculated result of this property given a specific font size: "font-size: 26px; calc(0.8em + 2px) -> 22.8px". The background features a subtle dark grid pattern.

calc() demo

font-size px

2px)

border-left-width: calc(0.8em + 2px)

font-size: 26px; calc(0.8em + 2px) -> 22.8px

A UI element with a slider controlling font size. Below it, a code snippet `border-left-width: calc(0.8em + 2px)` is displayed within a box, which has a dynamic purple left border that visually reflects the calculated width.

calc() demo

border-left-width: calc(0.8em + 2px)

font-size: 41px; calc(0.8em + 2px) -> 34.8px

A user interface demonstrating the CSS calc() function. At the top, a slider labeled "font-size" with units "px" is set to 41px. Below the slider, a dark rectangular box contains CSS code: "border-left-width: calc(0.8em + 2px)". This box has a prominent purple left border. To the left of the box, the text "2px)" is displayed, visually indicating the fixed 2px component of the border width calculation.

calc() demo

font-size

border-left-width: calc(0.8em + 2px)

2px)

font-size: 37px; calc(0.8em + 2px) -> 31.6px

A user interface demonstrating the CSS `calc()` function. It features a slider for adjusting font-size, labeled "font-size" and indicating "px" units. A rectangular box displays the CSS rule `border-left-width: calc(0.8em + 2px)`. To its left, a purple vertical bar visually represents the calculated border width. An annotation "2px)" is positioned on the left, highlighting the fixed pixel value in the calculation. The display area has a grid background.

calc() and its available inputs

Change font-size and the absolute part of calc(0.8em + 2px) changes with it. Percentages still need layout.

calc() demo

font-size

border-left-width:
calc(0.8em + 2px)

font-size: 34px; calc(0.8em + 2px) -> 29.2px

A demo interface illustrating the CSS calc() function. It features a slider to adjust font-size, a code display showing the CSS rule border-left-width: calc(0.8em + 2px), and an output line indicating the calculated value for a font-size of 34px as 29.2px.

calc() and its available inputs

Change font-size and the absolute part of calc(0.8em + 2px) changes with it. Percentages still need layout.

calc() demo

font-size: px

border-left-width: calc(0.8em + 2px)

font-size: 21px; calc(0.8em + 2px) -> 18.8px

An interactive demo illustrating the CSS `calc()` function. It shows a slider for adjusting a font-size, and a rectangular box with a purple left border whose `border-left-width` is calculated using `calc(0.8em + 2px)`. Below the box, a display shows the current `font-size` as 21px and the resulting computed pixel value of `calc(0.8em + 2px)` as 18.8px.

calc() and its available inputs

Change font-size and the absolute part of calc(0.8em + 2px) changes with it. Percentages still need layout.

calc() demo

font-size

border-left-width: calc(0.8em + 2px)
font-size: 12px; calc(0.8em + 2px) -> 11.6px

calc() and its available inputs

Change font-size and the absolute part of calc(0.8em + 2px) changes with it. Percentages still need layout.

calc() demo

border-left-width: calc(0.8em + 2px)
font-size: 12px; calc(0.8em + 2px) -> 11.6px
A demo interface with a slider input labeled "font-size" and a visual representation of a border's width adjusting.

calc() and its available inputs

Change font-size and the absolute part of calc(0.8em + 2px) changes with it. Percentages still need layout.

calc() demo

font-size

border-left-width: calc(0.8em + 2px)

font-size: 12px; calc(0.8em + 2px) -> 11.6px

A screenshot of an interactive demo illustrating the CSS calc() function. It shows a control for adjusting font-size and displays a `border-left-width` property set with `calc(0.8em + 2px)`. An example calculation shows that with `font-size: 12px`, `calc(0.8em + 2px)` evaluates to `11.6px`.

Used values with layout context

For width: 50%, the containing block decides the used pixel value.

  • Stylesheet text
  • Declared
  • Cascaded
  • Specified
  • Computed
  • Used
  • Actual

A vertical list on the right side illustrates the stages of CSS value resolution, from Stylesheet text down to Actual values, with the 'Used' stage highlighted.

Percentages and geometry

The same computed percentage can produce different used pixel widths.

Layout demo

The following CSS properties are applied to the child element:

width: 50%;
max-width: 400px;

With a container width of 482px, the child width is calculated as `min(50%, 400px)`, resulting in 243px.

A layout demonstration features a slider to adjust the width of a 'container'. Inside the container, a grid displays a purple child element. The element's width is dynamically shown as it responds to the container's width and its own CSS properties: `width: 50%;` and `max-width: 400px;`. A displayed calculation shows that for a 482px container, the child width computes to 243px.

Percentages and geometry

The same computed percentage can produce different used pixel widths.

Child element CSS properties:

width: 50%;
max-width: 400px;

For a container with a width of 373px, the child's width is calculated using `min(50%, 400px)`, resulting in a used width of 186.5px.

A layout demo illustrating how percentages and max-width properties interact. The demo includes a slider to adjust the container's width, currently showing 373px. Inside the container, a child element is displayed. A purple box highlights the child's CSS properties: `width: 50%;` and `max-width: 400px;`. Below the demo, a text output confirms the calculation: `container: 373px; child width: min(50%, 400px) -> 186.5px`.

Percentages and geometry

The same computed percentage can produce different used pixel widths.

Layout demo

width: 50%;
max-width: 400px;

container: 373px; child width: min(50%, 400px) -> 186.5px

An interactive layout demonstration shows a parent container, whose width is adjustable via a slider labeled "container".

Inside the container, a child element is displayed with its CSS properties `width: 50%; max-width: 400px;` shown in a purple box.

A readout below the demo shows the container's current width, followed by the child's effective width calculation `min(50%, 400px)`, and its resulting pixel width.

Percentages and geometry

The same computed percentage can produce different used pixel widths.

Layout demo

The child element has the following CSS properties: width: 50%; and max-width: 400px;

container: 304px; child width: min(50%, 400px) -> 152px

A layout demo interface is shown on the right. It features a slider labeled 'container' which adjusts the width of a dark gray grid-patterned container. Inside this container, a purple rectangular child element is displayed. The slider is currently set to a position indicating a container width of 304 pixels.

Percentages and geometry

The same computed percentage can produce different used pixel widths.

Layout demo

container

width: 50%;
max-width: 400px;

container: 482px; child width: min(50%, 400px)
-> 241px

A layout demo showing a container with a slider to adjust its width. Inside the container is a purple rectangular block representing a child element with CSS properties `width: 50%;` and `max-width: 400px;`. Below the demo, the calculated pixel width of the child element is shown as 241px when the container is 482px wide, demonstrating how the child width is `min(50%, 400px)`.

Percentages and geometry

The same computed percentage can produce different used pixel widths.

Layout demo

Container width is set to 384px.

Child element properties: width: 50%; max-width: 400px;

Result: container: 384px; child width: min(50%, 400px) -> 192px

A "Layout demo" user interface features a slider to adjust a container's width, currently set to 384px. Below the slider, a grid visualizes the container with a child rectangular element inside it. A purple callout box displays the child's CSS properties: width 50% and max-width 400px. Below the grid, the calculated result shows the child's width as 192px.

Percentages and geometry

The same computed percentage can produce different used pixel widths.

Layout demo

container px

width: 50%;
max-width: 400px;

container: 506px; child width: min(50%, 400px)
--> 253px

An interactive layout demo showing a container with a grid background and a slider to adjust its width. Inside the container is a purple child element with CSS properties `width: 50%;` and `max-width: 400px;`. Below the demo, the calculated width is shown for a container width of 506 pixels, resulting in a child width of 253 pixels.

Actual values and the output device

The used value may still need output-environment adjustments: rounding, device pixels, font substitution, and similar constraints.

  • Stylesheet text
  • Declared
  • Cascaded
  • Specified
  • Computed
  • Used
  • Actual
A flowchart diagram illustrating the sequence of value types in CSS, from 'Stylesheet text' down through 'Declared', 'Cascaded', 'Specified', 'Computed', 'Used', and finally 'Actual', with 'Actual' highlighted.

Actual values and the output device

The used value may still need output-environment adjustments: rounding, device pixels, font substitution, and similar constraints.

  • Stylesheet text
  • Declared
  • Cascaded
  • Specified
  • Computed
  • Used
  • Actual
A diagram on the right illustrates a vertical flow of steps in CSS value processing, starting from Stylesheet text and proceeding through Declared, Cascaded, Specified, Computed, Used, and culminating in Actual, with the final step highlighted.

Actual values and the output device

The used value may still need output-environment adjustments: rounding, device pixels, font substitution, and similar constraints.

  • Stylesheet text
  • Declared
  • Cascaded
  • Specified
  • Computed
  • Used
  • Actual

A flow diagram illustrating the stages of CSS value processing, from stylesheet text to actual values, including declared, cascaded, specified, computed, and used values.

Actual values and the output device

The used value may still need output-environment adjustments: rounding, device pixels, font substitution, and similar constraints.

  • Stylesheet text
  • Declared
  • Cascaded
  • Specified
  • Computed
  • Used
  • Actual

A vertical flow diagram illustrates the stages of CSS value resolution, starting from 'Stylesheet text' and progressing through 'Declared', 'Cascaded', 'Specified', 'Computed', 'Used', and finally 'Actual'.

getComputedStyle() and resolved values

For compatibility, resolved values may expose computed values for some properties and used values for others.

  • getComputedStyle()
  • resolved value
  • Computed data
  • Example: line-height: normal
  • normal stays normal
  • Used/layout data
  • Example: width: 50%
  • layout returns a pixel width

A diagram illustrates how getComputedStyle() yields a resolved value, which can be categorized into 'Computed data' or 'Used/layout data'. Computed data examples include `line-height: normal` where 'normal' stays 'normal'. Used/layout data examples include `width: 50%` where the layout returns a pixel width.

getComputedStyle() and resolved values

For compatibility, resolved values may expose computed values for some properties and used values for others.

  • getComputedStyle() (resolved value)
    • Computed data
      • Example: line-height: normal
      • normal (CSS norm)
    • Used/layout data
      • Example: width: 50%
      • layout returns a pixel width

A diagram illustrating that `getComputedStyle()` returns a resolved value which can be either computed data (e.g., line-height) or used/layout data (e.g., width).

getComputedStyle() and resolved values

For compatibility, resolved values may expose computed values for some properties and used values for others.

getComputedStyle()

resolved value

Computed data
  • Example: line-height: normal
  • normal stays normal
Used/layout data
  • Example: width: 50%
  • layout returns a pixel width
A diagram illustrating how getComputedStyle() returns resolved values, categorizing them as either computed data (like line-height) or used/layout data (like a pixel width for a percentage width).

Resolved values as a CSSOM decision

Some properties can be serialized from style. Others need a layout node or up-to-date layout before CSSOM can answer.

// Libraries/LibWeb/CSS/CSSStyleProperties.cpp
Layout::NodeWithStyle* layout_node = abstract_

bool const needs_layout = property_needs_layou
bool const needs_layout_node = property_needs_

if (needs_layout || needs_layout_node) {
	abstract_element.document().update_layout(l
	layout_node = abstract_element.layout_node
}

auto value = style_value_for_computed_property

return StyleProperty {
	.property_id = property_id,
	.value = *value,
};
// Libraries/LibWeb/CSS/CSSStyleProperties.cpp
Layout::NodeWithStyle* layout_node = abstract_element.unsafe_layout_node();

bool const needs_layout = property_needs_layout_for_getcomputedstyle(property_id);
bool const needs_layout_node = property_needs_layout_node_for_resolved_value(property_id);

if (needs_layout || needs_layout_node) {
    abstract_element.document().update_layout(DOM::UpdateLayoutReason::ResolvedCSSStyleDeclarationProperty);
    layout_node = abstract_element.layout_node();
}

auto value = style_value_for_computed_property(*layout_node, property_id);

return StyleProperty {
    .property_id = property_id,
    .value = *value,
};
// Libraries/LibWeb/CSS/CSSStyleProperties.cpp
Layout::NodeWithStyle* layout_node = abstract_element.unsafe_layout_node();

bool const needs_layout = property_needs_layout_for_computed_style(property_id);
bool const needs_layout_node = property_needs_layout_node_for_resolved_value(property_id);

if (needs_layout || needs_layout_node) {
	abstract_element.document().update_layout(DOM::UpdateLayoutReason::ResolvedCSSStyleDeclarationProperty);
	layout_node = abstract_element.layout_node();
}

auto value = style_value_for_computed_property(*layout_node, property_id);

return StyleProperty {
	.property_id = property_id,
	.value = *value,
};
Screenshot of a code editor displaying C++ code related to CSS style properties and layout updates.

width: 50%, stage by stage

Stylesheet text
width: 50%;
Declared
valid width value
Cascaded
this declaration wins
Specified
50%, or initial auto
Computed
50%
Used
640px container -> 320px
Actual
after output adjustments, often still 320px

padding-left, stage by stage

Stylesheet text
base: calc(50% - var(--gap)); theme: revert-layer
Declared
both declarations parse and apply
Cascaded
later layer wins: revert-layer
Specified
rollback exposes base value: calc(50% - var(--gap))
Computed
--gap: 1.5em, font-size 16px -> calc(50% - 24px)
Used
333px containing block -> 166.5px - 24px = 142.5px
Actual
paint may snap to device pixels, e.g. 143px at DPR 1

Context-sensitive values

width: 50%

Needs containing-block geometry

calc(0.8em + 2px)

Needs computed font metrics

color: currentColor

Needs another property

var(--x)

Needs cascade and substitution

Custom properties and deferred substitution

.box {
  --space: 2em;
  padding: var(--space);
}

The custom property cascades first. Substitution happens later, when the browser finally knows where the value is being used.

Style storage changes shape

  • Declared

    StyleProperty lists

    Only what authors wrote

  • Cascaded

    sparse map

    Only properties with candidates

  • Computed

    dense longhand array

    Every longhand gets an answer

  • Used

    typed layout data

    Sizes, boxes, paintables

  • Custom properties

    shared chains

    Inherited variables without full copies

A diagram illustrating the flow of style storage changes, depicted as five connected rectangular boxes, each representing a stage: Declared, Cascaded, Computed, Used, and Custom properties.

Style storage changes shape

  • Declared

    StyleProperty lists

    Only what authors wrote

  • Cascaded

    sparse map

    Only properties with candidates

  • Computed

    dense longhand array

    Every longhand gets an answer

  • Used

    typed layout data

    Sizes, boxes, paintables

  • Custom properties

    shared chains

    Inherited variables without full copies

A diagram illustrating five stages of style storage, represented as interconnected blocks in a left-to-right flow.

Style storage changes shape

Declared

StyleProperty lists
Only what authors wrote

Cascaded

sparse map
Only properties with candidates

Computed

dense longhand array
Every longhand gets an answer

Used

typed layout data
Sizes, boxes, paintables

Custom properties

shared chains
Inherited variables without full copies

Style storage changes shape

  • Declared

    StyleProperty lists

    Only what authors wrote

  • Cascaded

    sparse map

    Only properties with candidates

  • Computed

    dense longhand array

    Every longhand gets an answer

  • Used

    typed layout data

    Sizes, boxes, paintables

  • Custom properties

    shared chains

    Inherited variables without full copies

A conceptual diagram illustrating different stages or forms of style storage, from declared to cascaded, computed, used, and custom properties, each with specific data structures and characteristics.

Style storage changes shape

Declared

StyleProperty lists
Only what authors wrote

Cascaded

sparse map
Only properties with candidates

Computed

dense longhand array
Every longhand gets an answer

Used

typed layout data
Sizes, boxes, paintables

Custom properties

shared chains
Inherited variables without full copies

Style storage changes shape

Declared

  • StyleProperty lists
  • Only whilst authors wrote

Cascaded

  • sparse map
  • Only properties with candidates

Computed

  • dense longhand array
  • Every longhand gets an answer

Used

  • typed layout data
  • Sizes, boxes, paintables

Custom properties

  • shared chains
  • Inherited variables without full copies

Style storage changes shape

  • Declared

    • StyleProperty lists
    • Only what authors wrote
  • Cascaded

    • sparse map
    • Only properties with candidates
  • Computed

    • dense longhand array
    • Every longhand gets an answer
  • Used

    • typed layout data
    • Sizes, boxes, paintables
  • Custom properties

    • shared chains
    • Inherited variables without full copies

Style storage changes shape

Declared

StyleProperty lists

Only what authors wrote

Cascaded

sparse map

Only properties with candidates

Computed

dense longhand array

Every longhand gets an answer

Used

typed layout data

Sizes, boxes, paintables

Custom properties

shared chains

Inherited variables without full copies

A flow diagram illustrating the evolution of style storage, showing five distinct stages or types of storage represented by interconnected rectangular boxes arranged horizontally.

Style storage changes shape

Declared

StyleProperty lists
Only what authors wrote

Cascaded

sparse map
Only properties with candidates

Computed

dense longhand array
Every longhand gets an answer

Used

typed layout data
Sizes, boxes, paintables

Custom properties

shared chains
Inherited variables without full copies

A diagram illustrating five stages of style storage, shown as interconnected boxes from left to right: Declared, Cascaded, Computed, Used, and Custom properties.

Style storage changes shape

Declared

StyleProperty lists

Only what authors

Cascaded

sparse map

Only properties with candidates

Computed

dense longhand array

Every longhand gets

Used

typed layout data

Sizes, boxes, paintables

Custom properties

shared chains

Inherited variables without full copies

A diagram illustrating five stages or types of CSS style storage: Declared, Cascaded, Computed, Used, and Custom properties, each with a key concept and brief description.

Style storage changes shape

Declared

StyleProperty lists
  • Only what authors wrote

Cascaded

sparse map
  • Only properties with candidates

Computed

dense longhand array
  • Every longhand gets an answer

Used

typed layout data
  • Sizes, boxes, paintables

Custom properties

shared chains
  • Inherited variables without full copies

Sparse, dense, shared

Sparse while deciding

The cascade stores only properties that have declarations to compare

Dense once computed

Computed style is indexed by longhand property ID

Shared for variables

Custom properties form inherited parent chains

Thrown away

The temporary cascade object does not need to live with the page

// Libraries/LibWeb/CSS/CascadedProperties.h
HashMap<PropertyID, Vector<Entry>> m_properties;
AK::FixedBitmap<to_underlying(last_longhand_pr...
m_contained_properties_cache { false };

// Libraries/LibWeb/CSS/ComputedProperties.h
Array<RefPtr<StyleValue const>, number_of_longl...
m_property_values;
Array<u8, ceil_div(number_of_longhand_properti...
m_property_inherited {};
Code snippet illustrating C++ data structures for CSS properties.

Dense maps, shared

// Libraries/LibWeb/CSS/CascadedProperties.h
HashMap<PropertyID, Vector<Entry>> m_properties;
AK::FixedBitmap<to_underlying(last_longhand_property_id) + 1>
    m_contained_properties_cache { false };
// Libraries/LibWeb/CSS/ComputedProperties.h
Array<RefPtr<StyleValue const>>, number_of_longhand_properties>
    m_property_values;
Array<u8, ceil_div(number_of_longhand_properties, 8uz)>
    m_property_inherited {};
// Libraries/LibWeb/CSS/CascadedProperties.h
HashMap<PropertyID, Vector<Entry>> m_properties;
AK::FixedBitmap<to_underlying(last_longhand_property_id) + 1>
    m_contained_properties_cache { false };

// Libraries/LibWeb/CSS/ComputedProperties.h
Array<RefPtr<StyleValue const>, number_of_longhand_properties>
    m_property_values;
Array<u8, ceil_div(number_of_longhand_properties, 8uz)>
    m_property_inherited {};

Sparse, dense, shared

  • Sparse while deciding

    The cascade stores only properties that have declarations to compare

  • Dense once computed

    Computed style is indexed by longhand property ID

  • Shared for variables

    Custom properties form inherited parent chains

  • Thrown away

    The temporary cascade object does not need to live with the page

// Libraries/LibWeb/CSS/CascadedProperties.h
HashMap<PropertyID, Vector<Entry>> m_properties;
AK::FixedBitmap<to_underlying(last_longhand_property_id)>
	m_contained_properties_cache { false };

// Libraries/LibWeb/CSS/ComputedProperties.h
Array<RefPtr<StyleValue> const>, number_of_longhand_properties>
	m_property_values;
Array<u8, ceil_div(number_of_longhand_properties, 8)>
	m_property_inherited {};
A code snippet displaying C++ data structures for CSS cascaded and computed properties, including HashMap, FixedBitmap, and Array types.

Time-travelling font-size

html { font-size: medium; }
body { font-size: 200%; }
main { font-size: 2em; }
article { font-family: monospace; }

Usual inheritance

16px -> 32px -> 64px

Monospace inheritance

13px -> 26px -> 52px

A later font-family result changes how earlier inherited font-size values are interpreted.

// Libraries/LibWeb/CSS/StyleCompute
auto font_family_value =
    cascaded_properties.property(CSS::PropertyID::FontFamily);
if (!font_family_value || !is_monospace_font(font_family_value))
    return nullptr;

for (auto& ancestor : ancestors.in_reverse()) {
    auto font_size_value = ancestor.computed_style().property(CSS::PropertyID::FontSize);

    if (auto absolute_size = keyword_to_absolute_font_size(font_size_value)) {
        current_size_in_px = absolute_size;
    }

    if (font_size_value->is_percentage()) {
        current_size_in_px = font_size_value->to_percentage().resolve_against(current_size_in_px);
    }
}

Time-travelling font-size

html { font-size: medium; }
body { font-size: 200%; }
main { font-size: 2em; }
article { font-family: monospace; }
Usual inheritance
16px -> 32px -> 64px
Monospace inheritance
13px -> 26px -> 52px

A later font-family result changes how earlier inherited font-size values are interpreted.

// Libraries/LibWeb/CSS/StyleCompute
auto font_family_value =
	cascaded_properties.property(CSS::...);
if (!font_family_value || !is_monospace)
	return nullptr;

for (auto& ancestor : ancestors.in_reverse())
	auto font_size_value = ancestor.computed_values().font_size();

if (auto absolute_size = keyword_to_absolute_size(font_size_value.to_keyword()))
	current_size_in_px = absolute_size.to_px();

if (font_size_value->is_percentage())
	current_size_in_px = font_si;
}
// Libraries/LibWeb/CSS/StyleComputer.cpp
auto font_family_value =
    cascaded_properties.property(CSS::PropertyID::FontFamily);
if (!font_family_value || !is_monospace(*font_family_value))
    return nullptr;

for (auto& ancestor : ancestors.in_reverse()) {
    auto font_size_value = ancestor.computed_properties()->raw_cascaded_font_size();

    if (auto absolute_size = keyword_to_absolute_size(font_size_value->to_keyword()); absolute_size.has_value())
        current_size_in_px = absolute_size_mapping(absolute_size.value(), default_monospace_font_size_in_px);

    if (font_size_value->is_percentage())
        current_size_in_px = font_size_value->as_percentage().percentage().as_fraction() * current_size_in_px;
}
A code snippet from LibWeb/CSS/StyleComputer.cpp, showing C++ logic for computing font sizes, including handling monospace fonts, keywords, and percentages.

Ladybird concepts for the six stages

Declared
Parser :: Declaration and StyleProperty
Cascaded
CascadedProperties tracks winners and sources
Specified
StyleComputer applies inheritance, initial values, CSS-wide keywords
Computed
ComputedProperties stores computed longhand StyleValues
Used
Layout resolves percentages, constraints, and geometry
Actual
Painting and compositing turn style into device output

Specified and computed values in StyleComputer

This handles logical properties, inheritance, animations, viewport units, custom properties, and more.

// Libraries/LibWeb/CSS/StyleComputer
auto device_pixels_per_css_pixel =
    m_document->page().client().devi.

for (auto const& property_id : prope
    auto const& computation_context
        get_computation_context_for_
    auto const& specified_value =
        style.property(property_id,

    auto const& computed_value =
        compute_value_of_property(
            property_id,
            specified_value,
            get_property_specified_v
            computation_context,
            device_pixels_per_css_pi

    style.set_property_without_modifi
}
// Libraries/LibWeb/CSS/StyleComputer.cpp
auto device_pixels_per_css_pixel =
    m_document->page().client().device_pixels_per_css_pixel();

for (auto const& property_id : property_computation_order()) {
    auto const& computation_context =
        get_computation_context_for_property(property_id, style, abstract_element);
    auto const& specified_value =
        style.property(property_id, ComputedProperties::WithAnimationsApplied::No);

    auto const& computed_value =
        compute_value_of_property(
            property_id,
            specified_value,
            get_property_specified_value,
            computation_context,
            device_pixels_per_css_pixel);

    style.set_property_without_modifying_flags(property_id, computed_value);
}
Screenshot of C++ code in an IDE.

@container and the page tree

The rule parses early, but matching it for an element depends on an eligible ancestor, its computed container-type, writing mode, and measured container features.

@container card (inline-size > 40rem) {
  .title { font-size: 2rem; }
}
// Libraries/Lib/CSS/ContainerQuery.cpp
for (auto const* container = element.element();
     container = container->flat_tree_parent_element();
     container = container_button_ancestor();
     continue) {
  if (!container.name_matches(*container, container_name))
    continue;

  if (!container_satisfies_requirements(*container))
    continue;

  return m_condition->evaluate({
    .document = &element.document(),
    .query_container = container,
  });
}

@container and the page tree

The rule parses early, but matching it for an element depends on an eligible ancestor, its computed container-type, writing mode, and measured container features.

@container card (inline-size > 40rem) {
  .title { font-size: 2rem; }
}
// Libraries/LibWeb/CSS/ContainerQuery.cpp
for (auto const* container = element.element()) {
    container;
    container = container->flat_tree_parent_el...
    if (!container_name_matches(*container, co...
        continue;

    if (!container_satisfies_requirements(*con...
        continue;

    return m_condition->evaluate({
        .document = &element.document(),
        .query_container = container,
    });
}

@container and the page tree

The rule parses early, but matching it for an element depends on an eligible ancestor, its computed container-type, writing mode, and measured container features.

@container card (inline-size > 40rem) {
  .title { font-size: 2rem; }
}
// Libraries/LibWeb/CSS/ContainerQuery.cpp
for (auto const* container = element.element();
	container;
	container = container->flat_tree_parent_element()) {
  if (!container_name_matches(*container, container_name))
    continue;

  if (!container_satisfies_requirements(*container, query_conditions))
    continue;

  return m_condition->evaluate({
    .document = &element.document(),
    .query_container = container,
  });
}
@container card (inline-size > 40rem) {
  .title { font-size: 2rem; }
}

@container and the page tree

The rule parses early, but matching it for an element depends on an eligible ancestor, its computed container-type, writing mode, and measured container features.

// Libraries/LibWeb/CSS/ContainerQuery.cpp
for (auto const* container = element.element()) {
  container;
  container = container->flat_tree_parent_el...
  if (!container_name_matches(*container, co...
    continue;

  if (!container_satisfies_requirements(*cont...
    continue;

  return m_condition->evaluate({
    .document = &element.document;(),
    .query_container = container,
  });
}
A CSS code block demonstrating a container query for styling a card based on its inline size. A C++ code snippet illustrating the internal logic for evaluating container queries, showing checks for container name matching and requirement satisfaction.

@container and the page

// Libraries/LibWeb/CSS/ContainerQuery.cpp
for (auto const* container = element.element().flat_tree_parent_element();
     container;
     container = container->flat_tree_parent_element()) {
  if (!container_name_matches(*container, container_name))
    continue;

  if (!container_satisfies_requirements(*container, m_feature_requirements))
    continue;

  return m_condition->evaluate({
    .document = &element.document(),
    .query_container = container,
  });
}
return MatchResult::Unknown;
@container card (inline-size > 40rem) {
  .title { font-size: 2rem; }
}

CSS as a language with a runtime

A stylesheet is not just read. It is parsed, filtered, cascaded, defaulted, computed, laid out, painted, and exposed back to JavaScript.

Brought to you today by Ladybird, a brand-new browser.

A white vertical banner with a dark, stylized, blocky 'CSS' logo at the bottom, composed of interconnected rectangular shapes.

CSS as a language with a runtime

A stylesheet is not just read. It is parsed, filtered, cascaded, defaulted, computed, laid out, painted, and exposed back to JavaScript.

Brought to you today by Ladybird, a brand-new browser.

Thank you
A logo featuring an abstract, intertwined purple symbol.

Thank you

An abstract logo resembling interconnected orbits or a stylized atom.

Thank you

An abstract logo featuring two intertwined, elongated oval shapes.

Thank you

A stylized purple logo depicting interconnected loops or an abstract atom symbol.

Thank you

An abstract, stylized icon resembling intertwined loops or an atom is displayed next to the text.

Thank you

An abstract purple logo resembling an atom or interconnected nodes.

Thank you

An abstract logo featuring interconnected purple rings or orbits.

.now {

  • 12:40-13:55 | Lunch break

& Thank you

Google AG Grid

CSS Day logo.

An icon resembling a crossed-out box or the Roman numerals 'XI' in the bottom right corner of the slide.

LESS DAY

Stylized text forming the words 'LESS DAY' using abstract block shapes.

CSS Day

#cssday www.cssday.nl

Stylized logo for CSS Day, formed by black rectangular blocks spelling out 'CSS' on the top row and 'DAY' on the bottom row.

People

  • Andreas Kling
  • Chris Wanstrath
  • Jake Archibald

Technologies & Tools

  • Apple Lockdown Mode
  • Blink
  • C++
  • FFmpeg
  • Gecko
  • LibJS
  • Rust
  • SerenityOS
  • Servo
  • SpiderMonkey
  • V8
  • WebAssembly
  • WebKit

Standards & Specs

  • CSS Animations
  • CSS Cascade Layers
  • CSS Container Queries
  • CSS Custom Properties
  • CSS Transforms
  • DOM
  • Firefox DevTools protocol
  • getComputedStyle
  • IndexedDB
  • Test262
  • VP8
  • VP9
  • Web Platform Tests

Concepts & Methods

  • Copy-on-write
  • CSS Specificity
  • JIT compilation

Organisations & Products

  • Chromium
  • Firebird
  • Firefox
  • GitHub
  • Google
  • Igalia
  • Mozilla