Front End Web Development
Knowing CSS is Mastery to Frontend Development
Anselm Hannemann on the intersection between frameworks and learning the basics:
Nowadays people can write great React and TypeScript code. Most of the time a component library like MUI, Tailwind and others are used for styling. However, nearly no one is able to judge whether the CSS in the codebase is good or far from optimal. It is magically applied by our toolchain into the HTML and we struggle to understand why the website is getting slower and slower.
Related, from Alex Russell:
Many need help orienting themselves as to which end of the telescope is better for examining frontend problems. Frameworkism is now the dominant creed of frontend discourse. It insists that all user problems will be solved if teams just framework hard enough. This is non-sequitur, if not entirely backwards. In practice, the only thing that makes web experiences good is caring about the user experience — specifically, the experience of folks at the margins. Technologies come and go, but what always makes the difference is giving a toss about the user.
Knowing CSS is Mastery to Frontend Development originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Steven Heller’s Font of the Month: Gigafly
Read the book, Typographic Firsts
This month, Steven Heller takes a closer look at the Gigafly font family.
The post Steven Heller’s Font of the Month: Gigafly appeared first on I Love Typography.
The Law of Diminishing Returns
Some animation can make things feel natural. Too many animations becomes distracting.
Some line spacing can help legibility. Too much hurts it.
Some alt text is contextual. Too much alt text is noise.
Some padding feels comfy. Too much padding feels exposed.
Some specificity is manageable. Too much specificity is untenable.
Some technical debt is healthy. Too much of it becomes a burden.
Some corner rounding is classy. Too much is just a circle.
Some breakpoints are fluid. Too many of them becomes adaptive.
Some margin adds breathing room. Too much margin collapses things.
Some images add context. Too many images takes a long time to download (and impacts the environment).
Some JavaScript enhances interactions. Too much becomes a bottleneck.
A font pairing creates a typographic system. Too many pairings creates a visual distraction.
Some utility classes come in handy. Too many eliminates a separation of concerns.
Some data helps make decisions. Too much data kills the vibe.
Some AI can help write the boring parts of code. Too much puts downward pressure on code quality.
Some SEO improves search ranking. Too much mutes the human voice.
Some testing provides good coverage. Too much testing requires its own maintenance.
A few colors establish a visual hierarchy. Too many establish a cognitive dissonance.
Some planning helps productivity. Too much planning creates delays.
Striking the right balance can be tough. We don’t want cool mama bear’s porridge or hot papa’s bear porridge, but something right in the middle, like baby bear’s porridge.
The Law of Diminishing Returns originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
One of Those “Onboarding” UIs, With Anchor Positioning
Welcome to “Anchor Positioning 101” where we will be exploring this interesting new CSS feature. Our textbook for this class will be the extensive “Anchor Positioning Guide” that Juan Diego Rodriguez published here on CSS-Tricks.
I’m excited for this one. Some of you may remember when CSS-Tricks released the “Flexbox Layout Guide” or the “Grid Layout Guide” — I certainly do and still have them both bookmarked! I spend a lot of time flipping between tabs to make sure I have the right syntax in my “experimental” CodePens.
I’ve been experimenting with CSS anchor positioning like the “good old days” since Juan published his guide, so I figured it’d be fun to share some of the excitement, learn a bit, experiment, and of course: build stuff!
CSS Anchor Positioning introductionAnchor positioning lets us attach — or “anchor” — one element to one or more other elements. More than that, it allows us to define how a “target” element (that’s what we call the element we’re attaching to an anchor element) is positioned next to the anchor-positioned element, including fallback positioning in the form of a new @position-try at-rule.
The most hand-wavy way to explain the benefits of anchor positioning is to think of it as a powerful enhancement to position: absolute; as it helps absolutely-positioned elements do what you expect. Don’t worry, we’ll see how this works as we go.
Anchor positioning is currently a W3C draft spec, so you know it’s fresh. It’s marked as “limited availability” in Baseline which at the time of writing means it is limited to Chromium-based browsers (versions 125+). That said, the considerate folks over at Oddbird have a polyfill available that’ll help out other browsers until they ship support.
This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.
DesktopChromeFirefoxIEEdgeSafari125NoNo125NoMobile / TabletAndroid ChromeAndroid FirefoxAndroidiOS Safari131No131NoOddbird contributes polyfills for many new CSS features and you (yes, you!) can support their work on Github or Open Collective!
Tab Atkins-Bittner, contributing author to the W3C draft spec on anchor positioning, spoke on the topic at CSS Day 2024. The full conference talk is available on YouTube:
Here at CSS-Tricks, Juan demonstrated how to mix and match anchor positioning with view-driven animations for an awesome floating notes effect:
Front-end friend Kevin Powell recently released a video demonstrating how “CSS Popover + Anchor Positioning is Magical”.
And finally, in the tradition of “making fun games to learn CSS,” Thomas Park released Anchoreum (a “Flexbox Froggy“-type game) to learn about CSS anchor positioning. Highly recommend checking this out to get the hang of the position-area property!
The homeworkOK, now that we’re caught up on what CSS anchor positioning is and the excitement surrounding it, let’s talk about what it does. Tethering an element to another element? That has a lot of potential. Quite a few instances I can remember where I’ve had to fight with absolute positioning and z-index in order to get something positioned just right.
Let’s take a quick look at the basic syntax. First, we need two elements, an anchor-positioned element and the target element that will be tethered to it.
<!-- Anchor element --> <div id="anchor"> Anchor </div> <!-- Target element --> <div id="target"> Target </div>We set an element as an anchor-positioned element by providing it with an anchor-name. This is a unique name of our choosing, however it needs the double-dash prefix, like CSS custom properties.
#anchor { anchor-name: --anchor; }As for our target element, we’ll need to set position: absolute; on it as well as tell the element what anchor to tether to. We do that with a new CSS property, position-anchor using a value that matches the anchor-name of our anchor-positioned element.
#anchor { anchor-name: --anchor; } #target { position: absolute; position-anchor: --anchor; }May not look like it yet, but now our two elements are attached. We can set the actual positioning on the target element by providing a position-area. To position our target element, position-area creates an invisible 3×3 grid over the anchor-positioned element. Using positioning keywords, we can designate where the target element appears near the anchor-positioned element.
#target { position: absolute; position-anchor: --anchor; position-area: top center; }Now we see that our target element is anchored to the top-center of our anchor-positioned element!
CodePen Embed Fallback Anchoring pseudo-elementsWhile playing with anchor positioning, I noticed you can anchor pseudo-elements, just the same as any other element.
#anchor { anchor-name: --anchor; &::before { content: "Target"; position: absolute; position-anchor: --anchor; left: anchor(center); bottom: anchor(center); } } CodePen Embed FallbackMight be useful for adding design flourishes to elements or adding functionality as some sort of indicator.
Moving anchorsAnother quick experiment was to see if we can move anchors. And it turns out this is possible!
CodePen Embed FallbackNotice the use of anchor() functions instead of position-area to position the target element.
#target { position: absolute; position-anchor: --anchor-one; top: anchor(bottom); left: anchor(left); }CSS anchor functions are an alternate way to position target elements based on the computed values of the anchor-positioned element itself. Here we are setting the target element’s top property value to match the anchor-positioned element’s bottom value. Similarly, we can set the target’s left property value to match the anchor-positioned element’s left value.
Hovering over the container element swaps the position-anchor from --anchor-one to --anchor-two.
.container:hover { #target { position-anchor: --anchor-two; } }We are also able to set a transition as we position the target using top and left, which makes it swap smoothly between anchors.
Extra experimentalAlong with being the first to release CSS anchor-positioning, the Chrome dev team recently released new pseudo-selectors related to the <details> and <summary> elements. The ::details-content pseudo-selector allows you to style the “hidden” part of the <details> element.
With this information, I thought: “can I anchor it?” and sure enough, you can!
CodePen Embed FallbackAgain, this is definitely not ready for prime-time, but it’s always fun to experiment!
Practical examinationsLet’s take this a bit further and tackle more practical challenges using CSS anchor positioning. Please keep in mind that all these examples are Chrome-only at the time of writing!
TooltipsOne of the most straightforward use cases for CSS anchor positioning is possibly a tooltip. Makes a lot of sense: hover over an icon and a label floats nearby to explain what the icon does. I didn’t quite want to make yet another tutorial on how to make a tooltip and luckily for me, Zell Liew recently wrote an article on tooltip best practices, so we can focus purely on anchor positioning and refer to Zell’s work for the semantics.
CodePen Embed FallbackNow, let’s check out one of these tooltips:
<!-- ... -->; <li class="toolbar-item">; <button type="button" id="inbox-tool" aria-labelledby="inbox-label" class="tool"> <svg id="inbox-tool-icon"> <!-- SVG icon code ... --> </svg> </button> <div id="inbox-label" role="tooltip"> <p>Inbox</p> </div> </li> <!-- ... -->The HTML is structured in a way where the tooltip element is a sibling of our anchor-positioned <button>, notice how it has the [aria-labelledby] attribute set to match the tooltip’s [id]. The tooltip itself is a generic <div>, semantically enhanced to become a tooltip with the [role="tooltip"] attribute. We can also use [role="tooltip"] as a semantic selector to add common styles to tooltips, including the tooltip’s positioning relative to its anchor.
First, let’s turn our button into an anchored element by giving it an anchor-name. Next, we can set the target element’s position-anchor to match the anchor-name of the anchored element. By default, we can set the tooltip’s visibility to hidden, then using CSS sibling selectors, if the target element receives hover or focus-visible, we can then swap the visibility to visible.
/* Anchor-positioned Element */ #inbox-tool { anchor-name: --inbox-tool; } /* Target element */ [role="tooltip"]#inbox-label { position-anchor: --inbox-tool } /* Target positioning */ [role="tooltip"] { position: absolute; position-area: end center; /* Hidden by default */ visibility: hidden; } /* Visible when tool is hovered or receives focus */ .tool:hover + [role="tooltip"], .tool:focus-visible + [role="tooltip"] { visibility: visible; }Ta-da! Here we have a working, CSS anchor-positioned tooltip!
As users of touch devices aren’t able to hover over elements, you may want to explore toggletips instead!
Floating disclosuresDisclosures are another common component pattern that might be a perfect use case for anchor positioning. Disclosures are typically a component where interacting with a toggle will open and close a corresponding element. Think of the good ol’ <detail>/<summary> HTML element duo, for example.
Currently, if you are looking to create a disclosure-like component which floats over other portions of your user interface, you might be in for some JavaScript, absolute positioning, and z-index related troubles. Soon enough though, we’ll be able to combine CSS anchor positioning with another newer platform feature [popover] to create some incredibly straightforward (and semantically accurate) floating disclosure elements.
The Popover API provides a non-modal way to elevate elements to the top-layer, while also baking in some great functionality, such as light dismissals.
Zell also has more information on popovers, dialogs, and modality!
One of the more common patterns you might consider as a “floating disclosure”-type component is a dropdown menu. Here is the HTML we’ll work with:
<nav> <button id="anchor">Toggle</button> <ul id="target"> <li><a href="#">Link 1</a></li> <li><a href="#">Link 2</a></li> <li><a href="#">Link 3</a></li> </ul> </nav>We can set our target element, in this case the <ul>, to be our popover element by adding the [popover] attribute.
To control the popover, let’s add the attribute [popoveraction="toggle"] to enable the button as a toggle, and point the [popovertarget] attribute to the [id] of our target element.
<nav> <button id="anchor" popoveraction="toggle" popovertarget="target"> Toggle </button> <ul id="target" popover> <li><a href="#">Link 1</a></li> <li><a href="#">Link 2</a></li> <li><a href="#">Link 3</a></li> </ul> </nav>No JavaScript is necessary, and now we have a toggle-able [popover] disclosure element! The problem is that it’s still not tethered to the anchor-positioned element, let’s fix that in our CSS.
First, as this is a popover, let’s add a small bit of styling to remove the intrinsic margin popovers receive by default from browsers.
ul[popover] { margin: 0; }Let’s turn our button into an anchor-positioned element by providing it with an anchor-name:
ul[popover] { margin: 0; } #anchor { anchor-name: --toggle; }As for our target element, we can attach it to the anchor-positioned element by setting its position to absolute and the position-anchor to our anchor-positioned element’s anchor-name:
ul[popover] { margin: 0; } #anchor { anchor-name: --toggle; } #target { position: absolute; position-anchor: --toggle; }We can also adjust the target’s positioning near the anchor-positioned element with the position-area property, similar to what we did with our tooltip.
ul[popover] { margin: 0; } #anchor { anchor-name: --toggle; } #target { position: absolute; position-anchor: --toggle; position-area: bottom; width: anchor-size(width); }You may notice another CSS anchor function in here, anchor-size()! We can set the target’s width to match the width of the anchor-positioned element by using anchor-size(width).
CodePen Embed FallbackThere is one more neat thing we can apply here, fallback positioning! Let’s consider that maybe this dropdown menu might sometimes be located at the bottom of the viewport, either from scrolling or some other reason. We don’t really want it to overflow or cause any extra scrolling, but instead, swap to an alternate location that is visible to the user.
Anchor positioning makes this possible with the postion-try-fallbacks property, a way to provide an alternate location for the target element to display near an anchor-positioned element.
#target { position: absolute; position-anchor: --toggle; position-area: bottom; postion-try-fallbacks: top; width: anchor-size(width); }To keep things simple for our demo, we can add the opposite value of the value of the postion-area property: top.
CodePen Embed Fallback Shopping cart componentWe know how to make a tooltip and a disclosure element, now let’s build upon what we’ve learned so far and create a neat, interactive shopping cart component.
Let’s think about how we want to mark this up. First, we’ll need a button with a shopping cart icon:
<button id="shopping-cart-toggle"> <svg id="shopping-cart-icon" /> <!-- SVG icon code ... --> </svg> </button>We can already reuse what we learned with our tooltip styles to provide a functioning label for this toggle. Let’s add the class .tool to the button, then include a tooltip as our label.
<!-- Toggle --> <button id="shopping-cart-toggle" aria-labelledby="shopping-cart-label" class="tool"> <svg id="shopping-cart-icon" /> <!-- SVG icon code ... --> </svg> </button> <!-- Tooltip --> <div id="shopping-cart-label" role="tooltip" class="tooltip"> <p>Shopping Cart</p> </div>We’ll need to specify our <button> is an anchor-positioned element in CSS with an anchor-name, which we can also set as the tooltip’s position-anchor value to match.
button#shopping-cart-toggle { anchor-name: --shopping-cart-toggle; } [role="tooltip"]#shopping-cart-label { position-anchor: --shopping-cart-toggle; }Now we should have a nice-looking tooltip labeling our shopping cart button!
But wait, we want this thing to do more than that! Let’s turn it into a disclosure component that reveals a list of items inside the user’s cart. As we are looking to have a floating user-interface with a few actions included, we should consider a <dialog> element. However, we don’t necessarily want to be blocking background content, so we can opt for a non-modal dialog using the[popover] attribute again!
<!-- Toggle --> <button id="shopping-cart-toggle" aria-labelledby="shopping-cart-label" class="tool" popovertarget="shopping-cart" popoveraction="toggle"> <svg id="shopping-cart-icon" /> <!-- SVG icon code ... --> </svg> </button> <!-- Tooltip --> <div id="shopping-cart-label" role="tooltip" class="tooltip"> <p>Shopping Cart</p> </div> <!-- Shopping Cart --> <dialog id="shopping-cart" popover> <!-- Shopping cart template... --> <button popovertarget="shopping-cart" popoveraction="close"> Dismiss Cart </button> </dialog>To control the popover, we’ve added [popovertarget="shopping-cart"] and [popoveraction="toggle"] to our anchor-positioned element and included a second button within the <dialog> that can also be used to close the dialog with [popoveraction="close"].
To anchor the shopping cart <dialog> to the toggle, we can set position-anchor and position-area:
#shopping-cart { position-anchor: --shopping-cart; position-area: end center; }At this point, we should take a moment to realize that we have tethered two elements to the same anchor!
We won’t stop there, though. There is one more enhancement we can make to really show how helpful anchor positioning can be: Let’s add a notification badge to the element to describe how many items are inside the cart.
Let’s place the badge inside of our anchor-positioned element this time.
<!-- Toggle --> <button id="shopping-cart-toggle" aria-labelledby="shopping-cart-label" class="tool" popovertarget="shopping-cart" popoveraction="toggle"> <svg id="shopping-cart-icon" /> <!-- SVG icon code ... --> </svg> <!-- Notification Badge --> <div id="shopping-cart-badge" class="notification-badge"> 1 </div> </button> <!-- ... -->We can improve our tooltip to include verbiage about how many items are in the cart:
<!-- Tooltip --> <div id="shopping-cart-label" role="tooltip"> <p>Shopping Cart</p> <p>(1 item in cart)</p> </div>Now the accessible name of our anchor-positioned element will be read as Shopping Cart (1 item in cart), which helps provide context to assistive technologies like screen readers.
Let’s tether this notification badge to the same anchor as our tooltip and shopping cart <dialog>, we can do this by setting the position-anchor property of the badge to --shopping-cart-toggle:
#shopping-cart-badge { position: absolute; position-anchor: --shopping-cart-toggle; }Let’s look at positioning. We don’t want it below or next to the anchor, we want it overlapping, so we can use CSS anchor functions to position it based on the anchor-positioned element’s dimensions.
#shopping-cart-badge { position: absolute; position-anchor: --shopping-cart-toggle; bottom: anchor(center); left: anchor(center); }Here we are setting the bottom and left of the target element to match the anchor’s center. This places it in the upper-right corner of the SVG icon!
Folks, this means we have three elements anchored now. Isn’t that fantastic?
CodePen Embed Fallback Combining thingsTo put these anchor-positioned elements into perspective, I’ve combined all the techniques we’ve learned so far into a more familiar setting:
CodePen Embed FallbackDisclosure components, dropdown menus, tooltips (and toggletips!), as well as notification badges all made much simpler using CSS anchor positioning!
Final projectAs a final project for myself (and to bring this whole thing around full-circle), I decided to try to build a CSS anchor-positioned-based onboarding tool. I’ve previously attempted to build a tool like this at work, which I called “HandHoldJS” and it… well, it didn’t go so great. I managed to have a lot of the core functionality working using JavaScript, but it meant keeping track of quite a lot of positions and lots of weird things kept happening!
Let’s see if we can do better with CSS anchor positioning.
CodePen Embed FallbackFeel free to check out the code on CodePen! I went down quite a rabbit hole on this one, so I’ll provide a bit of a high-level overview here.
<hand-hold> is a native custom element that works entirely in the light DOM. It sort of falls into the category of an HTML web component, as it is mostly based on enabling its inner HTML. You can specify tour stops to any element on the page by adding [data-tour-stop] attributes with values in the order you want the tour to occur.
Inside the <hand-hold> element contains a <button> to start the tour, a <dialog> element to contain the tour information, <section> elements to separate content between tour stops, a fieldset[data-handhold-navigation] element which holds navigation radio buttons, as well as another <button> to end the tour.
Each <section> element corresponds to a tour stop with a matching [data-handhold-content] attribute applied. Using JavaScript, <hand-hold> dynamically updates tour stops to be anchor-positioned elements, which the <dialog> can attach itself (there is a sneaky pseudo-element attached to the anchor to highlight the tour stop element!).
Although the <dialog> element is attached via CSS anchor positioning, it also moves within the DOM to appear next to the anchor-position element in the accessibility tree. The (well-meaning) intention here is to help provide more context to those who may be navigating via assistive devices by figuring out which element the dialog is referring to. Believe me, though, this thing is far from perfect as an accessible user experience.
Also, since the <dialog> moves throughout the DOM, unfortunately, a simple CSS transition would not suffice. Another modern browser feature to the rescue yet again, as we can pass a DOM manipulation function into a View Transition, making the transitions feel smoother!
There is still quite a lot to test with this, so I would not recommend using <hand-hold> in a production setting. If for no other reason than browser support is quite limited at the moment!
This is just an experiment to see what I could cook up using CSS anchor positioning, I’m excited for the potential!
Class dismissed!After seeing what CSS anchor positioning is capable of, I have suspicions that it may change a lot of the ways we write CSS, similar to the introduction of flexbox or grid.
I’m excited to see what other user interface patterns can be accomplished with anchor positioning, and I’m even more excited to see what the community will do with it once it’s more broadly available!
One of Those “Onboarding” UIs, With Anchor Positioning originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
WordPress Multi-Multisite: A Case Study
The mission: Provide a dashboard within the WordPress admin area for browsing Google Analytics data for all your blogs.
The catch? You’ve got about 900 live blogs, spread across about 25 WordPress multisite instances. Some instances have just one blog, others have as many as 250. In other words, what you need is to compress a data set that normally takes a very long time to compile into a single user-friendly screen.
The implementation details are entirely up to you, but the final result should look like this Figma comp:
Design courtesy of the incomparable Brian Biddle.I want to walk you through my approach and some of the interesting challenges I faced coming up with it, as well as the occasional nitty-gritty detail in between. I’ll cover topics like the WordPress REST API, choosing between a JavaScript or PHP approach, rate/time limits in production web environments, security, custom database design — and even a touch of AI. But first, a little orientation.
Let’s define some termsWe’re about to cover a lot of ground, so it’s worth spending a couple of moments reviewing some key terms we’ll be using throughout this post.
What is WordPress multisite?WordPress Multisite is a feature of WordPress core — no plugins required — whereby you can run multiple blogs (or websites, or stores, or what have you) from a single WordPress installation. All the blogs share the same WordPress core files, wp-content folder, and MySQL database. However, each blog gets its own folder within wp-content/uploads for its uploaded media, and its own set of database tables for its posts, categories, options, etc. Users can be members of some or all blogs within the multisite installation.
What is WordPress multi-multisite?It’s just a nickname for managing multiple instances of WordPress multisite. It can get messy to have different customers share one multisite instance, so I prefer to break it up so that each customer has their own multisite, but they can have many blogs within their multisite.
So that’s different from a “Network of Networks”?It’s apparently possible to run multiple instances of WordPress multisite against the same WordPress core installation. I’ve never looked into this, but I recall hearing about it over the years. I’ve heard the term “Network of Networks” and I like it, but that is not the scenario I’m covering in this article.
Why do you keep saying “blogs”? Do people still blog?You betcha! And people read them, too. You’re reading one right now. Hence, the need for a robust analytics solution. But this article could just as easily be about any sort of WordPress site. I happen to be dealing with blogs, and the word “blog” is a concise way to express “a subsite within a WordPress multisite instance”.
One more thing: In this article, I’ll use the term dashboard site to refer to the site from which I observe the compiled analytics data. I’ll use the term client sites to refer to the 25 multisites I pull data from.
My implementationMy strategy was to write one WordPress plugin that is installed on all 25 client sites, as well as on the dashboard site. The plugin serves two purposes:
- Expose data at API endpoints of the client sites
- Scrape the data from the client sites from the dashboard site, cache it in the database, and display it in a dashboard.
The WordPress REST API is my favorite part of WordPress. Out of the box, WordPress exposes default WordPress stuff like posts, authors, comments, media files, etc., via the WordPress REST API. You can see an example of this by navigating to /wp-json from any WordPress site, including CSS-Tricks. Here’s the REST API root for the WordPress Developer Resources site:
The root URL for the WordPress REST API exposes structured JSON data, such as this example from the WordPress Developer Resources website.What’s so great about this? WordPress ships with everything developers need to extend the WordPress REST API and publish custom endpoints. Exposing data via an API endpoint is a fantastic way to share it with other websites that need to consume it, and that’s exactly what I did:
Open the code <?php [...] function register(\WP_REST_Server $server) { $endpoints = $this->get(); foreach ($endpoints as $endpoint_slug => $endpoint) { register_rest_route( $endpoint['namespace'], $endpoint['route'], $endpoint['args'] ); } } function get() { $version = 'v1'; return array( 'empty_db' => array( 'namespace' => 'LXB_DBA/' . $version, 'route' => '/empty_db', 'args' => array( 'methods' => array( 'DELETE' ), 'callback' => array($this, 'empty_db_cb'), 'permission_callback' => array( $this, 'is_admin' ), ), ), 'get_blogs' => array( 'namespace' => 'LXB_DBA/' . $version, 'route' => '/get_blogs', 'args' => array( 'methods' => array('GET', 'OPTIONS'), 'callback' => array($this, 'get_blogs_cb'), 'permission_callback' => array($this, 'is_dba'), ), ), 'insert_blogs' => array( 'namespace' => 'LXB_DBA/' . $version, 'route' => '/insert_blogs', 'args' => array( 'methods' => array( 'POST' ), 'callback' => array($this, 'insert_blogs_cb'), 'permission_callback' => array( $this, 'is_admin' ), ), ), 'get_blogs_from_db' => array( 'namespace' => 'LXB_DBA/' . $version, 'route' => '/get_blogs_from_db', 'args' => array( 'methods' => array( 'GET' ), 'callback' => array($this, 'get_blogs_from_db_cb'), 'permission_callback' => array($this, 'is_admin'), ), ), 'get_blog_details' => array( 'namespace' => 'LXB_DBA/' . $version, 'route' => '/get_blog_details', 'args' => array( 'methods' => array( 'GET' ), 'callback' => array($this, 'get_blog_details_cb'), 'permission_callback' => array($this, 'is_dba'), ), ), 'update_blogs' => array( 'namespace' => 'LXB_DBA/' . $version, 'route' => '/update_blogs', 'args' => array( 'methods' => array( 'PATCH' ), 'callback' => array($this, 'update_blogs_cb'), 'permission_callback' => array($this, 'is_admin'), ), ), ); }We don’t need to get into every endpoint’s details, but I want to highlight one thing. First, I provided a function that returns all my endpoints in an array. Next, I wrote a function to loop through the array and register each array member as a WordPress REST API endpoint. Rather than doing both steps in one function, this decoupling allows me to easily retrieve the array of endpoints in other parts of my plugin to do other interesting things with them, such as exposing them to JavaScript. More on that shortly.
Once registered, the custom API endpoints are observable in an ordinary web browser like in the example above, or via purpose-built tools for API work, such as Postman:
PHP vs. JavaScriptI tend to prefer writing applications in PHP whenever possible, as opposed to JavaScript, and executing logic on the server, as nature intended, rather than in the browser. So, what would that look like on this project?
- On the dashboard site, upon some event, such as the user clicking a “refresh data” button or perhaps a cron job, the server would make an HTTP request to each of the 25 multisite installs.
- Each multisite install would query all of its blogs and consolidate its analytics data into one response per multisite.
Unfortunately, this strategy falls apart for a couple of reasons:
- PHP operates synchronously, meaning you wait for one line of code to execute before moving to the next. This means that we’d be waiting for all 25 multisites to respond in series. That’s sub-optimal.
- My production environment has a max execution limit of 60 seconds, and some of my multisites contain hundreds of blogs. Querying their analytics data takes a second or two per blog.
Damn. I had no choice but to swallow hard and commit to writing the application logic in JavaScript. Not my favorite, but an eerily elegant solution for this case:
- Due to the asynchronous nature of JavaScript, it pings all 25 Multisites at once.
- The endpoint on each Multisite returns a list of all the blogs on that Multisite.
- The JavaScript compiles that list of blogs and (sort of) pings all 900 at once.
- All 900 blogs take about one-to-two seconds to respond concurrently.
Holy cow, it just went from this:
( 1 second per Multisite * 25 installs ) + ( 1 second per blog * 900 blogs ) = roughly 925 seconds to scrape all the data.To this:
1 second for all the Multisites at once + 1 second for all 900 blogs at once = roughly 2 seconds to scrape all the data.That is, in theory. In practice, two factors enforce a delay:
- Browsers have a limit as to how many concurrent HTTP requests they will allow, both per domain and regardless of domain. I’m having trouble finding documentation on what those limits are. Based on observing the network panel in Chrome while working on this, I’d say it’s about 50-100.
- Web hosts have a limit on how many requests they can handle within a given period, both per IP address and overall. I was frequently getting a “429; Too Many Requests” response from my production environment, so I introduced a delay of 150 milliseconds between requests. They still operate concurrently, it’s just that they’re forced to wait 150ms per blog. Maybe “stagger” is a better word than “wait” in this context:
With these limitations factored in, I found that it takes about 170 seconds to scrape all 900 blogs. This is acceptable because I cache the results, meaning the user only has to wait once at the start of each work session.
The result of all this madness — this incredible barrage of Ajax calls, is just plain fun to watch:
PHP and JavaScript: Connecting the dotsI registered my endpoints in PHP and called them in JavaScript. Merging these two worlds is often an annoying and bug-prone part of any project. To make it as easy as possible, I use wp_localize_script():
<?php [...] class Enqueue { function __construct() { add_action( 'admin_enqueue_scripts', array( $this, 'lexblog_network_analytics_script' ), 10 ); add_action( 'admin_enqueue_scripts', array( $this, 'lexblog_network_analytics_localize' ), 11 ); } function lexblog_network_analytics_script() { wp_register_script( 'lexblog_network_analytics_script', LXB_DBA_URL . '/js/lexblog_network_analytics.js', array( 'jquery', 'jquery-ui-autocomplete' ), false, false ); } function lexblog_network_analytics_localize() { $a = new LexblogNetworkAnalytics; $data = $a -> get_localization_data(); $slug = $a -> get_slug(); wp_localize_script( 'lexblog_network_analytics_script', $slug, $data ); } // etc. }In that script, I’m telling WordPress two things:
- Load my JavaScript file.
- When you do, take my endpoint URLs, bundle them up as JSON, and inject them into the HTML document as a global variable for my JavaScript to read. This is leveraging the point I noted earlier where I took care to provide a convenient function for defining the endpoint URLs, which other functions can then invoke without fear of causing any side effects.
Here’s how that ended up looking:
The JSON and its associated JavaScript file, where I pass information from PHP to JavaScript using wp_localize_script(). Auth: Fort Knox or Sandbox?We need to talk about authentication. To what degree do these endpoints need to be protected by server-side logic? Although exposing analytics data is not nearly as sensitive as, say, user passwords, I’d prefer to keep things reasonably locked up. Also, since some of these endpoints perform a lot of database queries and Google Analytics API calls, it’d be weird to sit here and be vulnerable to weirdos who might want to overload my database or Google Analytics rate limits.
That’s why I registered an application password on each of the 25 client sites. Using an app password in php is quite simple. You can authenticate the HTTP requests just like any basic authentication scheme.
I’m using JavaScript, so I had to localize them first, as described in the previous section. With that in place, I was able to append these credentials when making an Ajax call:
async function fetchBlogsOfInstall(url, id) { let install = lexblog_network_analytics.installs[id]; let pw = install.pw; let user = install.user; // Create a Basic Auth token let token = btoa(`${user}:${pw}`); let auth = { 'Authorization': `Basic ${token}` }; try { let data = await $.ajax({ url: url, method: 'GET', dataType: 'json', headers: auth }); return data; } catch (error) { console.error('Request failed:', error); return []; } }That file uses this cool function called btoa() for turning the raw username and password combo into basic authentication.
The part where we say, “Oh Right, CORS.”Whenever I have a project where Ajax calls are flying around all over the place, working reasonably well in my local environment, I always have a brief moment of panic when I try it on a real website, only to get errors like this:
Oh. Right. CORS. Most reasonably secure websites do not allow other websites to make arbitrary Ajax requests. In this project, I absolutely do need the Dashboard Site to make many Ajax calls to the 25 client sites, so I have to tell the client sites to allow CORS:
<?php // ... function __construct() { add_action( 'rest_api_init', array( $this, 'maybe_add_cors_headers' ), 10 ); } function maybe_add_cors_headers() { // Only allow CORS for the endpoints that pertain to this plugin. if( $this->is_dba() ) { add_filter( 'rest_pre_serve_request', array( $this, 'send_cors_headers' ), 10, 2 ); } } function is_dba() { $url = $this->get_current_url(); $ep_urls = $this->get_endpoint_urls(); $out = in_array( $url, $ep_urls ); return $out; } function send_cors_headers( $served, $result ) { // Only allow CORS from the dashboard site. $dashboard_site_url = $this->get_dashboard_site_url(); header( "Access-Control-Allow-Origin: $dashboard_site_url" ); header( 'Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization' ); header( 'Access-Control-Allow-Methods: GET, OPTIONS' ); return $served; } [...] }You’ll note that I’m following the principle of least privilege by taking steps to only allow CORS where it’s necessary.
Auth, Part 2: I’ve been known to auth myselfI authenticated an Ajax call from the dashboard site to the client sites. I registered some logic on all the client sites to allow the request to pass CORS. But then, back on the dashboard site, I had to get that response from the browser to the server.
The answer, again, was to make an Ajax call to the WordPress REST API endpoint for storing the data. But since this was an actual database write, not merely a read, it was more important than ever to authenticate. I did this by requiring that the current user be logged into WordPress and possess sufficient privileges. But how would the browser know about this?
In PHP, when registering our endpoints, we provide a permissions callback to make sure the current user is an admin:
<?php // ... function get() { $version = 'v1'; return array( 'update_blogs' => array( 'namespace' => 'LXB_DBA/' . $version, 'route' => '/update_blogs', 'args' => array( 'methods' => array( 'PATCH' ), 'callback' => array( $this, 'update_blogs_cb' ), 'permission_callback' => array( $this, 'is_admin' ), ), ), // ... ); } function is_admin() { $out = current_user_can( 'update_core' ); return $out; }JavaScript can use this — it’s able to identify the current user — because, once again, that data is localized. The current user is represented by their nonce:
async function insertBlog( data ) { let url = lexblog_network_analytics.endpoint_urls.insert_blog; try { await $.ajax({ url: url, method: 'POST', dataType: 'json', data: data, headers: { 'X-WP-Nonce': getNonce() } }); } catch (error) { console.error('Failed to store blogs:', error); } } function getNonce() { if( typeof wpApiSettings.nonce == 'undefined' ) { return false; } return wpApiSettings.nonce; }The wpApiSettings.nonce global variable is automatically present in all WordPress admin screens. I didn’t have to localize that. WordPress core did it for me.
Cache is KingCompressing the Google Analytics data from 900 domains into a three-minute loading .gif is decent, but it would be totally unacceptable to have to wait for that long multiple times per work session. Therefore I cache the results of all 25 client sites in the database of the dashboard site.
I’ve written before about using the WordPress Transients API for caching data, and I could have used it on this project. However, something about the tremendous volume of data and the complexity implied within the Figma design made me consider a different approach. I like the saying, “The wider the base, the higher the peak,” and it applies here. Given that the user needs to query and sort the data by date, author, and metadata, I think stashing everything into a single database cell — which is what a transient is — would feel a little claustrophobic. Instead, I dialed up E.F. Codd and used a relational database model via custom tables:
In the Dashboard Site, I created seven custom database tables, including one relational table, to cache the data from the 25 client sites, as shown in the image.It’s been years since I’ve paged through Larry Ullman’s career-defining (as in, my career) books on database design, but I came into this project with a general idea of what a good architecture would look like. As for the specific details — things like column types — I foresaw a lot of Stack Overflow time in my future. Fortunately, LLMs love MySQL and I was able to scaffold out my requirements using DocBlocks and let Sam Altman fill in the blanks:
Open the code <?php /** * Provides the SQL code for creating the Blogs table. It has columns for: * - ID: The ID for the blog. This should just autoincrement and is the primary key. * - name: The name of the blog. Required. * - slug: A machine-friendly version of the blog name. Required. * - url: The url of the blog. Required. * - mapped_domain: The vanity domain name of the blog. Optional. * - install: The name of the Multisite install where this blog was scraped from. Required. * - registered: The date on which this blog began publishing posts. Optional. * - firm_id: The ID of the firm that publishes this blog. This will be used as a foreign key to relate to the Firms table. Optional. * - practice_area_id: The ID of the firm that publishes this blog. This will be used as a foreign key to relate to the PracticeAreas table. Optional. * - amlaw: Either a 0 or a 1, to indicate if the blog comes from an AmLaw firm. Required. * - subscriber_count: The number of email subscribers for this blog. Optional. * - day_view_count: The number of views for this blog today. Optional. * - week_view_count: The number of views for this blog this week. Optional. * - month_view_count: The number of views for this blog this month. Optional. * - year_view_count: The number of views for this blog this year. Optional. * * @return string The SQL for generating the blogs table. */ function get_blogs_table_sql() { $slug = 'blogs'; $out = "CREATE TABLE {$this->get_prefix()}_$slug ( id BIGINT NOT NULL AUTO_INCREMENT, slug VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, url VARCHAR(255) NOT NULL UNIQUE, /* adding unique constraint */ mapped_domain VARCHAR(255) UNIQUE, install VARCHAR(255) NOT NULL, registered DATE DEFAULT NULL, firm_id BIGINT, practice_area_id BIGINT, amlaw TINYINT NOT NULL, subscriber_count BIGINT, day_view_count BIGINT, week_view_count BIGINT, month_view_count BIGINT, year_view_count BIGINT, PRIMARY KEY (id), FOREIGN KEY (firm_id) REFERENCES {$this->get_prefix()}_firms(id), FOREIGN KEY (practice_area_id) REFERENCES {$this->get_prefix()}_practice_areas(id) ) DEFAULT CHARSET=utf8mb4;"; return $out; }In that file, I quickly wrote a DocBlock for each function, and let the OpenAI playground spit out the SQL. I tested the result and suggested some rigorous type-checking for values that should always be formatted as numbers or dates, but that was the only adjustment I had to make. I think that’s the correct use of AI at this moment: You come in with a strong idea of what the result should be, AI fills in the details, and you debate with it until the details reflect what you mostly already knew.
How it’s goingI’ve implemented most of the user stories now. Certainly enough to release an MVP and begin gathering whatever insights this data might have for us:
It’s working!One interesting data point thus far: Although all the blogs are on the topic of legal matters (they are lawyer blogs, after all), blogs that cover topics with a more general appeal seem to drive more traffic. Blogs about the law as it pertains to food, cruise ships, germs, and cannabis, for example. Furthermore, the largest law firms on our network don’t seem to have much of a foothold there. Smaller firms are doing a better job of connecting with a wider audience. I’m positive that other insights will emerge as we work more deeply with this.
Regrets? I’ve had a few.This project probably would have been a nice opportunity to apply a modern JavaScript framework, or just no framework at all. I like React and I can imagine how cool it would be to have this application be driven by the various changes in state rather than… drumroll… a couple thousand lines of jQuery!
I like jQuery’s ajax() method, and I like the jQueryUI autocomplete component. Also, there’s less of a performance concern here than on a public-facing front-end. Since this screen is in the WordPress admin area, I’m not concerned about Google admonishing me for using an extra library. And I’m just faster with jQuery. Use whatever you want.
I also think it would be interesting to put AWS to work here and see what could be done through Lambda functions. Maybe I could get Lambda to make all 25 plus 900 requests concurrently with no worries about browser limitations. Heck, maybe I could get it to cycle through IP addresses and sidestep the 429 rate limit as well.
And what about cron? Cron could do a lot of work for us here. It could compile the data on each of the 25 client sites ahead of time, meaning that the initial three-minute refresh time goes away. Writing an application in cron, initially, I think is fine. Coming back six months later to debug something is another matter. Not my favorite. I might revisit this later on, but for now, the cron-free implementation meets the MVP goal.
I have not provided a line-by-line tutorial here, or even a working repo for you to download, and that level of detail was never my intention. I wanted to share high-level strategy decisions that might be of interest to fellow Multi-Multisite people. Have you faced a similar challenge? I’d love to hear about it in the comments!
WordPress Multi-Multisite: A Case Study originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Follow Up: We Officially Have a CSS Logo!
As a follow up to the search for a new CSS logo, it looks like we have a winner!
Since our last post, the color shifted away from a vibrant pink to a color with a remarkable history among the CSS community: rebeccapurple
CodePen Embed FallbackWith 400 votes on GitHub, I think the community has chosen well.
Check out Adam’s post on selecting the winner!
Follow Up: We Officially Have a CSS Logo! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Alt Text: Not Always Needed
Alt text is one of those things in my muscle memory that pops up anytime I’m working with an image element. The attribute almost writes itself.
<img src="image.jpg" alt="">Or if you use Emmet, that’s autocompleted for you. Don’t forget the alt text! Use it even if there’s no need for it, as an empty string is simply skipped by screen readers. That’s called “nulling” the alternative text and many screen readers simply announce the image file name. Just be sure it’s truly an empty string because even a space gets picked up by some assistive tech, which causes a screen reader to completely skip the image:
<!-- Not empty --> <img src="image.jpg" alt=" ">But wait… are there situations where an image doesn’t need alt text? I tend to agree with Eric that the vast majority of images are more than decorative and need to be described. Your images are probably not decorative and ought to be described with alt text.
Probably is doing a lot of lifting there because not all images are equal when it comes to content and context. Emma Cionca and Tanner Kohler have a fresh study on those situations where you probably don’t need alt. It’s a well-written and researched piece and I’m rounding up some nuggets from it.
What Users Need from Alt TextIt’s the same as what anyone else would need from an image: an easy path to accomplish basic tasks. A product image is a good example of that. Providing a visual smooths the path to purchasing because it’s context about what the item looks like and what to expect when you get it. Not providing an image almost adds friction to the experience if you have to stop and ask customer support basic questions about the size and color of that shirt you want.
So, yes. Describe that image in alt! But maybe “describe” isn’t the best wording because the article moves on to make the next point…
Quit Describing What Images Look LikeThe article gets into a common trap that I’m all too guilty of, which is describing an image in a way that I find helpful. Or, as the article says, it’s a lot like I’m telling myself, “I’ll describe it in the alt text so screen-reader users can imagine what they aren’t seeing.”
That’s the wrong way of going about it. Getting back to the example of a product image, the article outlines how a screen reader might approach it:
For example, here’s how a screen-reader user might approach a product page:
- Jump between the page headers to get a sense of the page structure.
- Explore the details of a specific section with the heading label Product Description.
- Encounter an image and wonder “What information that I might have missed elsewhere does this image communicate about the product?”
Interesting! Where I might encounter an image and evaluate it based on the text around it, a screen reader is already questioning what content has been missed around it. This passage is one I need to reflect on (emphasis mine):
Most of the time, screen-reader users don’t wonder what images look like. Instead, they want to know their purpose. (Exceptions to this rule might include websites presenting images, such as artwork, purely for visual enjoyment, or users who could previously see and have lost their sight.)
OK, so how in the heck do we know when an image needs describing? It feels so awkward making what’s ultimately a subjective decision. Even so, the article presents three questions to pose to ourselves to determine the best route.
- Is the image repetitive? Is the task-related information in the image also found elsewhere on the page?
- Is the image referential? Does the page copy directly reference the image?
- Is the image efficient? Could alt text help users more efficiently complete a task?
This is the meat of the article, so I’m gonna break those out.
Is the image repetitive?Repetitive in the sense that the content around it is already doing a bang-up job painting a picture. If the image is already aptly “described” by content, then perhaps it’s possible to get away with nulling the alt attribute.
This is the figure the article uses to make the point (and, yes, I’m alt-ing it):
The caption for this image describes exactly what the image communicates. Therefore, any alt text for the image will be redundant and a waste of time for screen-reader users. In this case, the actual alt text was the same as the caption. Coming across the same information twice in a row feels even more confusing and unnecessary.
The happy path:
<img src="image.jpg" alt="">But check this out this image about informal/semi-formal table setting showing how it is not described by the text around it (and, no, I’m not alt-ing it):
If I was to describe this image, I might get carried away describing the diagram and all the points outlined in the legend. If I can read all of that, then a screen reader should, too, right? Not exactly. I really appreciate the slew of examples provided in the article. A sampling:
- Bread plate and butter knife, located in the top left corner.
- Dessert fork, placed horizontally at the top center.
- Dessert spoon, placed horizontally at the top center, below the dessert fork.
That’s way less verbose than I would have gone. Talking about how long (or short) alt ought to be is another topic altogether.
Is the image referential?The second image I dropped in that last section is a good example of a referential image because I directly referenced it in the content preceding it. I nulled the alt attribute because of that. But what I messed up is not making the image recognizable to screen readers. If the alt attribute is null, then the screen reader skips it. But the screen reader should still know it’s there even if it’s aptly described.
The happy path:
<img src="image.jpg" alt="">Remember that a screen reader may announce the image’s file name. So maybe use that as an opportunity to both call out the image and briefly describe it. Again, we want the screen reader to announce the image if we make mention of it in the content around it. Simply skipping it may cause more confusion than clarity.
Is the image efficient?My mind always goes to performance when I see the word efficient pop up in reference to images. But in this context the article means whether or not the image can help visitors efficiently complete a task.
If the image helps complete a task, say purchasing a product, then yes, the image needs alt text. But if the content surrounding it already does the job then we can leave it null (alt="") or skip it (alt=" ") if there’s no mention of it.
Wrapping upI put a little demo together with some testing results from a few different screen readers to see how all of that shakes out.
CodePen Embed FallbackAlt Text: Not Always Needed 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.
