A Pure CSS Gallery Focus Effect with :not

Oftentimes in the past, I needed to figure out how to add styles to all elements inside the container but not the hovered one.

Animated GIF of a mouse cursor hovering over elements. The element the mouse cursor enters remains visible and the other elements fade.
Demo of the expected “fade-out” effect on siblings to let users “focus” on a particular element.

This effect requires selecting the siblings of a hovered element. I used to apply JavaScript for this, adding or removing the class that defined the proper CSS rules on mouseenter and mouseleave events, similar to this:

Although the code does the trick, my gut feeling always told me that there must be some pure-CSS way to achieve the same result. A few years ago, while working on a certain slider for my company, I came up with a solution similar to how Chris Geelhoed recreated the famous Netflix homepage animation and I understood that I didn’t need JavaScript for this anymore.

A couple of months ago I was trying to implement the same approach to a grid-based feed on my company website and — boom — it didn’t work because of the gap between the elements!

Luckily for me, it appeared that it doesn’t have to stay like this, and once again I didn’t need JavaScript for it.

Markup and base CSS

Let’s start coding by preparing the proper markup:

  • .grid is a grid-based <ul> list;
  • and .grid__child elements are <li> children that we want to interact with.

The markup looks like this:

<ul class="grid">
  <li class="grid__child"></li>
  <li class="grid__child"></li>
  <li class="grid__child"></li>
</ul>

The style should look like this:

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, 15rem);
  grid-gap: 1rem;
}

.grid__child {
  background: rgba(0, 0, 0, .1);
  border-radius: .5rem;
  aspect-ratio: 1/1;
}

This example code will create three list items occupying three columns in a grid.

The power of CSS selectors

Now, let’s add some interactivity. The approach that I initially applied was based on two steps:

  1. hovering on the container should change the styles of all elements inside…  
  2. …except the one that cursor is hovering at the moment.

Let’s start with grabbing every child while the cursor is hovering over the container:

.grid:hover .grid__child {
  /* ... */
}

Secondly, let’s exclude the currently hovered item and reduce the opacity of any other child:

.grid:hover .grid__child:not(:hover) {
  opacity: 0.3;
}

And this would be perfectly enough for containers without gaps between the child elements:

Animated GIF of a mouse cursor interacting with elements that are not separated by any gaps.
Demo of a solution that works without gaps.

However, in my case, I couldn’t remove these gaps:

Animated GIF of a mouse cursor hovering over elements. However, when the mouse enters a gap between two elements, the effect ends as the mouse leaves the element.
Demo of the problem encountered when gaps are introduced.

When I was moving the mouse between the tiles all of the children elements were fading out.

Ignoring the gaps

We can assume that gaps are parts of the container that are not overlayed by its children. We don’t want to run the effect every time the cursor enters the container, but rather when it hovers over one of the elements inside. Can we ignore the cursor moving above the gaps then? 

Yes, we can, using pointer-events: none on the .grid container and bringing them back with pointer-events: auto on its children:

.grid {
  /* ... */
  pointer-events: none;
}

/* ... */

.grid__child {
  /* ... */
  pointer-events: auto;
}

Let’s just add some cool transition on opacity and we have a ready component:

It’s probably even cooler when we add more tiles and create a 2-dimensional layout:

The final CSS looks like this:

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, 15rem);
  grid-gap: 3rem;
  pointer-events: none;
}

.grid:hover .grid__child:not(:hover) {
  opacity: 0.3;
}

.grid__child {
  background: rgba(0, 0, 0, .1);
  border-radius: .5rem;
  aspect-ratio: 1/1;
  pointer-events: auto;
  transition: opacity 300ms;
}

With only 2 additional lines of code we overcame the gap problem!

Possible issues

Although it’s a compact solution, there are some situations where it might require some workarounds.

Unfortunately, this trick won’t work when you want the container to be scrollable, e.g., like in some kind of horizontal slider. The pointer-events: none style would ignore not only the hover event but all the others, too. In such situations, you can wrap the .grid in another container, like this:

<div class="container">
  <ul class="grid">
    <li class="grid__child"></li>
    <li class="grid__child"></li>
    <li class="grid__child"></li>
    <li class="grid__child"></li>
    <li class="grid__child"></li>
    <li class="grid__child"></li>
    <li class="grid__child"></li>
  </ul>
</div>

Summary

I strongly encourage you to experiment and try to find a simpler and more native approach for tasks that are usually expected to have some level of complexity. Web technologies, like CSS, are getting more and more powerful and by using out-of-the-box native solutions you can achieve great results without the need of maintaining your code and cede it to browser vendors.

I hope that you liked this short tutorial and found it useful. Thanks!

The author selected the Tech Education to receive a donation as part of the Write for DOnations program.


A Pure CSS Gallery Focus Effect with :not originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Did You Know About the :has CSS Selector?

File this under stuff you don’t need to know just yet, but I think the :has CSS selector is going to have a big impact on how we write CSS in the future. In fact, if it ever ships in browsers, I think it breaks my mental model for how CSS fundamentally works because it would be the first example of a parent selector in CSS.

Before I explain all that, let’s look at an example:

div:has(p) {
  background: red;
}

Although it’s not supported in any browser today, this line of CSS would change the background of a div only if it has a paragraph within it. So, if there’s a div with no paragraphs in it, then these styles would not apply.

That’s pretty handy and yet exceptionally weird, right? Here’s another example:

div:has(+ div) { 
  color: blue; 
}

This CSS would only apply to any div that directly has another div following it.

The way I think about :has is this: it’s a parent selector pseudo-class. That is CSS-speak for “it lets you change the parent element if it has a child or another element that follows it.” This is so utterly strange to me because it breaks with my mental model of how CSS works. This is how I’m used to thinking about CSS:

/* Not valid CSS, just an illustration */
.parent {
  .child {
    color: red;
  }
}

You can only style down, from parent to child, but never back up the tree. :has completely changes this because up until now there have been no parent selectors in CSS and there are some good reasons why. Because of the way in which browsers parse HTML and CSS, selecting the parent if certain conditions are met could lead to all sorts of performance concerns.

Putting those concerns aside though, if I just sit down and think about all the ways I might use :has today then I sort of get a headache. It would open up this pandora’s box of opportunities that have never been possible with CSS alone.

Okay, one last example: let’s say we want to only apply styles to links that have images in them:

a:has(> img) {
  border: 20px solid white;
}

This would be helpful from time to time. I can also see :has being used for conditionally adding margin and padding to elements depending on their content. That would be neat.

Although :has isn’t supported in browsers right now (probably for those performance reasons), it is part of the CSS Selectors Level 4 specification which is the same spec that has the extremely useful :not pseudo-class. Unlike :has, :not does have pretty decent browser support and I used it for the first time the other day:

ul li:not(:first-of-type) {
  color: red;
}

That’s great I also love how gosh darn readable it is; you don’t ever have to have seen this line of code to understand what it does.

Another way you can use :not is for margins:

ul li:not(:last-of-type) {
  margin-bottom: 20px;
}

So every element that is not the last item gets a margin. This is useful if you have a bunch of elements in a card, like this:

CSS Selectors Level 4 is also the same spec that has the :is selector that can be used like this today in a lot of browsers:

:is(section, article, aside, nav) :is(h1, h2, h3, h4, h5, h6) {
  color: #BADA55;
}

/* ... which would be the equivalent of: */
section h1, section h2, section h3, section h4, section h5, section h6, 
article h1, article h2, article h3, article h4, article h5, article h6, 
aside h1, aside h2, aside h3, aside h4, aside h5, aside h6, 
nav h1, nav h2, nav h3, nav h4, nav h5, nav h6 {
  color: #BADA55;
}

So that’s it! :has might not be useful today but its cousins :is and :not can be fabulously helpful already and that’s only a tiny glimpse — just three CSS pseudo-classes — that are available in this new spec.


The post Did You Know About the :has CSS Selector? appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

Weekly Platform News: Focus Rings, Donut Scope, More em Units, and Global Privacy Control

In this week’s news, Chrome tackles focus rings, we learn how to get “donut” scope, Global Privacy Control gets big-name adoption, it’s time to ditch pixels in media queries, and a snippet that prevents annoying form validation styling.

Chrome will stop displaying focus rings when clicking buttons

Chrome, Edge, and other Chromium-based browsers display a focus indicator (a.k.a. focus ring) when the user clicks or taps a (styled) button. For comparison, Safari and Firefox don’t display a focus indicator when a button is clicked or tapped, but do only when the button is focused via the keyboard.

The focus ring will stay on the button until the user clicks somewhere else on the page.

Some developers find this behavior annoying and are using various workarounds to prevent the focus ring from appearing when a button is clicked or tapped. For example, the popular what-input library continuously tracks the user’s input method (mouse, keyboard or touch), allowing the page to suppress focus rings specifically for mouse clicks.

[data-whatintent="mouse"] :focus {
  outline: none;
}

A more recent workaround was enabled by the addition of the CSS :focus-visible pseudo-class to Chromium a few months ago. In the current version of Chrome, clicking or tapping a button invokes the button’s :focus state but not its :focus-visible state. that way, the page can use a suitable selector to suppress focus rings for clicks and taps without affecting keyboard users.

:focus:not(:focus-visible) {
  outline: none;
}

Fortunately, these workarounds will soon become unnecessary. Chromium’s user agent stylesheet recently switched from :focus to :focus-visible, and as a result of this change, button clicks and taps no longer invoke focus rings. The new behavior will first ship in Chrome 90 next month.

The enhanced CSS :not() selector enables “donut scope”

I recently wrote about the A:not(B *) selector pattern that allows authors to select all A elements that are not descendants of a B element. This pattern can be expanded to A B:not(C *) to create a “donut scope.”

For example, the selector article p:not(blockquote *) matches all <p> elements that are descendants of an <article> element but not descendants of a <blockquote> element. In other words, it selects all paragraphs in an article except the ones that are in a block quotation.

The donut shape that gives this scope its name

The New York Times now honors Global Privacy Control

Announced last October, Global Privacy Control (GPC) is a new privacy signal for the web that is designed to be legally enforceable. Essentially, it’s an HTTP Sec-GPC: 1 request header that tells websites that the user does not want their personal data to be shared or sold.

The DuckDuckGo Privacy Essentials extension enables GPC by default in the browser

The New York Times has become the first major publisher to honor GPC. A number of other publishers, including The Washington Post and Automattic (WordPress.com), have committed to honoring it “this coming quarter.”

From NYT’s privacy page:

Does The Times support the Global Privacy Control (GPC)?

Yes. When we detect a GPC signal from a reader’s browser where GDPR, CCPA or a similar privacy law applies, we stop sharing the reader’s personal data online with other companies (except with our service providers).

The case for em-based media queries

Some browsers allow the user to increase the default font size in the browser’s settings. Unfortunately, this user preference has no effect on websites that set their font sizes in pixels (e.g., font-size: 20px). In part for this reason, some websites (including CSS-Tricks) instead use font-relative units, such as em and rem, which do respond to the user’s font size preference.

Ideally, a website that uses font-relative units for font-size should also use em values in media queries (e.g., min-width: 80em instead of min-width: 1280px). Otherwise, the site’s responsive layout may not always work as expected.

For example, CSS-Tricks switches from a two-column to a one-column layout on narrow viewports to prevent the article’s lines from becoming too short. However, if the user increases the default font size in the browser to 24px, the text on the page will become larger (as it should) but the page layout will not change, resulting in extremely short lines at certain viewport widths.

If you’d like to try out em-based media queries on your website, there is a PostCSS plugin that automatically converts min-width, max-width, min-height, and max-height media queries from px to em.

(via Nick Gard)

A new push to bring CSS :user-invalid to browsers

In 2017, Peter-Paul Koch published a series of three articles about native form validation on the web. Part 1 points out the problems with the widely supported CSS :invalid pseudo-class:

  • The validity of <input> elements is re-evaluated on every key stroke, so a form field can become :invalid while the user is still typing the value.
  • If a form field is required (<input required>), it will become :invalid immediately on page load.

Both of these behaviors are potentially confusing (and annoying), so websites cannot rely solely on the :invalid selector to indicate that a value entered by the user is not valid. However, there is the option to combine :invalid with :not(:focus) and even :not(:placeholder-shown) to ensure that the page’s “invalid” styles do not apply to the <input> until the user has finished entering the value and moved focus to another element.

The CSS Selectors module defines a :user-invalid pseudo-class that avoids the problems of :invalid by only matching an <input> “after the user has significantly interacted with it.”

Firefox already supports this functionality via the :-moz-ui-invalid pseudo-class (see it in action). Mozilla now intends to un-prefix this pseudo-class and ship it under the standard :user-invalid name. There are still no signals from other browser vendors, but the Chromium and WebKit bugs for this feature have been filed.


The post Weekly Platform News: Focus Rings, Donut Scope, More em Units, and Global Privacy Control appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

Weekly Platform News: The :not() pseudo-class, Video Media Queries, clip-path: path() Support

Hey, we’re back with weekly updates about the browser landscape from Šime Vidas.

In this week’s update, the CSS :not pseudo class can accept complex selectors, how to disable smooth scrolling when using “Find on page…” in Chrome, Safari’s support for there media attribute on <video> elements, and the long-awaited debut of the path() function for the CSS clip-path property.

Let’s jump into the news…

The enhanced :not() pseudo-class enables new kinds of powerful selectors

After a years-long wait, the enhanced :not() pseudo-class has finally shipped in Chrome and Firefox, and is now supported in all major browser engines. This new version of :not() accepts complex selectors and even entire selector lists.

For example, you can now select all <p> elements that are not contained within an <article> element.

/* select all <p>s that are descendants of <article> */
article p {
}

/* NEW! */
/* select all <p>s that are not descendants of <article> */
p:not(article *) {
}

In another example, you may want to select the first list item that does not have the hidden attribute (or any other attribute, for that matter). The best selector for this task would be :nth-child(1 of :not([hidden])), but the of notation is still only supported in Safari. Luckily, this unsupported selector can now be re-written using only the enhanced :not() pseudo-class.

/* select all non-hidden elements that are not preceded by a non-hidden sibling (i.e., select the first non-hidden child */
:not([hidden]):not(:not([hidden]) ~ :not([hidden])) {
}

The HTTP Refresh header can be an accessibility issue

The HTTP Refresh header (and equivalent HTML <meta> tag) is a very old and widely supported non-standard feature that instructs the browser to automatically and periodically reload the page after a given amount of time.

<!-- refresh page after 60 seconds -->
<meta http-equiv="refresh" content="60">

According to Google’s data, the <meta http-equiv="refresh"> tag is used by a whopping 2.8% of page loads in Chrome (down from 4% a year ago). All these websites risk failing several success criteria of the Web Content Accessibility Guidelines (WCAG):

If the time interval is too short, and there is no way to turn auto-refresh off, people who are blind will not have enough time to make their screen readers read the page before the page refreshes unexpectedly and causes the screen reader to begin reading at the top.

However, WCAG does allow using the <meta http-equiv="refresh"> tag specifically with the value 0 to perform a client-side redirect in the case that the author does not control the server and hence cannot perform a proper HTTP redirect.

(via Stefan Judis)

How to disable smooth scrolling for the “Find on page…” feature in Chrome

CSS scroll-behavior: smooth is supported in Chrome and Firefox. When this declaration is set on the <html> element, the browser scrolls the page “in a smooth fashion.” This applies to navigations, the standard scrolling APIs (e.g., window.scrollTo({ top: 0 })), and scroll snapping operations (CSS Scroll Snap).

Unfortunately, Chrome erroneously keeps smooth scrolling enabled even when the user performs a text search on the page (“Find on page…” feature). Some people find this annoying. Until that is fixed, you can use Christian Schaefer’s clever CSS workaround that effectively disables smooth scrolling for the “Find on page…” feature only.

@keyframes smoothscroll1 {
  from,
  to {
    scroll-behavior: smooth;
  }
}

@keyframes smoothscroll2 {
  from,
  to {
    scroll-behavior: smooth;
  }
}

html {
  animation: smoothscroll1 1s;
}

html:focus-within {
  animation-name: smoothscroll2;
  scroll-behavior: smooth;
}

In the following demo, notice how clicking the links scrolls the page smoothly while searching for the words “top” and “bottom” scrolls the page instantly.

Safari still supports the media attribute on video sources

With the HTML <video> element, it is possible to declare multiple video sources of different MIME types and encodings. This allows websites to use more modern and efficient video formats in supporting browsers, while providing a fallback for other browsers.

<video>
  <source src="/flower.webm" type="video/webm">
  <source src="/flower.mp4" type="video/mp4">
</video>

In the past, browsers also supported the media attribute on video sources. For example, a web page could load a higher-resolution video if the user’s viewport exceeded a certain size.

<video>
  <source media="(min-width: 1200px)" src="/large.mp4" type="video/mp4">
  <source src="/small.mp4" type="video/mp4">
</video>

The above syntax is in fact still supported in Safari today, but it was removed from other browsers around 2014 because it was not considered a good feature:

It is not appropriate for choosing between low resolution and high resolution because the environment can change (e.g., the user might fullscreen the video after it has begun loading and want high resolution). Also, bandwidth is not available in media queries, but even if it was, the user agent is in a better position to determine what is appropriate than the author.

Scott Jehl (Filament Group) argues that the removal of this feature was a mistake and that websites should be able to deliver responsive video sources using <video> alone.

For every video we embed in HTML, we’re stuck with the choice of serving source files that are potentially too large or small for many users’ devices … or resorting to more complicated server-side or scripted or third-party solutions to deliver a correct size.

Scott has written a proposal for the reintroduction of media in video <source> elements and is welcoming feedback.

The CSS clip-path: path() function ships in Chrome

It wasn’t mentioned in the latest “New in Chrome 88” article, but Chrome just shipped the path() function for the CSS clip-path property, which means that this feature is now supported in all three major browser engines (Safari, Firefox, and Chrome).

The path() function is defined in the CSS Shapes module, and it accepts an SVG path string. Chris calls this the ultimate syntax for the clip-path property because it can clip an element with “literally any shape.” For example, here’s a photo clipped with a heart shape:


The post Weekly Platform News: The :not() pseudo-class, Video Media Queries, clip-path: path() Support appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS :not() with Multiple Classes

Say you want to select an element when it doesn't have a certain class. That's what the :not() selector is for.

body:not(.home) {
  
}

But what if there are multiple classes you want to avoid?

There are no logical combinators with :not(), like and or or, but you can chain them, which is effectively like and.

body:not(.home):not(.away):not(.page-50) {
  
}

The :not() selector doesn't add any specificy by itself, but what is inside does, so :not(.foo) adds the same weight as .foo does.

The post CSS :not() with Multiple Classes appeared first on CSS-Tricks.