Tech News
How Design Teams Are Reacting to 10x Developer Productivity from AI
At this point it's pretty obvious that AI coding agents can massively accelerate the time it takes to build software. But when software development teams experience huge productivity booms, how do design teams respond? Here's the most common reactions I've seen.
In all the technology companies I've worked at, big and small, there's always been a mindset of "we don't have enough resources to get everything we want done." Whether that's an excuse or not, companies consistently strive for more productivity. Well, now we have it.
More and more developers are finding that today's AI coding agents massively increase their productivity. As an example, Amazon's Joe Magerramov recently outlined how his "team's 10x throughput increase isn't theoretical, it's measurable." And before you think "vibe coding, crap" his post is a great walkthrough on how developers moving at 200 mph are cognizant of the need to keep quality high and rethink a lot of their process to effectively implement 100 commits a day vs. 10.
But what happens to software design teams when their development counterparts are shipping 10x faster? I've seen three recurring reactions:
- Our Role Has Changed
- We're Also Faster Now
- It's Just Faster Slop
Instead of spending most of their time creating mockups that engineers will later be asked to build, designers increasingly focus on UX alignment after things are built. That is, ensuring the increased volume of features developers are coding fit into a cohesive product experience. This flips the role of designers and developers.
For years, design teams operated "out ahead" of engineering, unburdened by technical debt and infrastructure limitations. Designers would spend time in mockups and prototypes envisioning what could be build before development started. Then developers would need to "clean up" by working out all the edge cases, states, technical issues, etc that came up when it came time to implement.
Now development teams are "out ahead" of design, with new features becoming code at a furious pace. UX refinement and thoughtful integration into the structure and purpose of a product is the "clean up" needed afterward.
We're Also Faster NowAn increasing number of designers are picking up AI coding tools themselves to prototype and even ship features. If developers can move this fast with AI, why can't designers? This lets them stay closer to the actual product rather than working in abstract mockups. At Perplexity, designers and engineers collaborate directly on prompting as a programming language. At Sigma, designers are fixing UX issues in production using tools like Augment Code.
It's Just Faster SlopThe third response I hear is more skeptical: just because AI makes developers faster doesn't mean it makes good products. While it feels good to take the high ground, the reality is software development is changing. Developers won't be going back to 1x productivity any time soon.
"Ninety percent of everything is crap" - Sturgeon's lawIt's also worth remembering Sturgeon's Law which originated when the science fiction writer was asked why 90% of science fiction writing is crap. He replied that 90% of everything is crap.
So is a lot of AI-generated code not great? Sure, but a lot of code is not great period. As always it's very hard to make something good, regardless of the tools one uses. For both designers and developers, the tools change but the fundamental job doesn't.
Getting Creative With Small Screens
Over the past few months, I’ve explored how we can get creative using well-supported CSS properties. Each article is intended to nudge web design away from uniformity, toward designs that are more distinctive and memorable. One bit of feedback from Phillip Bagleg deserves a follow up:
Andy’s guides are all very interesting, but mostly impractical in real life. Very little guidance on how magazine style design, works when thrown into a responsive environment.
Fair point well made, Phillip. So, let’s bust the myth that editorial-style web design is impractical on small screens.
My brief: Patty Meltt is an up-and-coming country music sensation, and she needed a website to launch her new album and tour. She wanted it to be distinctive-looking and memorable, so she called Stuff & Nonsense. Patty’s not real, but the challenges of designing and developing sites like hers are.
The problem with endless columnsOn mobile, people can lose their sense of context and can’t easily tell where a section begins or ends. Good small-screen design can help orient them using a variety of techniques.
When screen space is tight, most designers collapse their layouts into a single long column. That’s fine for readability, but it can negatively impact the user experience when hierarchy disappears; rhythm becomes monotonous, and content scrolls endlessly until it blurs. Then, nothing stands out, and pages turn from being designed experiences into content feeds.
Like a magazine, layout delivers visual cues in a desktop environment, letting people know where they are and suggesting where to go next. This rhythm and structure can be as much a part of visual storytelling as colour and typography.
But those cues frequently disappear on small screens. Since we can’t rely on complex columns, how can we design visual cues that help readers feel oriented within the content flow and stay engaged? One answer is to stop thinking in terms of one long column of content altogether. Instead, treat each section as a distinct composition, a designed moment that guides readers through the story.
Designing moments instead of columnsEven within a narrow column, you can add variety and reduce monotony by thinking of content as a series of meaningfully designed moments, each with distinctive behaviours and styles. We might use alternative compositions and sizes, arrange elements using different patterns, or use horizontal and vertical scrolling to create experiences and tell stories, even when space is limited. And fortunately, we have the tools we need to do that at our disposal:
- @media and @container queries
- CSS Grid and Flexbox
- Scroll Snap
- Orientation media features
- Logical properties
These moments might move horizontally, breaking the monotony of vertical scrolling, giving a section its own rhythm, and keeping related content together.
Make use of horizontal scrollingMy desktop design for Patty’s discography includes her album covers arranged in a modular grid. Layouts like these are easy to achieve using my modular grid generator.
But that arrangement isn’t necessarily going to work for small screens, where a practical solution is to transform the modular grid into a horizontal scrolling element. Scrolling horizontally is a familiar behaviour and a way to give grouped content its own stage, the way a magazine spread might.
I started by defining the modular grid’s parent — in this case, the imaginatively named modular-wrap — as a container:
.modular-wrap { container-type: inline-size; width: 100%; }Then, I added grid styles to create the modular layout:
.modular { display: grid; gap: 1.5rem; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(2, 1fr); overflow-x: visible; width: 100%; }It would be tempting to collapse those grid modules on small screens into a single column, but that would simply stack one album on top of another.
Collapsing grid modules on small screens into a single columnSo instead, I used a container query to arrange the album covers horizontally and enable someone to scroll across them:
@container (max-width: 30rem) { #example-1 .modular { display: grid; gap: 1.5rem; grid-auto-columns: minmax(70%, 1fr); grid-auto-flow: column; grid-template-columns: none; grid-template-rows: 1fr; overflow-x: auto; -webkit-overflow-scrolling: touch; } } Album covers are arranged horizontally rather than vertically. See this example in my lab.Now, Patty’s album covers are arranged horizontally rather than vertically, which forms a cohesive component while preventing people from losing their place within the overall flow of content.
Push elements off-canvasLast time, I explained how to use shape-outside and create the illusion of text flowing around both sides of an image. You’ll often see this effect in magazines, but hardly ever online.
The illusion of text flowing around both sides of an imageDesktop displays have plenty of space available, but what about smaller ones? Well, I could remove shape-outside altogether, but if I did, I’d also lose much of this design’s personality and its effect on visual storytelling. Instead, I can retain shape-outside and place it inside a horizontally scrolling component where some of its content is off-canvas and outside the viewport.
My content is split between two divisions: the first with half the image floating right, and the second with the other half floating left. The two images join to create the illusion of a single image at the centre of the design:
<div class="content"> <div> <img src="img-left.webp" alt=""> <p><!-- ... --></p> </div> <div> <img src="img-right.webp" alt=""> <p><!-- ... --></p> </div> </div>I knew this implementation would require a container query because I needed a parent element whose width determines when the layout should switch from static to scrolling. So, I added a section outside that content so that I could reference its width for determining when its contents should change:
<section> <div class="content"> <!-- ... --> </div> </section> section { container-type: inline-size; overflow-x: auto; position: relative; width: 100%; }My technique involves spreading content across two equal-width divisions, and these grid column properties will apply to every screen size:
.content { display: grid; gap: 0; grid-template-columns: 1fr 1fr; width: 100%; }Then, when the section’s width is below 48rem, I altered the width of my two columns:
@container (max-width: 48rem) { .content { grid-template-columns: 85vw 85vw; } }Setting the width of each column to 85% — a little under viewport width — makes some of the right-hand column’s content visible, which hints that there’s more to see and encourages someone to scroll across to look at it.
Some of the right-hand column’s content is visible. See this example in my lab.The same principle works at a larger scale, too. Instead of making small adjustments, we can turn an entire section into a miniature magazine spread that scrolls like a story in print.
Build scrollable mini-spreadsWhen designing for a responsive environment, there’s no reason to lose the expressiveness of a magazine-inspired layout. Instead of flattening everything into one long column, sections can behave like self-contained mini magazine spreads.
Sections can behave like self-contained mini magazine spreads.My final shape-outside example flowed text between two photomontages. Parts of those images escaped their containers, creating depth and a layout with a distinctly editorial feel. My content contained the two images and several paragraphs:
<div class="content"> <img src="left.webp" alt=""> <img src="right.webp" alt=""> <p><!-- ... --></p> <p><!-- ... --></p> <p><!-- ... --></p> </div>Two images float either left or right, each with shape-outside applied so text flows between them:
.content img:nth-of-type(1) { float: left; width: 45%; shape-outside: url("left.webp"); } .spread-wrap .content img:nth-of-type(2) { float: right; width: 35%; shape-outside: url("right.webp"); }That behaves beautifully at large screen sizes, but on smaller ones it feels cramped. To preserve the design’s essence, I used a container query to transform its layout into something different altogether.
First, I needed another parent element whose width would determine when the layout should change. So, I added a section outside so that I could reference its width and gave it a little padding and a border to help differentiate it from nearby content:
<section> <div class="content"> <!-- ... --> </div> </section> section { border: 1px solid var(--border-stroke-color); box-sizing: border-box; container-type: inline-size; overflow-x: auto; padding: 1.5rem; width: 100%; }When the section’s width is below 48rem, I introduced a horizontal Flexbox layout:
@container (max-width: 48rem) { .content { align-items: center; display: flex; flex-wrap: nowrap; gap: 1.5rem; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; } }And because this layout depends on a container query, I used container query units (cqi) for the width of my flexible columns:
.content > * { flex: 0 0 85cqi; min-width: 85cqi; scroll-snap-align: start; } On small screens, the layout flows from image to paragraphs to image. See this example in my lab.Now, on small screens, the layout flows from image to paragraphs to image, with each element snapping into place as someone swipes sideways. This approach rearranges elements and, in doing so, slows someone’s reading speed by making each swipe an intentional action.
To prevent my images from distorting when flexed, I applied auto-height combined with object-fit:
.content img { display: block; flex-shrink: 0; float: none; height: auto; max-width: 100%; object-fit: contain; }Before calling on the Flexbox order property to place the second image at the end of my small screen sequence:
.content img:nth-of-type(2) { order: 100; }Mini-spreads like this add movement and rhythm, but orientation offers another way to shift perspective without scrolling. A simple rotation can become a cue for an entirely new composition.
Make orientation-responsive layoutsWhen someone rotates their phone, that shift in orientation can become a cue for a new layout. Instead of stretching a single-column design wider, we can recompose it entirely, making a landscape orientation feel like a fresh new spread.
Turning a phone sideways is an opportunity to recompose a layout.Turning a phone sideways is an opportunity to recompose a layout, not just reflow it. When Patty’s fans rotate their phones to landscape, I don’t want the same stacked layout to simply stretch wider. Instead, I want to use that additional width to provide a different experience. This could be as easy as adding extra columns to a composition in a media query that’s applied when the device’s orientation is detected in landscape:
@media (orientation: landscape) { .content { display: grid; grid-template-columns: 1fr 1fr; } }For the long-form content on Patty Meltt’s biography page, text flows around a polygon clip-path placed over a large faux background image. This image is inline, floated, and has its width set to 100%:
<div class="content"> <img src="patty.webp" alt=""> <!-- ... --> </div> .content > img { float: left; width: 100%; max-width: 100%; }Then, I added shape-outside using the polygon coordinates and added a shape-margin:
.content > img { shape-outside: polygon(...); shape-margin: 1.5rem; }I only want the text to flow around the polygon and for the image to appear in the background when a device is held in landscape, so I wrapped that rule in a query which detects the screen orientation:
@media (orientation: landscape) { .content > img { float: left; width: 100%; max-width: 100%; shape-outside: polygon(...); shape-margin: 1.5rem; } } See this example in my lab.Those properties won’t apply when the viewport is in portrait mode.
Design stories that adapt, not layouts that collapseSmall screens don’t make design more difficult; they make it more deliberate, requiring designers to consider how to preserve a design’s personality when space is limited.
Phillip was right to ask how editorial-style design can work in a responsive environment. It does, but not by shrinking a print layout. It works when we think differently about how content flexes, shifts, and scrolls, and when a design responds not just to a device, but to how someone holds it.
The goal isn’t to mimic miniature magazines on mobile, but to capture their energy, rhythm, and sense of discovery that print does so well. Design is storytelling, and just because there’s less space to tell one, it shouldn’t mean it should make any less impact.
Getting Creative With Small Screens originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
CSS Animations That Leverage the Parent-Child Relationship
Modern CSS has great ways to position and move a group of elements relative to each other, such as anchor positioning. That said, there are instances where it may be better to take up the old ways for a little animation, saving time and effort.
We’ve always been able to affect an element’s structure, like resizing and rotating it. And when we change an element’s intrinsic sizing, its children are affected, too. This is something we can use to our advantage.
Let’s say a few circles need to move towards and across one another. Something like this:
Our markup might be as simple as a <main> element that contains four child .circle elements:
<main> <div class="circle"></div> <div class="circle"></div> <div class="circle"></div> <div class="circle"></div> </main>As far as rotating things, there are two options. We can (1) animate the <main> parent container, or (2) animate each .circle individually.
Tackling that first option is probably best because animating each .circle requires defining and setting several animations rather than a single animation. Before we do that, we ought to make sure that each .circle is contained in the <main> element and then absolutely position each one inside of it:
main { contain: layout; } .circle { position: absolute; &:nth-of-type(1){ background-color: rgb(0, 76, 255); } &:nth-of-type(2){ background-color: rgb(255, 60, 0); right: 0; } &:nth-of-type(3){ background-color: rgb(0, 128, 111); bottom: 0; } &:nth-of-type(4){ background-color: rgb(255, 238, 0); right: 0; bottom: 0; } }If we rotate the <main> element that contains the circles, then we might create a specific .animate class just for the rotation:
/* Applied on <main> (the parent element) */ .animate { width: 0; transform: rotate(90deg); transition: width 1s, transform 1.3s; }…and then set it on the <main> element with JavaScript when the button is clicked:
const MAIN = document.querySelector("main"); function play() { MAIN.className = ""; MAIN.offsetWidth; MAIN.className = "animate"; }It looks like we’re animating four circles, but what we’re really doing is rotating the parent container and changing its width, which rotates and squishes all the circles in it as well:
CodePen Embed FallbackEach .circle is fixed to a respective corner of the <main> parent with absolute positioning. When the animation is triggered in the parent element — i.e. <main> gets the .animate class when the button is clicked — the <main> width shrinks and it rotates 90deg. That shrinking pulls each .circle closer to the <main> element’s center, and the rotation causes the circles to switch places while passing through one another.
This approach makes for an easier animation to craft and manage for simple effects. You can even layer on the animations for each individual element for more variations, such as two squares that cross each other during the animation.
/* Applied on <main> (the parent element) */ .animate { transform: skewY(30deg) rotateY(180deg); transition: 1s transform .2s; .square { transform: skewY(30deg); transition: inherit; } } CodePen Embed FallbackSee that? The parent <main> element makes a 30deg skew and flip along the Y-axis, while the two child .square elements counter that distortion with the same skew. The result is that you see the child squares flip positions while moving away from each other.
If we want the squares to form a separation without the flip, here’s a way to do that:
/* Applied on <main> (the parent element) */ .animate { transform: skewY(30deg); transition: 1s transform .2s; .square { transform: skewY(-30deg); transition: inherit; } } CodePen Embed FallbackThis time, the <main> element is skewed 30deg, while the .square children cancel that with a -30deg skew.
Setting skew() on a parent element helps rearrange the children beyond what typical rectangular geometry allows. Any change in the parent can be complemented, countered, or cancelled by the children depending on what effect you’re looking for.
Here’s an example where scaling is involved. Notice how the <main> element’s skewY() is negated by its children and scale()s at a different value to offset it a bit.
/* Applied on <main> (the parent element) */ .animate { transform: rotate(-180deg) scale(.5) skewY(45deg) ; transition: .6s .2s; transition-property: transform, border-radius; .squares { transform: skewY(-45deg) scaleX(1.5); border-radius: 10px; transition: inherit; } } CodePen Embed FallbackThe parent element (<main>) rotates counter-clockwise (rotate(-180deg)), scales down (scale(.5)), and skews vertically (skewY(45deg)). The two children (.square) cancel the parent’s distortion by using the negative value of the parent’s skew angle (skewY(-45deg)), and scale up horizontally (scaleX(1.5)) to change from a square to a horizontal bar shape.
There are a lot of these combinations you can come up with. I’ve made a few more below where, instead of triggering the animation with a JavaScript interaction, I’ve used a <details> element that triggers the animation when it is in an [open] state once the <summary> element is clicked. And each <summary> contains an .icon child demonstrating a different animation when the <details> toggles between open and closed.
Click on a <details> to toggle it open and closed to see the animations in action.
CodePen Embed FallbackThat’s all I wanted to share — it’s easy to forget that we get some affordances for writing efficient animations if we consider how transforming a parent element intrinsically affects the size, position, and orientation. That way, for example, there’s no need to write complex animations for each individual child element, but rather leverage what the parent can do, then adjust the behavior at the child level, as needed.
CSS Animations That Leverage the Parent-Child Relationship originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Tackling Common UX Hurdles with AI
Some UX issues have been with us so long that we stopped thinking we could do better. Need to collect data from people? Web forms. People don't understand how your app works? Onboarding. But new technologies create new opportunities including ways to tackle long-standing UX challenges.
Today AI mostly shows up in software applications as a chat panel bolted onto the side of a user interface. While often useful, it's not the only way to improve an application's user experience with AI. We can also use what AI models are good at to address common user pain points that have been around for years.
I've written about some of these approaches but thought it would be useful to summarize a few in order to illustrate the higher level point.
Rethinking OnboardingMost apps start with empty states and onboarding flows that teach people how to use them. Show the UI. Explain the features. Walk through examples. Hope people stick around long enough to see value.
AI flips this. Instead of starting with nothing, AI can generate something for people to edit. Give people working content from day one. Let them refine, not create from scratch. The difference is immediate engagement versus delayed gratification. People can start using your product right away because there's already something there to work with. They learn by seeing what's possible, by modifying, by doing.
More in Let the AI do the Onboarding...
Rethinking SearchSearch interfaces traditionally meant keyword boxes, dropdown menus, faceted filters. Want to find something specific? Learn our taxonomy. Understand our categorization scheme. Click through multiple refinement options.
AI models have World knowledge baked in. They understand context. They can translate a natural question into a multi-step query without making people do the work. "Show me action movies from the 90s with high ratings" doesn't need separate dropdowns for genre, decade, and rating threshold. The AI figures out the query structure. It combines the filters. It returns results.
People search in many different ways. AI handles that variety better than rigid UI widgets ever could.
More in World Knowledge Improves AI Apps...
Rethinking FormsWeb forms exist to structure information for databases. Field labels. Input types. Validation rules. Forms force people to fit their information into our predetermined boxes.
But AI works with unstructured input. People can just drop in an image, a PDF file, or a URL. The AI extracts the structured data. It populates the database fields. The machine does the formatting work instead of the human. This shifts the burden from users to systems. People communicate naturally. Software handles the structure.
More in Unstructured Input in AI Apps Instead of Web Forms...
These examples share a common thread: AI capabilities let us reconsider how people interact with software. Not by adding AI features to existing patterns, but by rethinking the patterns themselves based on what AI makes possible. The constraints that shaped our current UX conventions are changing so it's time to start revisiting our solutions.
An Introduction to JavaScript Expressions
Editor’s note: Mat Marquis and Andy Bell have released JavaScript for Everyone, an online course offered exclusively at Piccalilli. This post is an excerpt from the course taken specifically from a chapter all about JavaScript expressions. We’re publishing it here because we believe in this material and want to encourage folks like yourself to sign up for the course. So, please enjoy this break from our regular broadcasting to get a small taste of what you can expect from enrolling in the full JavaScript for Everyone course.
Hey, I’m Mat, but “Wilto” works too — I’m here to teach you JavaScript.
Well, not here-here; technically, I’m over at JavaScript for Everyone to teach you JavaScript. What we have here is a lesson from the JavaScript for Everyone module on lexical grammar and analysis — the process of parsing the characters that make up a script file and converting it into a sequence of discrete “input elements” (lexical tokens, line ending characters, comments, and whitespace), and how the JavaScript engine interprets those input elements.
An expression is code that, when evaluated, resolves to a value. 2 + 2 is a timeless example.
2 + 2 // result: 4As mental models go, you could do worse than “anywhere in a script that a value is expected you can use an expression, no matter how simple or complex that expression may be:”
function numberChecker( checkedNumber ) { if( typeof checkedNumber === "number" ) { console.log( "Yep, that's a number." ); } } numberChecker( 3 ); // result: Yep, that's a number. numberChecker( 10 + 20 ); // result: Yep, that's a number. numberChecker( Math.floor( Math.random() * 20 ) / Math.floor( Math.random() * 10 ) ); // result: Yep, that's a number.Granted, JavaScript doesn’t tend to leave much room for absolute statements. The exceptions are rare, but it isn’t the case absolutely, positively, one hundred percent of the time:
console.log( -2**1 ); // result: Uncaught SyntaxError: Unary operator used immediately before exponentiation expression. Parenthesis must be used to disambiguate operator precedenceStill, I’m willing to throw myself upon the sword of “um, actually” on this one. That way of looking at the relationship between expressions and their resulting values is heart-and-soul of the language stuff, and it’ll get you far.
Primary ExpressionsThere’s sort of a plot twist, here: while the above example reads to our human eyes as an example of a number, then an expression, then a complex expression, it turns out to be expressions all the way down. 3 is itself an expression — a primary expression. In the same way the first rule of Tautology Club is Tautology Club’s first rule, the number literal 3 is itself an expression that resolves in a very predictable value (psst, it’s three).
console.log( 3 ); // result: 3Alright, so maybe that one didn’t necessarily need the illustrative snippet of code, but the point is: the additive expression 2 + 2 is, in fact, the primary expression 2 plus the primary expression 2.
Granted, the “it is what it is” nature of a primary expression is such that you won’t have much (any?) occasion to point at your display and declare “that is a primary expression,” but it does afford a little insight into how JavaScript “thinks” about values: a variable is also a primary expression, and you can mentally substitute an expression for the value it results in — in this case, the value that variable references. That’s not the only purpose of an expression (which we’ll get into in a bit) but it’s a useful shorthand for understanding expressions at their most basic level.
There’s a specific kind of primary expression that you’ll end up using a lot: the grouping operator. You may remember it from the math classes I just barely passed in high school:
console.log( 2 + 2 * 3 ); // result: 8 console.log( ( 2 + 2 ) * 3 ); // result: 12The grouping operator (singular, I know, it kills me too) is a matched pair of parentheses used to evaluate a portion of an expression as a single unit. You can use it to override the mathematical order of operations, as seen above, but that’s not likely to be your most common use case—more often than not you’ll use grouping operators to more finely control conditional logic and improve readability:
const minValue = 0; const maxValue = 100; const theValue = 50; if( ( theValue > minValue ) && ( theValue < maxValue ) ) { // If ( the value of `theValue` is greater than that of `minValue` ) AND less than `maxValue`): console.log( "Within range." ); } // result: Within range.Personally, I make a point of almost never excusing my dear Aunt Sally. Even when I’m working with math specifically, I frequently use parentheses just for the sake of being able to scan things quickly:
console.log( 2 + ( 2 * 3 ) ); // result: 8This use is relatively rare, but the grouping operator can also be used to remove ambiguity in situations where you might need to specify that a given syntax is intended to be interpreted as an expression. One of them is, well, right there in your developer console.
The syntax used to initialize an object — a matched pair of curly braces — is the same as the syntax used to group statements into a block statement. Within the global scope, a pair of curly braces will be interpreted as a block statement containing a syntax that makes no sense given that context, not an object literal. That’s why punching an object literal into your developer console will result in an error:
{ "theValue" : true } // result: `Uncaught SyntaxError: unexpected token: ':'It’s very unlikely you’ll ever run into this specific issue in your day-to-day JavaScript work, seeing as there’s usually a clear division between contexts where an expression or a statement are expected:
{ const theObject = { "theValue" : true }; }You won’t often be creating an object literal without intending to do something with it, which means it will always be in the context where an expression is expected. It is the reason you’ll see standalone object literals wrapped in a a grouping operator throughout this course — a syntax that explicitly says “expect an expression here”:
({ "value" : true });However, that’s not to say you’ll never need a grouping operator for disambiguation purposes. Again, not to get ahead of ourselves, but an Independently-Invoked Function Expression (IIFE), an anonymous function expression used to manage scope, relies on a grouping operator to ensure the function keyword is treated as a function expression rather than a declaration:
(function(){ // ... })(); Expressions With Side EffectsExpressions always give us back a value, in no uncertain terms. There are also expressions with side effects — expressions that result in a value and do something. For example, assigning a value to an identifier is an assignment expression. If you paste this snippet into your developer console, you’ll notice it prints 3:
theIdentifier = 3; // result: 3The resulting value of the expression theIdentifier = 3 is the primary expression 3; classic expression stuff. That’s not what’s useful about this expression, though — the useful part is that this expression makes JavaScript aware of theIdentifier and its value (in a way we probably shouldn’t, but that’s a topic for another lesson). That variable binding is an expression and it results in a value, but that’s not really why we’re using it.
Likewise, a function call is an expression; it gets evaluated and results in a value:
function theFunction() { return 3; }; console.log( theFunction() + theFunction() ); // result: 6We’ll get into it more once we’re in the weeds on functions themselves, but the result of calling a function that returns an expression is — you guessed it — functionally identical to working with the value that results from that expression. So far as JavaScript is concerned, a call to theFunction effectively is the simple expression 3, with the side effect of executing any code contained within the function body:
function theFunction() { console.log( "Called." ); return 3; }; console.log( theFunction() + theFunction() ); /* Result: Called. Called. 6 */Here theFunction is evaluated twice, each time calling console.log then resulting in the simple expression 3 . Those resulting values are added together, and the result of that arithmetic expression is logged as 6.
Granted, a function call may not always result in an explicit value. I haven’t been including them in our interactive snippets here, but that’s the reason you’ll see two things in the output when you call console.log in your developer console: the logged string and undefined.
JavaScript’s built-in console.log method doesn’t return a value. When the function is called it performs its work — the logging itself. Then, because it doesn’t have a meaningful value to return, it results in undefined. There’s nothing to do with that value, but your developer console informs you of the result of that evaluation before discarding it.
Comma OperatorSpeaking of throwing results away, this brings us to a uniquely weird syntax: the comma operator. A comma operator evaluates its left operand, discards the resulting value, then evaluates and results in the value of the right operand.
Based only on what you’ve learned so far in this lesson, if your first reaction is “I don’t know why I’d want an expression to do that,” odds are you’re reading it right. Let’s look at it in the context of an arithmetic expression:
console.log( ( 1, 5 + 20 ) ); // result: 25The primary expression 1 is evaluated and the resulting value is discarded, then the additive expression 5 + 20 is evaluated, and that’s resulting value. Five plus twenty, with a few extra characters thrown in for style points and a 1 cast into the void, perhaps intended to serve as a threat to the other numbers.
And hey, notice the extra pair of parentheses there? Another example of a grouping operator used for disambiguation purposes. Without it, that comma would be interpreted as separating arguments to the console.log method — 1 and 5 + 20 — both of which would be logged to the console:
console.log( 1, 5 + 20 ); // result: 1 25Now, including a value in an expression in a way where it could never be used for anything would be a pretty wild choice, granted. That’s why I bring up the comma operator in the context of expressions with side effects: both sides of the , operator are evaluated, even if the immediately resulting value is discarded.
Take a look at this validateResult function, which does something fairly common, mechanically speaking; depending on the value passed to it as an argument, it executes one of two functions, and ultimately returns one of two values.
For the sake of simplicity, we’re just checking to see if the value being evaluated is strictly true — if so, call the whenValid function and return the string value "Nice!". If not, call the whenInvalid function and return the string "Sorry, no good.":
function validateResult( theValue ) { function whenValid() { console.log( "Valid result." ); }; function whenInvalid() { console.warn( "Invalid result." ); }; if( theValue === true ) { whenValid(); return "Nice!"; } else { whenInvalid(); return "Sorry, no good."; } }; const resultMessage = validateResult( true ); // result: Valid result. console.log( resultMessage ); // result: "Nice!"Nothing wrong with this. The whenValid / whenInvalid functions are called when the validateResult function is called, and the resultMessage constant is initialized with the returned string value. We’re touching on a lot of future lessons here already, so don’t sweat the details too much.
Some room for optimizations, of course — there almost always is. I’m not a fan of having multiple instances of return, which in a sufficiently large and potentially-tangled codebase can lead to increased “wait, where is that coming from” frustrations. Let’s sort that out first:
function validateResult( theValue ) { function whenValid() { console.log( "Valid result." ); }; function whenInvalid() { console.warn( "Invalid result." ); }; if( theValue === true ) { whenValid(); } else { whenInvalid(); } return theValue === true ? "Nice!" : "Sorry, no good."; }; const resultMessage = validateResult( true ); // result: Valid result. resultMessage; // result: "Nice!"That’s a little better, but we’re still repeating ourselves with two separate checks for theValue. If our conditional logic were to be changed someday, it wouldn’t be ideal that we have to do it in two places.
The first — the if/else — exists only to call one function or the other. We now know function calls to be expressions, and what we want from those expressions are their side effects, not their resulting values (which, absent a explicit return value, would just be undefined anyway).
Because we need them evaluated and don’t care if their resulting values are discarded, we can use comma operators (and grouping operators) to sit them alongside the two simple expressions — the strings that make up the result messaging — that we do want values from:
function validateResult( theValue ) { function whenValid() { console.log( "Valid result." ); }; function whenInvalid() { console.warn( "Invalid result." ); }; return theValue === true ? ( whenValid(), "Nice!" ) : ( whenInvalid(), "Sorry, no good." ); }; const resultMessage = validateResult( true ); // result: Valid result. resultMessage; // result: "Nice!"Lean and mean thanks to clever use of comma operators. Granted, there’s a case to be made that this is a little too clever, in that it could make this code a little more difficult to understand at a glance for anyone that might have to maintain this code after you (or, if you have a memory like mine, for your near-future self). The siren song of “I could do it with less characters” has driven more than one JavaScript developer toward the rocks of, uh, slightly more difficult maintainability. I’m in no position to talk, though. I chewed through my ropes years ago.
Between this lesson on expressions and the lesson on statements that follows it, well, that would be the whole ballgame — the entirety of JavaScript summed up, in a manner of speaking — were it not for a not-so-secret third thing. Did you know that most declarations are neither statement nor expression, despite seeming very much like statements?
Variable declarations performed with let or const, function declarations, class declarations — none of these are statements:
if( true ) let theVariable; // Result: Uncaught SyntaxError: lexical declarations can't appear in single-statement contextif is a statement that expects a statement, but what it encounters here is one of the non-statement declarations, resulting in a syntax error. Granted, you might never run into this specific example at all if you — like me — are the sort to always follow an if with a block statement, even if you’re only expecting a single statement.
I did say “one of the non-statement declarations,” though. There is, in fact, a single exception to this rule — a variable declaration using var is a statement:
if( true ) var theVariable;That’s just a hint at the kind of weirdness you’ll find buried deep in the JavaScript machinery. 5 is an expression, sure. 0.1 * 0.1 is 0.010000000000000002, yes, absolutely. Numeric values used to access elements in an array are implicitly coerced to strings? Well, sure — they’re objects, and their indexes are their keys, and keys are strings (or Symbols). What happens if you use call() to give this a string literal value? There’s only one way to find out — two ways to find out, if you factor in strict mode.
That’s where JavaScript for Everyone is designed take you: inside JavaScript’s head. My goal is to teach you the deep magic — the how and the why of JavaScript. If you’re new to the language, you’ll walk away from this course with a foundational understanding of the language worth hundreds of hours of trial-and-error. If you’re a junior JavaScript developer, you’ll finish this course with a depth of knowledge to rival any senior.
I hope to see you there.
JavaScript for Everyone is now available and the launch price runs until midnight, October 28. Save £60 off the full price of £249 (~$289) and get it for £189 (~$220)!
Get the CourseAn Introduction to JavaScript Expressions originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
New business wanted
Last week Krijn and I decided to cancel performance.now() 2021. Although it was the right decision it leaves me in financially fairly dire straits. So I’m looking for new jobs and/or donations.
Even though the Corona trends in NL look good, and we could probably have brought 350 people together in November, we cannot be certain: there might be a new flare-up. More serious is the fact that it’s very hard to figure out how to apply the Corona checks Dutch government requires, especially for non-EU citizens. We couldn’t figure out how UK and US people should be tested, and for us that was the straw that broke the camel’s back. Cancelling the conference relieved us of a lot of stress.
Still, it also relieved me of a lot of money. This is the fourth conference in a row we cannot run, and I have burned through all my reserves. That’s why I thought I’d ask for help.
So ...
Has QuirksMode.org ever saved you a lot of time on a project? Did it advance your career? If so, now would be a great time to make a donation to show your appreciation.
I am trying my hand at CSS coaching. Though I had only few clients so far I found that I like it and would like to do it more. As an added bonus, because I’m still writing my CSS for JavaScripters book I currently have most of the CSS layout modules in my head and can explain them straight away — even stacking contexts.
Or if there’s any job you know of that requires a technical documentation writer with a solid knowledge of web technologies and the browser market, drop me a line. I’m interested.
Anyway, thanks for listening.
position: sticky, draft 1
I’m writing the position: sticky part of my book, and since I never worked with sticky before I’m not totally sure if what I’m saying is correct.
This is made worse by the fact that there are no very clear tutorials on sticky. That’s partly because it works pretty intuitively in most cases, and partly because the details can be complicated.
So here’s my draft 1 of position: sticky. There will be something wrong with it; please correct me where needed.
The inset properties are top, right, bottom and left. (I already introduced this terminology earlier in the chapter.)
h3,h4,pre {clear: left} section.scroll-container { border: 1px solid black; width: 300px; height: 250px; padding: 1em; overflow: auto; --text: 'scroll box'; float: left; clear: left; margin-right: 0.5em; margin-bottom: 1em; position: relative; font-size: 1.3rem; } .container,.outer-container { border: 1px solid black; padding: 1em; position: relative; --text: 'container'; } .outer-container { --text: 'outer container'; } :is(.scroll-container,.container,.outer-container):before { position: absolute; content: var(--text); top: 0.2em; left: 0.2em; font-size: 0.8rem; } section.scroll-container h2 { position: sticky; top: 0; background: white; margin: 0 !important; color: inherit !important; padding: 0.5em !important; border: 1px solid; font-size: 1.4rem !important; } .nowrap p { white-space: nowrap; } Introductionposition: sticky is a mix of relative and fixed. A sticky box takes its normal position in the flow, as if it had position: relative, but if that position scrolls out of view the sticky box remains in a position defined by its inset properties, as if it has position: fixed. A sticky box never escapes its container, though. If the container start or end scrolls past the sticky box abandons its fixed position and sticks to the top or the bottom of its container.
It is typically used to make sure that headers remain in view no matter how the user scrolls. It is also useful for tables on narrow screens: you can keep headers or the leftmost table cells in view while the user scrolls.
Scroll box and containerA sticky box needs a scroll box: a box that is able to scroll. By default this is the browser window — or, more correctly, the layout viewport — but you can define another scroll box by setting overflow on the desired element. The sticky box takes the first ancestor that could scroll as its scroll box and calculates all its coordinates relative to it.
A sticky box needs at least one inset property. These properties contain vital instructions, and if the sticky box doesn’t receive them it doesn’t know what to do.
A sticky box may also have a container: a regular HTML element that contains the sticky box. The sticky box will never be positioned outside this container, which thus serves as a constraint.
The first example shows this set-up. The sticky <h2> is in a perfectly normal <div>, its container, and that container is in a <section> that is the scroll box because it has overflow: auto. The sticky box has an inset property to provide instructions. The relevant styles are:
section.scroll-container { border: 1px solid black; width: 300px; height: 300px; overflow: auto; padding: 1em; } div.container { border: 1px solid black; padding: 1em; } section.scroll-container h2 { position: sticky; top: 0; } The rules Sticky headerRegular content
Regular content
Regular content
Regular content
Regular content
Regular content
Regular content
Content outside container
Content outside container
Content outside container
Content outside container
Content outside container
Content outside container
Now let’s see exactly what’s going on.
A sticky box never escapes its containing box. If it cannot obey the rules that follow without escaping from its container, it instead remains at the edge. Scroll down until the container disappears to see this in action.
A sticky box starts in its natural position in the flow, as if it has position: relative. It thus participates in the default flow: if it becomes higher it pushes the paragraphs below it downwards, just like any other regular HTML element. Also, the space it takes in the normal flow is kept open, even if it is currently in fixed position. Scroll down a little bit to see this in action: an empty space is kept open for the header.
A sticky box compares two positions: its natural position in the flow and its fixed position according to its inset properties. It does so in the coordinate frame of its scroll box. That is, any given coordinate such as top: 20px, as well as its default coordinates, is resolved against the content box of the scroll box. (In other words, the scroll box’s padding also constrains the sticky box; it will never move up into that padding.)
A sticky box with top takes the higher value of its top and its natural position in the flow, and positions its top border at that value. Scroll down slowly to see this in action: the sticky box starts at its natural position (let’s call it 20px), which is higher than its defined top (0). Thus it rests at its position in the natural flow. Scrolling up a few pixels doesn’t change this, but once its natural position becomes less than 0, the sticky box switches to a fixed layout and stays at that position.
The sticky box has bottom: 0
Regular content
Regular content
Regular content
Regular content
Regular content
Regular content
Sticky headerContent outside container
Content outside container
Content outside container
Content outside container
Content outside container
Content outside container
It does the same for bottom, but remember that a bottom is calculated relative to the scroll box’s bottom, and not its top. Thus, a larger bottom coordinate means the box is positioned more to the top. Now the sticky box compares its default bottom with the defined bottom and uses the higher value to position its bottom border, just as before.
With left, it uses the higher value of its natural position and to position its left border; with right, it does the same for its right border, bearing in mind once more that a higher right value positions the box more to the left.
If any of these steps would position the sticky box outside its containing box it takes the position that just barely keeps it within its containing box.
Details Sticky headerVery, very long line of content to stretch up the container quite a bit
Regular content
Regular content
Regular content
Regular content
Regular content
Regular content
Content outside container
Content outside container
Content outside container
Content outside container
Content outside container
Content outside container
Content outside container
The four inset properties act independently of one another. For instance the following box will calculate the position of its top and left edge independently. They can be relative or fixed, depending on how the user scrolls.
p.testbox { position: sticky; top: 0; left: 0; }Content outside container
Content outside container
Content outside container
Content outside container
Content outside container
The sticky box has top: 0; bottom: 0
Regular content
Regular content
Regular content
Regular content
Sticky headerRegular content
Regular content
Regular content
Regular content
Regular content
Content outside container
Content outside container
Content outside container
Content outside container
Content outside container
Setting both a top and a bottom, or both a left and a right, gives the sticky box a bandwidth to move in. It will always attempt to obey all the rules described above. So the following box will vary between 0 from the top of the screen to 0 from the bottom, taking its default position in the flow between these two positions.
p.testbox { position: sticky; top: 0; bottom: 0; } No containerRegular content
Regular content
Sticky headerRegular content
Regular content
Regular content
Regular content
Regular content
Regular content
Regular content
Regular content
Regular content
So far we put the sticky box in a container separate from the scroll box. But that’s not necessary. You can also make the scroll box itself the container if you wish. The sticky element is still positioned with respect to the scroll box (which is now also its container) and everything works fine.
Several containers Sticky headerRegular content
Regular content
Regular content
Regular content
Regular content
Regular content
Regular content
Content outside container
Content outside container
Content outside outer container
Content outside outer container
Or the sticky item can be several containers removed from its scroll box. That’s fine as well; the positions are still calculated relative to the scroll box, and the sticky box will never leave its innermost container.
Changing the scroll box Sticky headerThe container has overflow: auto.
Regular content
Regular content
Regular content
Regular content
Regular content
Regular content
Content outside container
Content outside container
Content outside container
One feature that catches many people (including me) unaware is giving the container an overflow: auto or hidden. All of a sudden it seems the sticky header doesn’t work any more.
What’s going on here? An overflow value of auto, hidden, or scroll makes an element into a scroll box. So now the sticky box’s scroll box is no longer the outer element, but the inner one, since that is now the closest ancestor that is able to scroll.
The sticky box appears to be static, but it isn’t. The crux here is that the scroll box could scroll, thanks to its overflow value, but doesn’t actually do so because we didn’t give it a height, and therefore it stretches up to accomodate all of its contents.
Thus we have a non-scrolling scroll box, and that is the root cause of our problems.
As before, the sticky box calculates its position by comparing its natural position relative to its scroll box with the one given by its inset properties. Point is: the sticky box doesn’t scroll relative to its scroll box, so its position always remains the same. Where in earlier examples the position of the sticky element relative to the scroll box changed when we scrolled, it no longer does so, because the scroll box doesn’t scroll. Thus there is no reason for it to switch to fixed positioning, and it stays where it is relative to its scroll box.
The fact that the scroll box itself scrolls upward is irrelevant; this doesn’t influence the sticky box in the slightest.
Sticky headerRegular content
Regular content
Regular content
Regular content
Regular content
Regular content
Regular content
Content outside container
Content outside container
Content outside container
Content outside container
Content outside container
Content outside container
One solution is to give the new scroll box a height that is too little for its contents. Now the scroll box generates a scrollbar and becomes a scrolling scroll box. When we scroll it the position of the sticky box relative to its scroll box changes once more, and it switches from fixed to relative or vice versa as required.
Minor itemsFinally a few minor items:
- It is no longer necessary to use position: -webkit-sticky. All modern browsers support regular position: sticky. (But if you need to cater to a few older browsers, retaining the double syntax doesn’t hurt.)
- Chrome (Mac) does weird things to the borders of the sticky items in these examples. I don’t know what’s going on and am not going to investigate.
Breaking the web forward
Safari is holding back the web. It is the new IE, after all. In contrast, Chrome is pushing the web forward so hard that it’s starting to break. Meanwhile web developers do nothing except moan and complain. The only thing left to do is to pick our poison.
blockquote { font-size: inherit; font-family: inherit; } blockquote p { font-size: inherit; font-family: inherit; } Safari is the new IERecently there was yet another round of “Safari is the new IE” stories. Once Jeremy’s summary and a short discussion cleared my mind I finally figured out that Safari is not IE, and that Safari’s IE-or-not-IE is not the worst problem the web is facing.
Perry Sun argues that for developers, Safari is crap and outdated, emulating the old IE of fifteen years ago in this respect. He also repeats the theory that Apple is deliberately starving Safari of features in order to protect the app store, and thus its bottom line. We’ll get back to that.
The allegation that Safari is holding back web development by its lack of support for key features is not new, but it’s not true, either. Back fifteen years ago IE held back the web because web developers had to cater to its outdated technology stack. “Best viewed with IE” and all that. But do you ever see a “Best viewed with Safari” notice? No, you don’t. Another browser takes that special place in web developers’ hearts and minds.
Chrome is the new IE, but in reverseJorge Arango fears we’re going back to the bad old days with “Best viewed in Chrome.” Chris Krycho reinforces this by pointing out that, even though Chrome is not the standard, it’s treated as such by many web developers.
“Best viewed in Chrome” squares very badly with “Safari is the new IE.” Safari’s sad state does not force web developers to restrict themselves to Safari-supported features, so it does not hold the same position as IE.
So I propose to lay this tired old meme to rest. Safari is not the new IE. If anything it’s the new Netscape 4.
Meanwhile it is Chrome that is the new IE, but in reverse.
Break the web forwardBack in the day, IE was accused of an embrace, extend, and extinguish strategy. After IE6 Microsoft did nothing for ages, assuming it had won the web. Thanks to web developers taking action in their own name for the first (and only) time, IE was updated once more and the web moved forward again.
Google learned from Microsoft’s mistakes and follows a novel embrace, extend, and extinguish strategy by breaking the web and stomping on the bits. Who cares if it breaks as long as we go forward. And to hell with backward compatibility.
Back in 2015 I proposed to stop pushing the web forward, and as expected the Chrome devrels were especially outraged at this idea. It never went anywhere. (Truth to tell: I hadn’t expected it to.)
I still think we should stop pushing the web forward for a while until we figure out where we want to push the web forward to — but as long as Google is in charge that won’t happen. It will only get worse.
On alertA blog storm broke out over the decision to remove alert(), confirm() and prompt(), first only the cross-origin variants, but eventually all of them. Jeremy and Chris Coyier already summarised the situation, while Rich Harris discusses the uses of the three ancient modals, especially when it comes to learning JavaScript.
With all these articles already written I will only note that, if the three ancient modals are truly as horrendous a security issue as Google says they are it took everyone a bloody long time to figure that out. I mean, they turn 25 this year.
Although it appears Firefox and Safari are on board with at least the cross-origin part of the proposal, there is no doubt that it’s Google that leads the charge.
From Google’s perspective the ancient modals have one crucial flaw quite apart from their security model: they weren’t invented there. That’s why they have to be replaced by — I don’t know what, but it will likely be a very complicated API.
Complex systems and arrogant priests rule the webThus the new embrace, extend, and extinguish is breaking backward compatibility in order to make the web more complicated. Nolan Lawson puts it like this:
we end up with convoluted specs like Service Worker that you need a PhD to understand, and yet we still don't have a working <dialog> element.
In addition, Google can be pretty arrogant and condescending, as Chris Ferdinandi points out.
The condescending “did you actually read it, it’s so clear” refrain is patronizing AF. It’s the equivalent of “just” or “simply” in developer documentation.
I read it. I didn’t understand it. That’s why I asked someone whose literal job is communicating with developers about changes Chrome makes to the platform.
This is not isolated to one developer at Chrome. The entire message thread where this change was surfaced is filled with folks begging Chrome not to move forward with this proposal because it will break all-the-things.
If you write documentation or a technical article and nobody understands it, you’ve done a crappy job. I should know; I’ve been writing this stuff for twenty years.
Extend, embrace, extinguish. And use lots of difficult words.
Patience is a virtueAs a reaction to web dev outcry Google temporarily halted the breaking of the web. That sounds great but really isn’t. It’s just a clever tactical move.
I saw this tactic in action before. Back in early 2016 Google tried to break the de-facto standard for the mobile visual viewport that I worked very hard to establish. I wrote a piece that resonated with web developers, whose complaints made Google abandon the plan — temporarily. They tried again in late 2017, and I again wrote an article, but this time around nobody cared and the changes took effect and backward compatibility was broken.
So the three ancient modals still have about 12 to 18 months to live. Somewhere in late 2022 to early 2023 Google will try again, web developers will be silent, and the modals will be gone.
The pursuit of appinessBut why is Google breaking the web forward at such a pace? And why is Apple holding it back?
Safari is kept dumb to protect the app store and thus revenue. In contrast, the Chrome team is pushing very hard to port every single app functionality to the browser. Ages ago I argued we should give up on this, but of course no one listened.
When performing Valley Kremlinology, it is useful to see Google policies as stemming from a conflict between internal pro-web and anti-web factions. We web developers mainly deal with the pro-web faction, the Chrome devrel and browser teams. On the other hand, the Android team is squarely in the anti-web camp.
When seen in this light the pro-web camp’s insistence on copying everything appy makes excellent sense: if they didn’t Chrome would lag behind apps and the Android anti-web camp would gain too much power. While I prefer the pro-web over the anti-web camp, I would even more prefer the web not to be a pawn in an internal Google power struggle. But it has come to that, no doubt about it.
Solutions?Is there any good solution? Not really.
Jim Nielsen feels that part of the issue is the lack of representation of web developers in the standardization process. That sounds great but is proven not to work.
Three years ago Fronteers and I attempted to get web developers represented and were met with absolute disinterest. Nobody else cared even one shit, and the initiative sank like a stone.
So a hypothetical web dev representative in W3C is not going to work. Also, the organisational work would involve a lot of unpaid labour, and I, for one, am not willing to do it again. Neither is anyone else. So this is not the solution.
And what about Firefox? Well, what about it? Ten years ago it made a disastrous mistake by ignoring the mobile web for way too long, then it attempted an arrogant and uninformed come-back with Firefox OS that failed, and its history from that point on is one long slide into obscurity. That’s what you get with shitty management.
Pick your poisonSo Safari is trying to slow the web down. With Google’s move-fast-break-absofuckinglutely-everything axiom in mind, is Safari’s approach so bad?
Regardless of where you feel the web should be on this spectrum between Google and Apple, there is a fundamental difference between the two.
We have the tools and procedures to manage Safari’s disinterest. They’re essentially the same as the ones we deployed against Microsoft back in the day — though a fundamental difference is that Microsoft was willing to talk while Apple remains its old haughty self, and its “devrels” aren’t actually allowed to do devrelly things such as managing relations with web developers. (Don’t blame them, by the way. If something would ever change they’re going to be our most valuable internal allies — just as the IE team was back in the day.)
On the other hand, we have no process for countering Google’s reverse embrace, extend, and extinguish strategy, since a section of web devs will be enthusiastic about whatever the newest API is. Also, Google devrels talk. And talk. And talk. And provide gigs of data that are hard to make sense of. And refer to their proprietary algorithms that “clearly” show X is in the best interest of the web — and don’t ask questions! And make everything so fucking complicated that we eventually give up and give in.
So pick your poison. Shall we push the web forward until it’s broken, or shall we break it by inaction? What will it be? Privately, my money is on Google. So we should say goodbye to the old web while we still can.
Custom properties and @property
You’re reading a failed article. I hoped to write about @property and how it is useful for extending CSS inheritance considerably in many different circumstances. Alas, I failed. @property turns out to be very useful for font sizes, but does not even approach the general applicability I hoped for.
Grandparent-inheritingIt all started when I commented on what I thought was an interesting but theoretical idea by Lea Verou: what if elements could inherit the font size of not their parent, but their grandparent? Something like this:
div.grandparent { /* font-size could be anything */ } div.parent { font-size: 0.4em; } div.child { font-size: [inherit from grandparent in some sort of way]; font-size: [yes, you could do 2.5em to restore the grandparent's font size]; font-size: [but that's not inheriting, it's just reversing a calculation]; font-size: [and it will not work if the parent's font size is also unknown]; }Lea told me this wasn’t a vague idea, but something that can be done right now. I was quite surprised — and I assume many of my readers are as well — and asked for more information. So she wrote Inherit ancestor font-size, for fun and profit, where she explained how the new Houdini @property can be used to do this.
This was seriously cool. Also, I picked up a few interesting bits about how CSS custom properties and Houdini @property work. I decided to explain these tricky bits in simple terms — mostly because I know that by writing an explanation I myself will understand them better — and to suggest other possibilities for using Lea’s idea.
Alas, that last objective is where I failed. Lea’s idea can only be used for font sizes. That’s an important use case, but I had hoped for more. The reasons why it doesn’t work elsewhere are instructive, though.
Tokens and valuesLet’s consider CSS custom properties. What if we store the grandparent’s font size in a custom property and use that in the child?
div.grandparent { /* font-size could be anything */ --myFontSize: 1em; } div.parent { font-size: 0.4em; } div.child { font-size: var(--myFontSize); /* hey, that's the grandparent's font size, isn't it? */ }This does not work. The child will have the same font size as the parent, and ignore the grandparent. In order to understand why we need to understand how custom properties work. What does this line of CSS do?
--myFontSize: 1em;It sets a custom property that we can use later. Well duh.
Sure. But what value does this custom property have?
... errr ... 1em?
Nope. The answer is: none. That’s why the code example doesn’t work.
When they are defined, custom properties do not have a value or a type. All that you ordered the browsers to do is to store a token in the variable --myFontSize.
This took me a while to wrap my head around, so let’s go a bit deeper. What is a token? Let’s briefly switch to JavaScript to explain.
let myVar = 10;What’s the value of myVar in this line? I do not mean: what value is stored in the variable myVar, but: what value does the character sequence myVar have in that line of code? And what type?
Well, none. Duh. It’s not a variable or value, it’s just a token that the JavaScript engine interprets as “allow me to access and change a specific variable” whenever you type it.
CSS custom properties also hold such tokens. They do not have any intrinsic meaning. Instead, they acquire meaning when they are interpreted by the CSS engine in a certain context, just as the myVar token is in the JavaScript example.
So the CSS custom property contains the token 1em without any value, without any type, without any meaning — as yet.
You can use pretty any bunch of characters in a custom property definition. Browsers make no assumptions about their validity or usefulness because they don’t yet know what you want to do with the token. So this, too, is a perfectly fine CSS custom property:
--myEgoTrip: ppk;Browsers shrug, create the custom property, and store the indicated token. The fact that ppk is invalid in all CSS contexts is irrelevant: we haven’t tried to use it yet.
It’s when you actually use the custom property that values and types are assigned. So let’s use it:
background-color: var(--myEgoTrip);Now the CSS parser takes the tokens we defined earlier and replaces the custom property with them:
background-color: ppk;And only NOW the tokens are read and intrepreted. In this case that results in an error: ppk is not a valid value for background-color. So the CSS declaration as a whole is invalid and nothing happens — well, technically it gets the unset value, but the net result is the same. The custom property itself is still perfectly valid, though.
The same happens in our original code example:
div.grandparent { /* font-size could be anything */ --myFontSize: 1em; /* just a token; no value, no meaning */ } div.parent { font-size: 0.4em; } div.child { font-size: var(--myFontSize); /* becomes */ font-size: 1em; /* hey, this is valid CSS! */ /* Right, you obviously want the font size to be the same as the parent's */ /* Sure thing, here you go */ }In div.child he tokens are read and interpreted by the CSS parser. This results in a declaration font-size: 1em;. This is perfectly valid CSS, and the browsers duly note that the font size of this element should be 1em.
font-size: 1em is relative. To what? Well, to the parent’s font size, of course. Duh. That’s how CSS font-size works.
So now the font size of the child becomes the same as its parent’s, and browsers will proudly display the child element’s text in the same font size as the parent element’s while ignoring the grandparent.
This is not what we wanted to achieve, though. We want the grandparent’s font size. Custom properties — by themselves — don’t do what we want. We have to find another solution.
@propertyLea’s article explains that other solution. We have to use the Houdini @property rule.
@property --myFontSize { syntax: "<length>"; initial-value: 0; inherits: true; } div { border: 1px solid; padding: 1em; } div.grandparent { /* font-size could be anything */ --myFontSize: 1em; } div.parent { font-size: 0.4em; } div.child { font-size: var(--myFontSize); }Now it works. Wut? Yep — though only in Chrome so far.
@property --myFontSize { syntax: ""; initial-value: 0; inherits: true; } section.example { max-width: 500px; } section.example div { border: 1px solid; padding: 1em; } div.grandparent { font-size: 23px; --myFontSize: 1em; } div.parent { font-size: 0.4em; } div.child { font-size: var(--myFontSize); } This is the grandparent This is the parent This is the childWhat black magic is this?
Adding the @property rule changes the custom property --myFontSize from a bunch of tokens without meaning to an actual value. Moreover, this value is calculated in the context it is defined in — the grandfather — so that the 1em value now means 100% of the font size of the grandfather. When we use it in the child it still has this value, and therefore the child gets the same font size as the grandfather, which is exactly what we want to achieve.
(The variable uses a value from the context it’s defined in, and not the context it’s executed in. If, like me, you have a grounding in basic JavaScript you may hear “closures!” in the back of your mind. While they are not the same, and you shouldn’t take this apparent equivalency too far, this notion still helped me understand. Maybe it’ll help you as well.)
Unfortunately I do not quite understand what I’m doing here, though I can assure you the code snippet works in Chrome — and will likely work in the other browsers once they support @property.
Misson completed — just don’t ask me how.
SyntaxYou have to get the definition right. You need all three lines in the @property rule. See also the specification and the MDN page.
@property --myFontSize { syntax: "<length>"; initial-value: 0; inherits: true; }The syntax property tells browsers what kind of property it is and makes parsing it easier. Here is the list of possible values for syntax, and in 99% of the cases one of these values is what you need.
You could also create your own syntax, e.g. syntax: "ppk | <length>"
Now the ppk keyword and any sort of length is allowed as a value.
Note that percentages are not lengths — one of the many things I found out during the writing of this article. Still, they are so common that a special value for “length that may be a percentage or may be calculated using percentages” was created:
syntax: "<length-percentage>"Finally, one special case you need to know about is this one:
syntax: "*"MDN calls this a universal selector, but it isn’t, really. Instead, it means “I don’t know what syntax we’re going to use” and it tells browsers not to attempt to interpret the custom property. In our case that would be counterproductive: we definitely want the 1em to be interpreted. So our example doesn’t work with syntax: "*".
initial-value and inheritsAn initial-value property is required for any syntax value that is not a *. Here that’s simple: just give it an initial value of 0 — or 16px, or any absolute value. The value doesn’t really matter since we’re going to overrule it anyway. Still, a relative value such as 1em is not allowed: browsers don’t know what the 1em would be relative to and reject it as an initial value.
Finally, inherits: true specifies that the custom property value can be inherited. We definitely want the computed 1em value to be inherited by the child — that’s the entire point of this experiment. So we carefully set this flag to true.
Other use casesSo far this article merely rehashed parts of Lea’s. Since I’m not in the habit of rehashing other people’s articles my original plan was to add at least one other use case. Alas, I failed, though Lea was kind enough to explain why each of my ideas fails.
Percentage of what?Could we grandfather-inherit percentual margins and paddings? They are relative to the width of the parent of the element you define them on, and I was wondering if it might be useful to send the grandparent’s margin on to the child just like the font size. Something like this:
@property --myMargin { syntax: "<length-percentage>"; initial-value: 0; inherits: true; } div.grandparent { --myMargin: 25%; margin-left: var(--myMargin); } div.parent { font-size: 0.4em; } div.child { margin-left: var(--myMargin); /* should now be 25% of the width of the grandfather's parent */ /* but isn't */ }Alas, this does not work. Browsers cannot resolve the 25% in the context of the grandparent, as they did with the 1em, because they don’t know what to do.
The most important trick for using percentages in CSS is to always ask yourself: “percentage of WHAT?”
That’s exactly what browsers do when they encounter this @property definition. 25% of what? The parent’s font size? Or the parent’s width? (This is the correct answer, but browsers have no way of knowing that.) Or maybe the width of the element itself, for use in background-position?
Since browsers cannot figure out what the percentage is relative to they do nothing: the custom property gets the initial value of 0 and the grandfather-inheritance fails.
ColoursAnother idea I had was using this trick for the grandfather’s text colour. What if we store currentColor, which always has the value of the element’s text colour, and send it on to the grandchild? Something like this:
@property --myColor { syntax: "<color>"; initial-value: black; inherits: true; } div.grandparent { /* color unknown */ --myColor: currentColor; } div.parent { color: red; } div.child { color: var(--myColor); /* should now have the same color as the grandfather */ /* but doesn't */ }Alas, this does not work either. When the @property blocks are evaluated, and 1em is calculated, currentColor specifically is not touched because it is used as an initial (default) value for some inherited SVG and CSS properties such as fill. Unfortunately I do not fully understand what’s going on, but Tab says this behaviour is necessary, so it is.
Pity, but such is life. Especially when you’re working with new CSS functionalities.
ConclusionSo I tried to find more possbilities for using Lea’s trick, but failed. Relative units are fairly sparse, especially when you leave percentages out of the equation. em and related units such as rem are the only ones, as far as I can see.
So we’re left with a very useful trick for font sizes. You should use it when you need it (bearing in mind that right now it’s only supported in Chromium-based browsers), but extending it to other declarations is not possible at the moment.
Many thanks to Lea Verou and Tab Atkins for reviewing and correcting an earlier draft of this article.
Let’s talk about money
Let’s talk about money!
Let’s talk about how hard it is to pay small amounts online to people whose work you like and who could really use a bit of income. Let’s talk about how Coil aims to change that.
Taking a subscription to a website is moderately easy, but the person you want to pay must have enabled them. Besides, do you want to purchase a full subscription in order to read one or two articles per month?
Sending a one-time donation is pretty easy as well, but, again, the site owner must have enabled them. And even then it just gives them ad-hoc amounts that they cannot depend on.
Then there’s Patreon and Kickstarter and similar systems, but Patreon is essentially a subscription service while Kickstarter is essentially a one-time donation service, except that both keep part of the money you donate.
And then there’s ads ... Do we want small content creators to remain dependent on ads and thus support the entire ad ecosystem? I, personally, would like to get rid of them.
The problem today is that all non-ad-based systems require you to make conscious decisions to support someone — and even if you’re serious about supporting them you may forget to send in a monthly donation or to renew your subscription. It sort-of works, but the user experience can be improved rather dramatically.
That’s where Coil and the Web Monetization Standard come in.
Web MonetizationThe idea behind Coil is that you pay for what you consume easily and automatically. It’s not a subscription - you only pay for what you consume. It’s not a one-time donation, either - you always pay when you consume.
Payments occur automatically when you visit a website that is also subscribed to Coil, and the amount you pay to a single site owner depends on the time you spend on the site. Coil does not retain any of your money, either — everything goes to the people you support.
In this series of four articles we’ll take a closer look at the architecture of the current Coil implementation, how to work with it right now, the proposed standard, and what’s going to happen in the future.
OverviewSo how does Coil work right now?
Both the payer and the payee need a Coil account to send and receive money. The payee has to add a <meta> tag with a Coil payment pointer to all pages they want to monetize. The payer has to install the Coil extension in their browsers. You can see this extension as a polyfill. In the future web monetization will, I hope, be supported natively in all browsers.
Once that’s done the process works pretty much automatically. The extension searches for the <meta> tag on any site the user visits. If it finds one it starts a payment stream from payer to payee that continues for as long as the payer stays on the site.
The payee can use the JavaScript API to interact with the monetization stream. For instance, they can show extra content to paying users, or keep track of how much a user paid so far. Unfortunately these functionalities require JavaScript, and the hiding of content is fairly easy to work around. Thus it is not yet suited for serious business purposes, especially in web development circles.
This is one example of how the current system is still a bit rough around the edges. You’ll find more examples in the subsequent articles. Until the time browsers support the standard natively and you can determine your visitors’ monetization status server-side these rough bits will continue to exist. For the moment we will have to work with the system we have.
This article series will discuss all topics we touched on in more detail.
Start now!For too long we have accepted free content as our birthright, without considering the needs of the people who create it. This becomes even more curious for articles and documentation that are absolutely vital to our work as web developers.
Take a look at this list of currently-monetized web developer sites. Chances are you’ll find a few people whose work you used in the past. Don’t they deserve your direct support?
Free content is not a right, it’s an entitlement. The sooner we internalize this, and start paying independent voices, the better for the web.
The only alternative is that all articles and documentation that we depend on will written by employees of large companies. And employees, no matter how well-meaning, will reflect the priorities and point of view of their employer in the long run.
So start now.
In order to support them you should invest a bit of time once and US$5 per month permanently. I mean, that’s not too much to ask, is it?
ContinueI wrote this article and its sequels for Coil, and yes, I’m getting paid. Still, I believe in what they are doing, so I won’t just spread marketing drivel. Initially it was unclear to me exactly how Coil works. So I did some digging, and the remaining parts of this series give a detailed description of how Coil actually works in practice.
For now the other three articles will only be available on dev.to. I just published part 2, which gives a high-level overview of how Coil works right now. Part 3 will describe the meta tag and the JavaScript API, and in part 4 we’ll take a look at the future, which includes a formal W3C standard. Those parts will be published next week and the week after that.
- « first
- ‹ previous
- 1
- 2
- 3