CSS Full-Bleed Scroll-Snapping Carousel with Centered Content and Visible Overflow

For a project I’m working at, we’re building in some carousels (aka content sliders). While building a scroll-snapping carousel in itself is no rocket science nowadays, there was an extra variation that made my eyes sparkle: a full-bleed carousel that spans the entire page, with its focussed slide anchored to the center column of the page, and with the other slides “overflowing” outside of that column.

~

To jump right in, let’s take a look at the final result:

The first carousel shown is a regular/typical one: all slide-wrappers (.slidewrapper) are as big as the carousel (.slider) itself. They slide-wrappers are laid out on one row using CSS Flexbox. Because they have the same size, the carousel will only show one slide after having scroll-snapped.

The second carousel is the variation I mentioned in the intro: it also visually aligns the focussed slide with the center column — just like the first carousel — but the carousel itself does span the entire width of the page. This leaves the non-centered slides of the carousel also visible, making it look like they’re overflowing outside of the center column.

The solution to this problem uses the same markup and CSS, but with some CSS additions. As it’s not as easy as simply adding overflow: visible; to the .slider — as that would remove the scroll container — I adjusted the CSS as follows:

  1. The carousel-wrapper (.slider) spans the entire width of the page by means of the .full-bleed utility class.
  2. There’s a gap between each .slidewrapper. This is done using margin-right instead of gap, as older browsers don’t support the property in combination with Flexbox.

And now, the key part:

  1. The first .slidewrapper is given a padding on its left edge so that it appears centered on load. The last .slidewrapper is given a padding on its right edge so that it appears centered watching the last item. To make this work, the .slidewrapper elements need box-sizing: content-box; to be applied.

💡 An alternative approach would be add this padding to the .slider itself, which is an approach I took in my Pure CSS Cover Flow with Scroll-Timeline demo.

To grasp this padding-trick better and to see what’s going on, I’ve added a “Show Debug Outlines” option in the CodePen embed above. Take a close look at the green boxes that wrap the first and last slide contents.

🗺 Legend:
  • Slider Wrapper / Carousel Container = .slider = hotpink
  • Slide Wrapper = .slidewrapper = lime
  • Slide Contents .slide = grey

I like the fact that I was able to re-use the same markup for both variations, with their only difference being the .full-bleed class. Nice and DRY 🙂

~

🔥 Like what you see? Want to stay in the loop? Here's how:

Create a color theme with CSS Relative Color Syntax, CSS color-mix(), and CSS color-contrast()

Fabio Giolito explores three new CSS color features that landed in Safari Technology Preview:

  1. Relative color syntax, e.g.

    .bg-primary-100 {
      background-color: hsl(from var(--theme-primary) h s 90%);
    }
    .bg-primary-200 {
      background-color: hsl(from var(--theme-primary) h s 80%);
    }
    .bg-primary-300 {
      background-color: hsl(from var(--theme-primary) h s 70%);
    }
    ...
  2. CSS color-contrast, e.g.

    .text-contrast-primary {
      color: color-contrast(var(--theme-primary) vs white, black);
    }
  3. CSS color-mix, e.g.

    .text-primary-dark {
      color: color-mix(var(--theme-primary), black 10%);
    }
    .text-primary-darker {
      color: color-mix(var(--theme-primary), black 20%);
    }

All three features are part of the the CSS Color Module Level 5 spec and are a very welcome addition.

Create a color theme with these upcoming CSS features →

A first look at CQFill, a Polyfill for CSS Container Queries

Jonathan Neal just announced that he has been working on a polyfill for CSS Container Queries. Let’s take a look at how it works …

🤔 Container Queries?

Container Queries allow authors to style elements according to the size of a container. This is similar to a @media query, except that it evaluates against a container instead of the viewport.

~

Unfortunately the polyfill is not a simple drop-in that will work with your existing CSS code. This is because rendering engines that don’t support Container Queries will discard those specific statements and declarations.

To work around this, the polyfill requires you to duplicate some CSS with an alternative syntax.

  1. Duplicate the value for the contain property into a CSS Custom Property named --css-contain
  2. Duplicate the @container rule as an @media rule bearing the text --css-container

Like so:

/* Create a Container Root */
.container {
  contain: layout inline-size; /* For browsers that support Container Queries */
  --css-contain: layout inline-size; /* For the polyfill to use */
}
/* Container Query */
@container (min-width: 700px) { /* For browsers that support Container Queries */
  .contained {
    /* … */
  }
}

@media --css-container and (min-width: 700px) { /* For the polyfill to use */
  .contained {
    /* … */
  }
}

As those duplicated rules are valid CSS, browsers won’t discard them and the polyfill can pick them up 🙂

⚠️ It’s very import to use the naming as used in the code above. The Custom Property must be named --css-contain and the Media Query must contain the text --css-container. If named differently, the polyfill won’t be able to pick them up.

~

Once your styles have been declared you can import the polyfill and call it:

import { cqfill } from "https://cdn.skypack.dev/cqfill";

cqfill();

If you want a local copy of CQFill, you can install it per NPM/Yarn.

npm install cqfill

☝️ When using Next.js or PostCSS you don’t even need to call the polyfill, as the CQFill repo includes plugins for those.

~

Here’s my original demo, adjusted to include the polyfill:

See the Pen CSS Container Queries Demo (with Polyfill) by Bramus (@bramus) on CodePen.

Great work Jonathan, works like a charm!

CQFill: CSS Container Queries Polyfill (GitHub) →

~

🔥 Like what you see? Want to stay in the loop? Here's how:

Float an Element to the Bottom Corner

Temani Afif shares this clever trick to float an element to the bottom corner of a container.

The solution is threefold:

  1. Float a full-height wrapper (which contains the image) to the right
  2. Use flexbox to place the image at the bottom inside that wrapper
  3. Use shape-outside to clip the wrapper

Clever!

Float an Element to the Bottom Corner →

☝️ Did you know that to resize the container in the pen above, only one line of CSS is needed?!

Identify and Extract Pseudo-Element Selectors from built-in HTML Elements using DevTools

Recently Stefan Judis shared how to style the browse button of a file selector using the ::file-selector-button pseudo-element

But what about other complex elements in the browser? How can we tweak individual parts of those? Take <audio> for example: is there a pseudo-element we can use to style the play button?

~

Table of Contents

  1. Dissecting <input type="file" />
  2. How to use DevTools to peek inside <input type="file" />
  3. Dissecting <audio>
  4. Is there a catch?
  5. One last thing

~

# Dissecting <input type="file" />

Before we answer the question above, let’s first take a look at the <input type="file" /> Stefan used. Below is screenshot of how it’s rendered in Chromium on Mac, with a few extra outlines added.

Even though we only type 1 element in our HTML code, we see it consists of two parts:

  1. A “Choose File” button
  2. A label reading “No file chosen”

Internally, this is also how the browser builds it. We type in:

<input type="file" />

But what’s being rendered is this:

<input type="button" value="Choose file" pseudo="-webkit-file-upload-button" id="file-upload-button">
<span aria-hidden="true">No file chosen</span>

(I’ll show you further down this post how I know this 😉)

The ::file-selector-button selector Stefan mentioned targets only the <input type="button" …> you see there. Using it you can style the button, like he did.

🧐 If you’re paying close attention you might notice that the pseudo-element used is ::file-selector-button, whilst the pseudo attribute of the button reads -webkit-file-upload-button. More on that further down the post 😉

~

# How to use DevTools to peek inside <input type="file" />

In the Chromium DevTools we don’t get to see the two elements that make up <input type="file" />. That’s because this information as is hidden in the Shadow DOM.

Thankfully there is a way to have DevTools show them. To do so, open DevTools’ Settings, and under Elements check the option that reads “Show user agent Shadow DOM”.

🦊 Firefox or 🧭 Safari user? The DevTools in Firefox/Safari have this option enabled out of the box.

Once enabled you’ll see #shadow-root (user-agent) appear in the Elements Tree for all elements that are built that way (and there quite a few!).

<input type="file">
  ↳ #shadow-root (user-agent)
      <input type="button" value="Choose file" pseudo="-webkit-file-upload-button" id="file-upload-button">
      <span aria-hidden="true">No file chosen</span>
</input>

☝️ Having this option enabled all the time can be quite distracting. I personally only turn it on when I need it.

~

# Dissecting <audio>

Winging back to our initial question “is there a pseudo-element we can use to style the play button?”, we can use the DevTools to see the underlying structure.

In Chromium we get back this structure:

<audio controls="" src="/media/cc0-audio/t-rex-roar.mp3">
  ↳ #shadow-root (user-agent)
    <div pseudo="-webkit-media-controls" class="phase-ready state-stopped">
      <div pseudo="-webkit-media-controls-overlay-enclosure">
        <input pseudo="-internal-media-controls-overlay-cast-button" type="button" aria-label="play on remote device" style="display: none;">
      </div>
      <div pseudo="-webkit-media-controls-enclosure">
        <div pseudo="-webkit-media-controls-panel">
          <input type="button" pseudo="-webkit-media-controls-play-button" aria-label="play" class="pause" style="">
          <div aria-label="elapsed time: 0:00" pseudo="-webkit-media-controls-current-time-display" style="">0:00</div>
          <div aria-label="total time: / 0:02" pseudo="-webkit-media-controls-time-remaining-display" style="">/ 0:02</div>
          <input type="range" step="any" pseudo="-webkit-media-controls-timeline" max="2.115918" aria-label="audio time scrubber 0:00 / 0:02" aria-valuetext="elapsed time: 0:00">
          <div pseudo="-webkit-media-controls-volume-control-container" class="closed" style="">
            <div pseudo="-webkit-media-controls-volume-control-hover-background"></div>
            <input type="range" step="any" max="1" aria-valuemax="100" aria-valuemin="0" aria-label="volume" pseudo="-webkit-media-controls-volume-slider" aria-valuenow="100" class="closed" style=""><input type="button" pseudo="-webkit-media-controls-mute-button" aria-label="mute" style="">
          </div>
          <input type="button" pseudo="-webkit-media-controls-fullscreen-button" aria-label="enter full screen" style="display: none;">
          <input type="button" aria-label="show more media controls" title="more options" pseudo="-internal-media-controls-overflow-button" style="">
        </div>
      </div>
      <div role="menu" aria-label="Options" pseudo="-internal-media-controls-text-track-list" style="display: none;"></div>
      <div pseudo="-internal-media-controls-overflow-menu-list" role="menu" class="closed" style="display: none;">
        <label pseudo="-internal-media-controls-overflow-menu-list-item" role="menuitem" tabindex="0" aria-label=" Play " style="display: none;">
          <input type="button" pseudo="-webkit-media-controls-play-button" tabindex="-1" aria-label="play" class="pause" style="display: none;">
          <div aria-hidden="true">
            <span>Play</span>
          </div>
        </label>
        <label pseudo="-internal-media-controls-overflow-menu-list-item" role="menuitem" tabindex="0" aria-label="enter full screen Full screen " style="display: none;">
          <input type="button" pseudo="-webkit-media-controls-fullscreen-button" aria-label="enter full screen" tabindex="-1" style="display: none;">
          <div aria-hidden="true">
            <span>Full screen</span>
          </div>
        </label>
        <label pseudo="-internal-media-controls-overflow-menu-list-item" role="menuitem" tabindex="0" aria-label="download media Download " class="animated-0" style="">
          <input type="button" aria-label="download media" pseudo="-internal-media-controls-download-button" tabindex="-1" style="">
          <div aria-hidden="true">
            <span>Download</span>
          </div>
        </label>
        <label pseudo="-internal-media-controls-overflow-menu-list-item" role="menuitem" tabindex="0" aria-label=" Mute " class="animated-2" style="display: none;">
          <input type="button" pseudo="-webkit-media-controls-mute-button" tabindex="-1" aria-label="mute" style="display: none;">
          <div aria-hidden="true">
            <span>Mute</span>
          </div>
        </label>
        <label pseudo="-internal-media-controls-overflow-menu-list-item" role="menuitem" tabindex="0" aria-label="play on remote device Cast " class="animated-1" style="display: none;">
          <input pseudo="-internal-media-controls-cast-button" type="button" aria-label="play on remote device" tabindex="-1" style="display: none;">
          <div aria-hidden="true">
            <span>Cast</span>
          </div>
        </label>
        <label pseudo="-internal-media-controls-overflow-menu-list-item" role="menuitem" tabindex="0" aria-label="show closed captions menu Captions " class="animated-0" style="display: none;">
          <input aria-label="show closed captions menu" type="button" pseudo="-webkit-media-controls-toggle-closed-captions-button" tabindex="-1" style="display: none;">
          <div aria-hidden="true">
            <span>Captions</span>
          </div>
        </label>
      </div>
    </div>
</audio>

Or visually, with outlines added:

With a bit of digging we can find the the play button on line #9, and extract its pseudo attribute.

<input type="button" pseudo="-webkit-media-controls-play-button" aria-label="play" class="pause" style="">

👉 To style the play button we can use ::-webkit-media-controls-play-button

~

# Is there a catch?

While the ::-webkit-media-controls-play-button selector above works, there’s a catch though: it only works in Chromium based browsers, and this for several reasons:

  1. Every browser engine has its own implementation for what makes up an <audio> element. Shown below is a comparison of the UI for the <audio> as seen in Firefox, Chromium, and Safari.

    Whilst all implementation contain a play button, not all — for example — contain an element that indicates the current time. Styling that wouldn’t be possible.

  2. Not all browser expose the same parts of the UI using pseudo-elements. When it comes to <audio> for example, only Chromium exposes parts of its UI. Firefox and Safari don’t expose any pseudo-element for <audio>

    And even if they would, the wouldn’t use ::-webkit-media-controls-play-button for it.

Another aspect to take into account is that ::-webkit-media-controls-play-button is something that Chromium decided to use. This wasn’t discussed with any other browser vendor. As Thomas Steiner warns:

~

# One last thing

To close off I still owe you an explanation to why Chromium lists ::-webkit-file-upload-button for the browse button of <input type="file" />, instead of ::file-selector-button.

This is because they first exposed it using their own internal ::-webkit-file-upload-button name. It was only later that the CSS Working Group decided to standardize it to ::file-selector-button.

~

Did this help you out? Like what you see?
Thank me with a coffee.

I don't do this for profit but a small one-time donation would surely put a smile on my face. Thanks!

☕️ Buy me a Coffee (€3)

To stay in the loop you can follow @bramus or follow @bramusblog on Twitter.

Animating Text Underlines

Instead of resorting to faux underlines using injected content, Michelle Barker shares that we nowadays can animate the text-decoration-* properties to achieve similar (and better) results.

This approach however won’t work in Chromium, as only Firefox/Safari support animating text-underline-offset at the moment … but thankfully a tiny amount of CSS Houdini Magic can be sprinkled on top to make Chromium happy 🤩

See the Pen
Underlines (Chrome solution with Houdini)
by Michelle Barker (@michellebarker)
on CodePen.

Animating Text Underlines →

Container Queries are Actually Coming / Say Hello To CSS Container Queries

In addition to my first look + demo at Container Queries, both Andy Bell and Ahmad Shadeed have also published posts covering them.

~

Ahmad starts off with explaining the idea behind them, before digging into a ton of use-cases. I especially like the pagination use-case, something I hadn’t thought of yet myself.

~

In his post, Andy taps into a — not yet existentcw unit, where 1cw equals 1% of the container. This comes in handy for tweaking the font-size based on the available space.

/* Before */
h1 {
  font-size: clamp(
    var(--fluid-type-min, 1rem),
    calc(1rem + var(--fluid-type-target, 3vw)),
    var(--fluid-type-max, 1.3rem)
  );
}

/* After */
h1 {
  font-size: clamp(
    var(--fluid-type-min, 1rem),
    calc(1rem + var(--fluid-type-target, 5cw)),
    var(--fluid-type-max, 1.3rem)
  );
}

Yes, we totally need this type of unit!

~

Container Queries are Actually Coming (by Andy) →
Say Hello To CSS Container Queries (by Ahmad) →

Dark mode in 5 minutes, with inverted lightness variables

Lea Verou shows a method to implement dark mode, not by swapping entire colors, but by simply changing their lightness

The basic idea is to use custom properties for the lightness of colors instead of the entire color. Then, in dark mode, you override these variables with 100% - lightness. This generally produces light colors for dark colors, medium colors for medium colors, and dark colors for light colors, and still allows you to define colors inline, instead of forcing you to use a variable for every single color.

For best results she also taps into LCH colors.

LCH is a much better color space for this technique, because its lightness actually means something, not just across different lightnesses of the same color, but across different hues and chromas.

Dark mode in 5 minutes, with inverted lightness variables →

Style Pseudo-elements with Javascript Using Custom Properties

Over at CSS { In Real Life }, author Michelle Barker has detailed a clever way to style pseudo-elements (such as ::before and ::after) through JavaScript.

In Javascript we have a few ways of selecting elements, but we can’t directly target pseudo-elements. […] Luckily, CSS custom properties can help.

👉 If you set a custom property on the element that “owns” the pseudo-element the pseudo-element itself can pick it up, thus enabling a way to style it.

Quick Tip: Style Pseudo-elements with Javascript Using Custom Properties →

Debug/Inspect z-index stacking with the “CSS Stacking Context Inspector” DevTools extension

The Stacking Contexts Inspector is a DevTools extension for Google Chrome that allows you to analyse the stacking contexts available on a webpage. This extension will add a new panel to the DevTools and a new sidebar on the elements panel.

Handy for when you’re having stacking issues.

CSS Stacking Context Inspector →
Stacking Contexts Inspector →

☝️ If you’re running “Edgium”, you can use it’s built-in 3D View to visualize the stacking contexts.

Hat tip, Josh!