> All in One 586: January 2026

Ads

Saturday, January 31, 2026

Clear today!



With a high of F and a low of 23F. Currently, it's 27F and Fair outside.

Current wind speeds: 6 from the West

Pollen: 0

Sunrise: January 31, 2026 at 07:59PM

Sunset: February 1, 2026 at 06:11AM

UV index: 0

Humidity: 74%

via https://ift.tt/OG0MwhU

February 1, 2026 at 10:02AM

Friday, January 30, 2026

Cloudy today!



With a high of F and a low of 15F. Currently, it's 19F and Cloudy outside.

Current wind speeds: 7 from the Southeast

Pollen: 0

Sunrise: January 30, 2026 at 08:00PM

Sunset: January 31, 2026 at 06:09AM

UV index: 0

Humidity: 97%

via https://ift.tt/CRmSszA

January 31, 2026 at 10:02AM

What’s !important #4: Videos & View Transitions, Named Media Queries, How Browsers Work, and More

Neither Chrome, Safari, nor Firefox have shipped new features in the last couple of weeks, but fear not because leading this issue of What’s !important is some of the web development industry’s best educators with, frankly, some killer content.

Maintaining video state across different pages using view transitions

Chris Coyier demonstrates how to maintain a video’s state across different pages using CSS view transitions. He notes that this is fairly easy to do with same-page view transitions, but with multi-page view transitions you’ll need to leverage JavaScript’s pageswap event to save information about the video’s state in sessionStorage as a JSON string (works with audio and iframes too), and then use that information to restore the state on pagereveal. Yes, there’s a tiiiiny bit of audio stutter because we’re technically faking it, but it’s still super neat.

Also, CodePen, which I’m sure you already know was founded by Chris, announced a private beta of CodePen 2.0, which you can request to be a part of. One of the benefits of CodePen 2.0 is that you can create actual projects with multiple files, which means that you can create view transitions in CodePen. Pretty cool!

How to ‘name’ media queries

Kevin Powell shows us how to leverage CSS cascade layers to ‘name’ media queries. This technique isn’t as effective as @custom-media (or even container style queries, as one commenter suggested), but until those are supported in all web browsers, Kevin’s trick is pretty creative.

Adam Argyle reminded us last week that @custom-media is being trialed in Firefox Nightly (no word on container style queries yet), but if you get up to speed on CSS cascade layers, you can utilize Kevin’s trick in the meantime.

Vale’s CSS reset

I do love a good CSS reset. It doesn’t matter how many of them I read, I always discover something awesome and add it to my own reset. From Vale’s CSS reset I stole svg:not([fill]) { fill: currentColor; }, but there’s much more to take away from it than that!

How browsers work

If you’ve ever wondered how web browsers actually work — how they get IP addresses, make HTTP requests, parse HTML, build DOM trees, render layouts, and paint, the recently-shipped How Browsers Work by Dmytro Krasun is an incredibly interesting, interactive read. It really makes you wonder about the bottlenecks of web development languages and why certain HTML, CSS, and JavaScript features are the way they are.

A diagram showing the HTML parsing process with a code example on the left and the resulting DOM tree structure on the right.

How CSS layout works

In addition, Polypane explains the fundamentals of CSS layout, including the box model, lines and baselines, positioning schemes, the stacking context, grid layout, and flexbox. If you’re new to CSS, I think these explanations will really help you click with it. If you’re an old-timer (like me), I still think it’s important to learn how these foundational concepts apply to newer CSS features, especially since CSS is evolving exponentially these days.

A diagram showing CSS z-index stacking order with code examples on the left and visual representations of layered elements on the right.

CSS masonry is (probably) just around the corner

Speaking of layouts, Jen Simmons clarifies when we’ll be able to use display: grid-lanes, otherwise known as CSS masonry. While it’s not supported in any web browser yet, Firefox, Safari, and Chrome/Edge are all trialing it, so that could change pretty quickly. Jen provides some polyfills, anyway!

If you want to get ahead of the curve, you can let Sunkanmi Fafowora walk you through display: grid-lanes.

A comparison showing two masonry-style card layouts labeled 'Grid Lanes' and 'CSS Grid 1' with different arrangements of image cards.
Source: Webkit.

Theming animations using relative color syntax

If you’re obsessed with design systems and organization, and you tend to think of illustration and animation as impressive but messy art forms, Andy Clarke’s article on theming animations using CSS relative color syntax will truly help you to bridge the gap between art and logic. If CSS variables are your jam, then this article is definitely for you.

A diagram showing CSS color calculations with code examples above and visual comparisons of lightness, chroma, and hue adjustments below.

Modals vs. pages (and everything in-between)

Modals? Pages? Lightboxes? Dialogs? Tooltips? Understanding the different types of overlays and knowing when to use each one is still pretty confusing, especially since newer CSS features like popovers and interest invokers, while incredibly useful, are making the landscape more cloudy. In short, Ryan Neufeld clears up the whole modal vs. page thing and even provides a framework for deciding which type of overlay to use.

Source: UX Planet

Text scaling support is being trialed in Chrome Canary

You know when you’re dealing with text that’s been increased or decreased at the OS-level? Well…if you’re a web developer, maybe you don’t. After all, this feature doesn’t work on the web! However, Josh Tumath tells us that Chrome Canary is trialing a meta tag that makes web browsers respect this OS setting. If you’re curious, it’s <meta name="text-scale" content="scale">, but Josh goes into more detail and it’s worth a read.

See you next time!


What’s !important #4: Videos & View Transitions, Named Media Queries, How Browsers Work, and More originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/uMdQRoK
via IFTTT

Thursday, January 29, 2026

Mostly Clear today!



With a high of F and a low of 22F. Currently, it's 26F and Partly Cloudy outside.

Current wind speeds: 8 from the Northwest

Pollen: 0

Sunrise: January 29, 2026 at 08:00PM

Sunset: January 30, 2026 at 06:08AM

UV index: 0

Humidity: 74%

via https://ift.tt/UnSmCyl

January 30, 2026 at 10:02AM

Wednesday, January 28, 2026

Clouds Early/Clearing Late today!



With a high of F and a low of 26F. Currently, it's 31F and Clear outside.

Current wind speeds: 7 from the Northwest

Pollen: 0

Sunrise: January 28, 2026 at 08:01PM

Sunset: January 29, 2026 at 06:07AM

UV index: 0

Humidity: 47%

via https://ift.tt/aGmyEkr

January 29, 2026 at 10:02AM

ReliCSS

We all have a few skeletons in our CSS closets. There’s probably that one-off !important where you can now manage that more effectively with cascade layers. Or maybe a dated Checkbox Hack that :has() has solved. Perhaps it’s been a long while since your last site redesign and it’s chock-full of vendor-prefixed properties from 2012. Thar be demons!

Stu Robson’s ReliCSS (clever name!) tool can excavate outdated CSS in your codebase that have modern CSS solutions.

Each relic is assigned a level of severity. As Stu explains it:

  • High Severity: True “fossils”. Hacks for (now) unsupported browsers (IE6/7) or “dangerous” techniques. High-risk, obsolete, should be first targets for removal.
  • Medium Severity: The middle ground. Hacks for older unsupported browsers (IE8-10). They work but they’re fragile. Hacks to review to see if they’re still relevant for your actual users.
  • Low Severity: Modern artifacts. Usually vendor prefixes (-webkit-, -moz-). Safe mostly, but better handled by automated tools like Autoprefixer. They’re an opportunity to improve your build process.

It’s been a little while since my personal site got an overhaul. Not to toot my own horn, but heyyyyyy!

Screenshot of a CSS audit using Stu Robson's ReliCSS tool. No issues are found.

Seriously, though. I know there are things in there I’m embarrassed to admit.

But what if we do archeological dig on CSS-Tricks? I mean, it’s been at least five years since this place has gotten the love it deserves. I’m almost afraid to look. Here goes…

Screenshot auditing CSS-Tricks CSS stylesheet in Stu Robson's ReliCSS tool. Out shows 19 total relics.
🫣

OK, not as bad as I imagined. It’s largely vendor prefixing, which I’m sure comes courtesy of an older Autoprefixer configuration.


ReliCSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/RxScmW2
via IFTTT

How to Style the New ::search-text and Other Highlight-y Pseudo-Elements

Chrome 144 recently shipped ::search-text, which is now one of several highlight-related pseudo-elements. This one selects find-in-page text, which is the text that gets highlighted when you do a Ctrl/Command + F-type search for something on a page and matches are found.

By default, ::search-text matches are yellow while the current target (::search-text:current) is orange, but ::search-text enables us to change that.

I’ll admit, I hadn’t really been following these highlight pseudo-elements. Up until now, I didn’t even know that there was a name for them, but I’m glad there is because that makes it easier to round them all up and compare them, which is exactly what I’m going to do here today, as it’s not super obvious what they do based on the name of the pseudo-element. I’ll also explain why we’re able to customize them, and suggest how.

The different types of highlight pseudo-elements

Pseudo-selector Selects… Notes
::search-text Find-in-page matches ::search-text:current selects the current target
::target-text Text fragments Text fragments allow for programmatic highlighting using URL parameters. If you’re referred to a website by a search engine, it might use text fragments, which is why ::target-text is easily confused with ::search-text.
::selection Text highlighted using the pointer
::highlight() Custom highlights as defined by JavaScript’s Custom Highlight API
::spelling-error Incorrectly spelled words Pretty much applies to editable content only
::grammar-error Incorrect grammar Pretty much applies to editable content only

And let’s not forget about the <mark> HTML element either, which is what I’m using in the demos below.

What should highlight pseudo-elements look like?

The question is, if they all (besides ::highlight()) have default styling, why would we need to select them with pseudo-elements? The reason is accessibility (color contrast, specifically) and usability (emphasis). For example, if the default yellow background of ::search-text doesn’t contrast well enough with the text color, or if it doesn’t stand out against the background of the container, then you’ll want to change that.

I’m sure there are many ways to solve this (I want to hear “challenge accepted” in the comments), but the best solution that I’ve come up with uses relative color syntax. I took wrong turns with both background-clip: text and backdrop-filter: invert(1) before realizing that many CSS properties are off-limits when it comes to highlight pseudo-elements:

body {
  --background: #38003c;
  background: var(--background);

  mark,
  ::selection,
  ::target-text,
  ::search-text {
    /* Match color to background */
    color: var(--background);

    /* Convert to RGB then subtract channel value from channel maximum (255) */
    background: rgb(from var(--background) calc(255 - r) calc(255 - g) calc(255 - b));
  }
}

Your browser might not support that yet, so here’s a video that shows how the highlighted text adapts to background color changes.

What’s happening here is that I’m converting the container’s background color to RGB format and then subtracting the value of each channel (r, g, and b) from the maximum channel value of 255, inverting each channel and the overall color. This color is then set as the background color of the highlighting, ensuring that it stands out no matter what, and thanks to the new CodePen slideVars, you can mess around with the demo to see this in action. You might be able to do this with color formats besides RGB, but RGB is the easiest.

So that covers the usability, but what about the accessibility?

Well, the highlighting’s text color is the same as the container’s background color because we know that it’s the inverse of the highlighting’s background color. While this doesn’t mean that the two colors will have accessible contrast, it seems as though they will most of the time (you should always check color contrast using color contrast tools, regardless).

If you don’t like the randomness of inverting colors, that’s understandable. You can totally pick colors and write conditional CSS for them manually instead, but finding accessible colors that stand out against the different backdrops of your design for all of the different types of highlight pseudo-elements, while accounting for alternative viewing modes such as dark mode, is a headache. Besides, I think certain UI elements (e.g., highlights, errors, focus indicators) should be ugly. They should stand out in a brutalist sort of way and feel disconnected from the design’s color palette. They should demand maximum attention by intentionally not fitting in.

Keep in mind that the different types of highlight pseudo-elements should be visually distinctive too, for obvious reasons, but also in case two different types overlap each other (e.g., the user selects text currently matched by find-in-page). Therefore, in the amended code snippet below, mark, ::selection, ::target-text, and ::search-text all have slightly different backgrounds.

I’ve left mark unchanged, the r value of ::selection as it was, the g value of ::target-text as it was, and the b value of ::search-text as it was, so those last three only have two channels inverted instead of all three. They’re varied in color now (but still look inverted), and with the addition of an alpha value at 70% (100% for ::search-text:current), they also blend into each other so that we can see where each highlight begins and ends:

body {
  --background: #38003c;
  background: var(--background);

  mark,
  ::selection,
  ::target-text,
  ::search-text {
    color: var(--background);
  }

  mark {
    /* Invert all channels */
    background: rgb(from var(--background) calc(255 - r) calc(255 - g) calc(255 - b) / 70%);
  }

  ::selection {
    /* Invert all channels but R */
    background: rgb(from var(--background) r calc(255 - g) calc(255 - b) / 70%);
  }

  ::target-text {
    /* Invert all channels but G */
    background: rgb(from var(--background) calc(255 - r) g calc(255 - b) / 70%);
  }

  ::search-text {
    /* Invert all channels but B */
    background: rgb(from var(--background) calc(255 - r) calc(255 - g) b / 70%);
    
    &:current {
      /* Invert all channels but B, but without transparency */
      background: rgb(from var(--background) calc(255 - r) calc(255 - g) b / 100%);
    }
  }
}

::spelling-error and ::grammar-error are excluded from all this because they have their own visual affordances (red underlines and green underlines respectively, typically contrasted against the neutral background of an editable element such as <textarea>).

But mark, ::selection, ::target-text, and new-to-Chrome ::search-text? Well, they can appear anywhere (even on top of each other), so I think it’s important that they’re visually distinctive from each other while being accessible at all times. Again though, even fully-inverted colors can be inaccessible. In fact, the inverse of #808080 is #808080, so test, test, test! Although, maybe contrast-color() could come to the rescue once the CSS Color Module Level 5 version of it ships.

In the meantime, please, no more highlight-y elements!


How to Style the New ::search-text and Other Highlight-y Pseudo-Elements originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/WAdOgpf
via IFTTT

Tuesday, January 27, 2026

Clear today!



With a high of F and a low of 19F. Currently, it's 23F and Clear outside.

Current wind speeds: 7 from the Southwest

Pollen: 0

Sunrise: January 27, 2026 at 08:02PM

Sunset: January 28, 2026 at 06:06AM

UV index: 0

Humidity: 65%

via https://ift.tt/1AXpuvi

January 28, 2026 at 10:02AM

Monday, January 26, 2026

Mostly Clear today!



With a high of F and a low of 15F. Currently, it's 22F and Clear outside.

Current wind speeds: 8 from the Southwest

Pollen: 0

Sunrise: January 26, 2026 at 08:03PM

Sunset: January 27, 2026 at 06:05AM

UV index: 0

Humidity: 56%

via https://ift.tt/MFtxigI

January 27, 2026 at 10:02AM

There is No Need to Trap Focus on a Dialog Element

I was building a Modal component that uses the <dialog> element’s showModal method. While testing the component, I discovered I could tab out of the <dialog> (in modal mode) and onto the address bar.

And I was surprised — accessibility advice around modals have commonly taught us to trap focus within the modal. So this seems wrong to me.

Upon further research, it seems like we no longer need to trap focus within the <dialog> (even in modal mode). So, the focus-trapping is deprecated advice if you use <dialog>.

Some notes for you

Instead of asking you to read through the entire GitHub Issue detailing the discussion, I summarized a couple of key points from notable people below.

Here are some comments from Scott O’Hara that tells us about the history and context of the focus-trapping advice:

WCAG is not normatively stating focus must be trapped within a dialog. Rather, the normative WCAG spec makes zero mention of requirements for focus behavior in a dialog.

The informative 2.4.3 focus order understanding doc does talk about limiting focus behavior within a dialog – but again, this is in the context of a scripted custom dialog and was written long before inert or <dialog> were widely available.

The purpose of the APG is to demonstrate how to use ARIA. And, without using native HTML features like <dialog> or inert, it is far easier to trap focus within the custom dialog than it is to achieve the behavior that the <dialog> element has.

Both the APG modal dialog and the WCAG understanding doc were written long before the inert attribute or the <dialog> element were widely supported. And, the alternative to instructing developers to trap focus in the dialog would have been to tell them that they needed to ensure that all focusable elements in the web page, outside of the modal dialog, received a tabindex=-1.

Léonie Watson weighs in and explains why it’s okay for a screen-reader user to move focus to the address bar:

In the page context you can choose to Tab out of the bottom and around the browser chrome, you can use a keyboard command to move straight to the address bar or open a particular menu, you can close the tab, and so on. This gives people a choice about how, why, and what they do to escape out of the context.

It seems logical (to me at least) for the same options to be available to people when in a dialog context instead of a page context.

Finally, Matatk shared the conclusion from the W3C’s Accessible Platform Architectures (APA) Working Group that okay-ed the notion that <dialog>‘s showModal method doesn’t need to trap focus.

We addressed this question in the course of several APA meetings and came to the conclusion that the current behavior of the native dialog element should be kept as it is. So, that you can tab from the dialog to the browser functionalities.

We see especially the benefit that keyboard users can, for example, open a new tab to look something important up or to change a browser setting this way. At the same time, the dialog element thus provides an additional natural escape mechanism (i.e. moving to the address bar) in, for example, kiosk situations where the user cannot use other standard keyboard shortcuts.

From what I’m reading, it sounds like we don’t have to worry about focus trapping if we’re properly using the Dialog API’s showModal method!

Hope this news make it easier for you to build components. 😉


There is No Need to Trap Focus on a Dialog Element originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/iQX6GLW
via IFTTT

Sunday, January 25, 2026

Snow Showers Early today!



With a high of F and a low of -3F. Currently, it's 6F and Snow Shower outside.

Current wind speeds: 10 from the Northeast

Pollen: 0

Sunrise: January 25, 2026 at 08:03PM

Sunset: January 26, 2026 at 06:04AM

UV index: 0

Humidity: 87%

via https://ift.tt/l8npS97

January 26, 2026 at 10:02AM

Saturday, January 24, 2026

Partly Cloudy today!



With a high of F and a low of -3F. Currently, it's 5F and Cloudy outside.

Current wind speeds: 7 from the Northwest

Pollen: 0

Sunrise: January 24, 2026 at 08:04PM

Sunset: January 25, 2026 at 06:03AM

UV index: 0

Humidity: 76%

via https://ift.tt/jYRT2zU

January 25, 2026 at 10:02AM

Friday, January 23, 2026

Cloudy today!



With a high of F and a low of -4F. Currently, it's -2F and Cloudy outside.

Current wind speeds: 8 from the Southeast

Pollen: 0

Sunrise: January 23, 2026 at 08:05PM

Sunset: January 24, 2026 at 06:01AM

UV index: 0

Humidity: 82%

via https://ift.tt/8xVLqtb

January 24, 2026 at 10:02AM

Open Props @custom-media Recipes

The @custom-media at-rule has landed in Firefox Nightly! I couldn’t find it in the release notes but Adam Argyle’s on the beat noting that it’s behind a flag for now.

The Firefox Nightly configuration screen searching for custom-media and with layout.css.custom-media.enabled.
Look for layout.css.custom-media.enabled

I often forget the exact name of an @media query or simply get tired writing something like @media screen and (prefers-reduced-motion: no-preference) over and over again. @custom-media will be a nice bit of relief to the ol’ muscle memory because it allows us to create aliases for queries.

In fact, Adam’s Open Props project has more than 45 of them that make for excellent recipes:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

@media (--motionOK) {
  /* animations and transitions */
}

Open Props @custom-media Recipes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/SiAYTNl
via IFTTT

Responsive Hexagon Grid Using Modern CSS

Five years ago I published an article on how to create a responsive grid of hexagon shapes. It was the only technique that didn’t require media queries or JavaScript. It works with any number of items, allowing you to easily control the size and gap using CSS variables.

I am using float, inline-block, setting font-size equal to 0, etc. In 2026, this may sound a bit hacky and outdated. Not really since this method works fine and is well supported, but can we do better using modern features? In five years, many things have changed and we can improve the above implementation and make it less hacky!

Support is limited to Chrome only because this technique uses recently released features, including corner-shape, sibling-index(), and unit division.

The CSS code is shorter and contains fewer magic numbers than the last time I approached this. You will also find some complex calculations that we will dissect together.

Before diving into this new demo, I highly recommend reading my previous article first. It’s not mandatory, but it allows you to compare both methods and realize how much (and rapidly) CSS has evolved in the last five years by introducing new features that make one-difficult things like this easier.

The Hexagon Shape

Let’s start with the hexagon shape, which is the main element of our grid. Previously, I had to rely on clip-path: polygon() to create it:

.hexagon {
  --s: 100px;
  width: var(--s);
  height: calc(var(--s) * 1.1547);
  clip-path: polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%);
}

But now, we can rely on the new corner-shape property which works alongside the border-radius property:

.hexagon {
  width: 100px;
  aspect-ratio: cos(30deg);
  border-radius: 50% / 25%;
  corner-shape: bevel;
}

Simpler than how we used to bevel elements, and as a bonus, we can add a border to the shape without workarounds!

The corner-shape property is the first modern feature we are relying on. It makes drawing CSS shapes a lot easier than traditional methods, like using clip-path. You can still keep using the clip-path method, of course, for better support (and if you don’t need a border on the element), but here is a more modern implementation:

.hexagon {
  width: 100px;
  aspect-ratio: cos(30deg);
  clip-path: polygon(-50% 50%,50% 100%,150% 50%,50% 0);
}

There are fewer points inside the polygon, and we replaced the magic number 1.1547 with an aspect-ratio declaration. I won’t spend more time on the code of the shapes, but here are two articles I wrote if you want a detailed explanation with more examples:

The Responsive Grid

Now that we have our shape, let’s create the grid. It’s called a “grid,” but I am going to use a flexbox configuration:

<div class="container">
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <!-- etc. -->
</div>
.container {
  --s: 120px; /* size  */
  --g: 10px; /* gap */
  
  display: flex;
  gap: var(--g);
  flex-wrap: wrap;
}
.container > * {
  width: var(--s);
  aspect-ratio: cos(30deg);
  border-radius: 50% / 25%;
  corner-shape: bevel;
}

Nothing fancy so far. From there, we add a bottom margin to all items to create an overlap between the rows:

.container > * {
  margin-bottom: calc(var(--s)/(-4*cos(30deg)));
}

The last step is to add a left margin to the first item of the even rows (i.e., 2nd, 4th, 6th, and so). This margin will create the shift between rows to achieve a perfect grid.

Said like that, it sounds easy, but it’s the trickiest part where we need complex calculations. The grid is responsive, so the “first” item we are looking for can be any item depending, on the container size, item size, gap, etc.

Let’s start with a figure:

Two grids of hexagons, arranged side-by-side. N and M variables are between them illustrating odd rows with the N variable and even rows with M.

Our grid can have two aspects depending on the responsiveness. We can either have the same number of items in all the rows (Grid 1 in the figure above) or a difference of one item between two consecutive rows (Grid 2). The N and M variables represent the number of items in the rows. In Grid 1 we have N = M, and in Grid 2 we have M = N - 1.

In Grid 1, the items with a left margin are 6, 16, 26, etc., and in Grid 2, they are 7, 18, 29, etc. Let’s try to identify the logic behind those numbers.

The first item in both grids (6 or 7) is the first one in the second row, so it’s the item N + 1. The second item (16 or 18) is the first one in the third row, so it’s the item N + M + N + 1. The third item (26 or 29) is the item N + M + N + M + N + 1. If you look closely, you can see a pattern that we can express using the following formula:

N*i + M*(i - 1) + 1

…where i is a positive integer (zero excluded). The items we are looking for can be found using the following pseudo-code:

for(i = 0; i< ?? ;i++) {
  index = N*i + M*(i - 1) + 1
  Add margin to items[index]  
}

We don’t have loops in CSS, though, so we will have to do something different. We can obtain the index of each item using the new sibling-index() function. The logic is to test if that index respect the previous formula.

Instead of writing this:

index = N*i + M*(i - 1) + 1

…let’s express i using the index:

i = (index - 1 + M)/(N + M)

We know that i is a positive integer (zero excluded), so for each item, we get its index and test if (index - 1 + M)/(N + M) is a positive integer. Before that, let’s calculate the number of items, N and M.

Calculating the number of items per row is the same as calculating how many items can fit in that row.

N = round(down,container_size / item_size);

Dividing the container size by the item size gives us a number. If we round()` it down to the nearest integer, we get the number of items per row. But we have a gap between items, so we need to account for this in the formula:

N = round(down, (container_size + gap)/ (item_size + gap));

We do the same for M, but this time we need to also account for the left margin applied to the first item of the row:

M = round(down, (container_size + gap - margin_left)/ (item_size + gap));

Let’s take a closer look and identify the value of that margin in the next figure:

Illustrating the width of a single hexagon shape and the left margin between rows, which is one half the width of an item.

It’s equal to half the size of an item, plus half the gap:

M = round(down, (container_size + gap - (item_size + gap)/2)/(item_size + gap));

M = round(down, (container_size - (item_size - gap)/2)/(item_size + gap));

The item size and the gap are defined using the --s and --g variables, but what about the container size? We can rely on container query units and use 100cqw.

Let’s write what we have until now using CSS:

.container {
  --s: 120px;  /* size  */
  --g: 10px;   /* gap */
  
  container-type: inline-size; /* we make it a container to use 100cqw */
}
.container > * {
  --_n: round(down,(100cqw + var(--g))/(var(--s) + var(--g)));
  --_m: round(down,(100cqw - (var(--s) - var(--g))/2)/(var(--s) + var(--g))); 
  --_i: calc((sibling-index() - 1 + var(--_m))/(var(--_n) + var(--_m)));
  
  margin-left: ???; /* We're getting there! */
}

We can use mod(var(--_i),1) to test if --_i is an integer. If it’s an integer, the result is equal to 0. Otherwise, it’s equal to a value between 0 and 1.

We can introduce another variable and use the new if() function!

.container {
  --s: 120px;  /* size  */
  --g: 10px;   /* gap */
  
  container-type: inline-size; /* we make it a container to use 100cqw */
}
.container > * {
  --_n: round(down,(100cqw + var(--g))/(var(--s) + var(--g)));
  --_m: round(down,(100cqw - (var(--s) - var(--g))/2)/(var(--s) + var(--g))); 
  --_i: calc((sibling-index() - 1 + var(--_m))/(var(--_n) + var(--_m)));
  --_c: mod(var(--_i),1);
  margin-left: if(style(--_c: 0) calc((var(--s) + var(--g))/2) else 0;);
}

Tada!

It’s important to note that you need to register the variable --_c variable using @property to be able to do the comparison (I write more about this in “How to correctly use if()in CSS).

This is a good use case for if(), but we can do it differently:

--_c: round(down, 1 - mod(var(--_i), 1));

The mod() function gives us a value between 0 and 1, where 0 is the value we want. -1*mod() gives us a value between -1 and 0. 1 - mod() gives us a value between 0 and 1, but this time it’s the 1 we need. We apply round() to the calculation, and the result will be either 0 or 1. The --_c variable is now a Boolean variable that we can use directly within a calculation.

margin-left: calc(var(--_c) * (var(--s) + var(--g))/2);

If --_c is equal to 1, we get a margin. Otherwise, the margin is equal to 0. This time you don’t need to register the variable using @property. I personally prefer this method as it requires less code, but the if() method is also interesting.

Should I remember all those formulas by heart?! It’s too much!

No, you don’t. I tried to provide a detailed explanation behind the math, but it’s not mandatory to understand it to work with the grid. All you have to do is update the variables that control the size and gap. No need to touch the part that set the left margin. We will even explore how the same code structure can work with more shapes!

More Examples

The common use case is a hexagon shape but what about other shapes? We can, for example, consider a rhombus and, for this, we simply adjust the code that controls the shape.

From this:

.container > * {
  aspect-ratio: cos(30deg);
  border-radius: 50% / 25%;
  corner-shape: bevel;
  margin-bottom: calc(var(--s)/(-4*cos(30deg)));
}

…to this:

.container > * {
  aspect-ratio: 1;
  border-radius: 50%;
  corner-shape: bevel;
  margin-bottom: calc(var(--s)/-2);
}

A responsive grid of rhombus shapes — with no effort! Let’s try an octagon:

.container > * {
  aspect-ratio: 1;
  border-radius: calc(100%/(2 + sqrt(2)));
  corner-shape: bevel;
  margin-bottom: calc(var(--s)/(-1*(2 + sqrt(2))));
}

Almost! For an octagon, we need to adjust the gap because we need more horizontal space between the items:

.container {
  --g: calc(10px + var(--s)/(sqrt(2) + 1));
  gap: 10px var(--g);
}

The variable --g includes a portion of the size var(--s)/(sqrt(2) + 1) and is applied as a row gap, while the column gap is kept the same (10px).

From there, we can also get another type of hexagon grid:

And why not a grid of circles as well? Here we go:

As you can see, we didn’t touch the complex calculation that sets the left margin in any of those examples. All we had to do was to play with the border-radius and aspect-ratio properties to control the shape and adjust the bottom margin to rectify the overlap. In some cases, we need to adjust the horizontal gap.

Conclusion

I will end this article with another demo that will serve as a small homework for you:

This time, the shift is applied to the odd rows rather than the even ones. I let you dissect the code as a small exercise. Try to identify the change I have made and what’s the logic behind it (Hint: try to redo the calculation steps using this new configuration.)


Responsive Hexagon Grid Using Modern CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/ROG5TAi
via IFTTT

Thursday, January 22, 2026

Mostly Cloudy today!



With a high of F and a low of 3F. Currently, it's 18F and Clear outside.

Current wind speeds: 6 from the East

Pollen: 0

Sunrise: January 22, 2026 at 08:05PM

Sunset: January 23, 2026 at 06:00AM

UV index: 0

Humidity: 53%

via https://ift.tt/TASMLrh

January 23, 2026 at 10:02AM

Wednesday, January 21, 2026

Clear today!



With a high of F and a low of 8F. Currently, it's 22F and Clear outside.

Current wind speeds: 7 from the Northeast

Pollen: 0

Sunrise: January 21, 2026 at 08:06PM

Sunset: January 22, 2026 at 05:59AM

UV index: 0

Humidity: 52%

via https://ift.tt/982lyT7

January 22, 2026 at 10:02AM

I Learned The First Rule of ARIA the Hard Way

Some time ago, I shipped a component that felt accessible by every measure I could test. Keyboard navigation worked. ARIA roles were correctly applied. Automated audits passed without a single complaint. And yet, a screen reader user couldn’t figure out how to trigger it. When I tested it myself with keyboard-only navigation and NVDA, I saw the same thing: the interaction simply didn’t behave the way I expected.

Nothing on the checklist flagged an error. Technically, everything was “right.” But in practice, the component wasn’t predictable. Here’s a simplified version of the component that caused the issue:

As you can see in the demo, the markup is not at all complicated:

<button class="cta" role="link">Save changes</button>

And the fix was much easier than expected. I had to delete the ARIA role attribute that I had added with the best intentions.

The markup is even less complicated than before:

<button class="cta">Save changes</button>

That experience changed how I think about accessibility. The biggest lesson was this: Semantic HTML does a lot more accessibility work than we usually give it credit for already — and ARIA is simple to abuse when we use it both as a shortcut and as a supplement.

Many of us already know the first rule of ARIA: don’t use it. Well, use it. But not if the accessible benefits and functionality you need are already baked in, which it was in my case before adding the role attribute.

Let me outline exactly what happened, step-by-step, because I think the my error is actually a pretty common practice. There are many articles out there that say exactly what I’m saying here, but I think it often helps to internalize it by hearing it through a real-life experience.

Note: This article was tested using keyboard navigation and a screen reader (NVDA) to observe real interaction behavior across native and ARIA-modified elements.

1: Start with the simplest possible markup

Again, this is merely a minimal page with a single native <button> and no ARIA. And by default, it allows keyboard focus and demonstrates the functionality of using Tab, Enter, and Space out of the box. Geoff recently made this case when explaining the accessibility benefits of semantic HTML elements.

If the interaction triggers an action, then that element is a button. And in this case, the <button> is designed to run a script that saves changes to a user profile:

<button>Save changes</button>

That single line gives us a surprising amount for free:

  • Keyboard activation with the Enter and Space keys
  • Correct focus behavior
  • A role that assistive technology already understands
  • Consistent announcements across screen readers

At this point, there is no ARIA — and that’s intentional. But I did have an existing class for styling buttons in my CSS, so I added that:

<button class="cta">Save changes</button>

2: Observe the native behavior before adding anything

With just the native element in place, I tested three things:

  1. Keyboard only (Tab, Enter, Space)
  2. A screen reader (listening to how the control is announced)
  3. Focus order within the page

Everything behaved predictably. The browser was doing exactly what users expect. This step matters because it establishes a baseline. If something breaks later, you know it wasn’t HTML that caused it. In fact, we can see that everything is in perfect working order by inspecting the element in DevTool’s Accessibility panel.

DevTools Accessibility panel showing the accessible role of button with a label of Semantic Button.

3: Add well‑intentioned ARIA

The problem crept in when I tried to make the button behave like a link:

<button class="cta" role="link">Save changes</button>

I did this for styling and routing reasons. This button needed to be styled a little differently than the default .cta class and I figured I could use the ARIA attribute rather than using a modifier class. You can start to see how I let the styling dictate and influence the functionality. A <button> is still the correct element for this purpose, but I wanted it to look like a link because of the design requirements. Might as well give that element a link role then, right?

On the surface, nothing seemed broken. Automated tools stayed quiet. But in real use, the cracks showed quickly:

  • Space no longer activated the control reliably.
  • Screen readers announced conflicting roles.
  • Keyboard users encountered behavior that didn’t fully match either a button or a link.

ARIA didn’t add clarity here; it introduced ambiguity. But I had already “tested” my work and nothing was screaming at me that I’d conflated the element’s role with another type of element. Again, all it takes is a quick look at DevTools.

DevTools Accessibility panel showing the accessible role of link with a label of Semantic Button.

4: Back to semantics

The fix wasn’t clever. It was subtractive. I reverted my styles, used a class for styling, and went back to the semantic markup prior to changing the accessible role:

<button class="cta">Save changes</button>

I know it sounds easy: if it’s an action, use a <button>. If it takes you somewhere, use a link (<a>). But, in practice, we’re making decisions with every key we type and it’s just as easy to conflate actions with destinations. In this case, I totally used the correct element! My mistake was thinking that ARIA was an appropriate styling hook for my CSS.

Once the correct element was in place — absent of ARIA — the issues disappeared. Instead, I could define a new classname and, you guessed it, use keep styles with styles.

<button class="cta cta-alt">Save changes</button>

Just like that, I was able to style the element how I needed and the user who report the issue was able to confirm that everything worked as expected. It was an inadvertent mistake born of a basic misunderstanding about ARIA’s place in the stack.

Why this keeps happening

ARIA attributes are used to define the nature of something but they do not redefine the behavioral default of the native elements. When we override semantics, we quietly take responsibility for:

  • keyboard interactions,
  • focus management,
  • expected announcements, and
  • platform‑specific quirks.

That’s a large surface area to maintain, and it’s why small ARIA changes can have outsized and unpredictable effects.

A rule I now follow

Here’s the workflow that has saved me the most time and bugs:

  1. Use native HTML to express intent.
  2. Test with keyboard and a screen reader.
  3. Add ARIA only to communicate missing state, not to redefine roles.

If ARIA feels like it’s doing heavy lifting, it’s usually a sign the markup is fighting the browser.

Where ARIA does belong

One example would be a simple disclosure widget using a native <button> plus aria-expanded to communicate state — showing ARIA used to add state, not replace semantics.

This demo uses a native <button> with aria-expanded, which is toggled with a sprinkle of JavaScript:

const button = document.getElementById("toggle");
const panel = document.getElementById("panel");

button.addEventListener("click", () => {
  const expanded = button.getAttribute("aria-expanded") === "true";
  button.setAttribute("aria-expanded", !expanded);
  panel.hidden = expanded;
});

The accessible state (true/false) is communicated correctly without replacing the button’s semantics.

Now, I know that ARIA is essential when:

  • communicating expanded or collapsed state,
  • announcing dynamic updates,
  • building truly custom widgets, and
  • exposing relationships HTML can’t express.

Used this way, ARIA complements semantic HTML instead of competing with it.

Let the platform work for you

The biggest accessibility improvement I’ve made wasn’t learning about more attributes — it was trusting the ones browsers already understand. Semantic HTML is not the baseline you move past. It’s the foundation that everything else depends on.

And that’s what I really hope you take away from my experience. We all make mistakes. It’s part of the job, unfortunately. But what good are they if we can’t learn from them, even if it takes a hard lesson.


I Learned The First Rule of ARIA the Hard Way originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/9P3JM5A
via IFTTT

Tuesday, January 20, 2026

Mostly Clear/Wind today!



With a high of F and a low of 15F. Currently, it's 32F and Clear/Wind outside.

Current wind speeds: 20 from the North

Pollen: 0

Sunrise: January 20, 2026 at 08:07PM

Sunset: January 21, 2026 at 05:58AM

UV index: 0

Humidity: 48%

via https://ift.tt/uZhDqe2

January 21, 2026 at 10:02AM

Monday, January 19, 2026

Clear today!



With a high of F and a low of 14F. Currently, it's 26F and Clear outside.

Current wind speeds: 14 from the Southwest

Pollen: 0

Sunrise: January 19, 2026 at 08:07PM

Sunset: January 20, 2026 at 05:57AM

UV index: 0

Humidity: 67%

via https://ift.tt/7PTOJL9

January 20, 2026 at 10:02AM

Sunday, January 18, 2026

Snow today!



With a high of F and a low of 18F. Currently, it's 32F and Snow Shower outside.

Current wind speeds: 14 from the Northeast

Pollen: 0

Sunrise: January 18, 2026 at 08:08PM

Sunset: January 19, 2026 at 05:56AM

UV index: 0

Humidity: 52%

via https://ift.tt/FsZoIdT

January 19, 2026 at 10:02AM

Saturday, January 17, 2026

Clear today!



With a high of F and a low of 13F. Currently, it's 18F and Partly Cloudy outside.

Current wind speeds: 9 from the Southwest

Pollen: 0

Sunrise: January 17, 2026 at 08:08PM

Sunset: January 18, 2026 at 05:55AM

UV index: 0

Humidity: 33%

via https://ift.tt/xvmKF9J

January 18, 2026 at 10:02AM

Friday, January 16, 2026

Mostly Cloudy/Wind today!



With a high of F and a low of 13F. Currently, it's 18F and Partly Cloudy outside.

Current wind speeds: 13 from the North

Pollen: 0

Sunrise: January 16, 2026 at 08:08PM

Sunset: January 17, 2026 at 05:54AM

UV index: 0

Humidity: 31%

via https://ift.tt/JYk0Gpd

January 17, 2026 at 10:02AM

HTTP Archive 2025 Web Almanac

I love me some good web research reports. I’m a sucker for them. HTTP Archive’s Web Almanac is one report I look forward to every year, and I know I’m not alone there. It’s one of those highly-anticipated publications on the state of the web, chock-full of well-documented findings about millions of live websites — 17.2 million in this edition! — from page content, to performance, to accessibility, to UX, to… well, let’s just get to it.

It just came out, so there’s no way I’ve read through all 15 chapters, let alone digested and reflected on everything in it. Really, I just want you to be aware that it’s out. That said, it’s hard for me to resist sharing at least a few notable stats that hit me and that I’ll be sure to dig into.

Some highlights:

  • New text-wrap values are showing up! It’s small, but not surprising for features that only shipped as far back as 2023. Specifically, I’m looking at the balance (2.67%) and pretty (1.71%) values.
  • Variable fonts are no longer a novelty. “How popular are variable fonts? This year, 39.4% of desktop websites and 41.3% of mobile websites used at least one variable font on their pages. In other words, now about 4 in 10 sites are using variable fonts.”
  • Why can’t we nail down color contrast?! Only 30% of sites meet WCAG guidelines, and though that’s a number that’s trending up (21% in 2020), that’s a sorry stat.
  • Removing focus styles is an epidemic. A whopping 67% of sights remove focus outlines despite WCAG’s requirement that “Any keyboard operable user interface has a mode of operation where the keyboard focus indicator is visible.”
  • Many images are apparently decorative. At least, that’s what 30% of sites are suggesting by leaving the alt attribute empty. But if we consider that 14% of sites leave off the attribute completely, we’re looking at roughly 44% of sites that aren’t describing their visual content. On that note, your images probably are not decorative.
  • ARIA labels are everywhere. We’re looking at 70% usage (29% on buttons). This doesn’t mean anything in and of itself. It could be a good thing, but could also be an issue without proper usage.
  • The CMS landscape is largely unchanged. I mean, WordPress is still the dominant force, and that’s no dang surprise. At this point, its expansion wavers between a couple percentage points every year. “These changes suggest that WordPress is shifting from a focus on expansion to one on stabilization.” That’s a good thing.
  • Bloat, bloat, bloat. “In July 2015, the median mobile home page was a meager 845 KB. As of July 2025, the same median page is now 2,362 KB. The page decade brought a 202.8% increase.” In a perfect world where we’re all super conscious about page weight, I’d say we oughta aim for less than half that total.
  • JavaScript be heavy. Images are heaviest, of course, but 697 KB of JavaScript is a lot to stomach. That massive growth in page weight since 2015 is more support that this was a lost decade we must reckon with.

HTTP Archive 2025 Web Almanac originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/JYKvO3F
via IFTTT

“I Heart CSS” DailyDev Squad

If you’re reading this, chances are you already have some sort of way that you’re following when we publish new content, whether that’s RSS, Bluesky, Mastodon, or what have you. But I know a lot of folks like to use DailyDev as well and, if that’s you, we have a couple of ways you can get our stuff there as well. There’s our channel that automatically pulls in new content. There’s also a community page — what DailyDev calls a “squad” — where we curate our content as well as other interesting CSS-y links of interest, called I Heart CSS.

See you there?


“I Heart CSS” DailyDev Squad originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/OXDFVy1
via IFTTT

Thursday, January 15, 2026

Partly Cloudy/Wind today!



With a high of F and a low of 21F. Currently, it's 36F and Clear/Wind outside.

Current wind speeds: 25 from the Northwest

Pollen: 0

Sunrise: January 15, 2026 at 08:09PM

Sunset: January 16, 2026 at 05:53AM

UV index: 0

Humidity: 39%

via https://ift.tt/H15YnUQ

January 16, 2026 at 10:02AM

What’s !important #3: Popover Context Menus, @scope, New Web Platform Features, and More

Wednesday, January 14, 2026

Mostly Clear today!



With a high of F and a low of 30F. Currently, it's 31F and Fair outside.

Current wind speeds: 7 from the West

Pollen: 0

Sunrise: January 14, 2026 at 08:09PM

Sunset: January 15, 2026 at 05:52AM

UV index: 0

Humidity: 89%

via https://ift.tt/Kb3PUrO

January 15, 2026 at 10:02AM

Playing With CodePen slideVars

Super cool new CodePen feature alert! You’ve probably seen a bunch of “interactive” demos that let you changed values on the fly from a UI panel embedded directly in the demo. Jhey’s demos come immediately to mind, like this one:

That’s a tool called TweakPane doing the work. There’s another one called Knobs by Yair Even Or that Adam Argyle often uses:

I’ve often faked it with either the Checkbox Hack or a sprinkle of JavaScript when I’m demoing a very specific concept:

OK, enough examples because CodePen has a homegrown tool of its own called slideVars. All you have to do is import it and call it in the JavaScript panel:

import { slideVars } from "@codepen/slidevars";

slideVars.init();

You can import it into a project off CodePen if you’re so inclined.

That two-liner does a lot of lifting. It auto-detects CSS variables in your CSS and builds the panel for you, absolutely-positioned in the top-right corner:

It looks like you have to declare your variables on the :root element with default usage. I tried scoping them directly to the element and it was a no-go. It’s possible with a manual configuration, though.

Pretty cool, right? You can manually configure the input type, a value range, a default value, unit type, and yes, a scope that targets the element where the variables are defined. As far as units go, it supports all kinds of CSS numeric units. That includes unit-less values, though the documentation doesn’t explicitly say it. Just leave the unit property as an empty string ("").

I guess the only thing I’d like is to tell slideVars exactly what increments to use when manually configuring things. For example, unit-less values simply increment in integers, even if you define the default value as a decimal:

It works in default mode, however:

There’s a way to place the slideVars wherever you want by slapping a custom element where you want it in the HTML. It’s auto-placed at the bottom of the HTML <body> by default.

<slide-vars>
  <p>Custom Label!</p>
</slide-vars>

Or CSS it by selecting the custom element:

So much fun!


Playing With CodePen slideVars originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/HPSxanL
via IFTTT

Partly Cloudy today!

With a high of F and a low of 30F. Currently, it's 42F and Clear outside. Current wind speeds: 9 from the Southwest Pollen: 0 Su...