CSS – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Mon, 24 Oct 2022 12:59:17 +0000 en-US hourly 1 https://wordpress.org/?v=6.1.1 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 CSS – CSS-Tricks https://css-tricks.com 32 32 45537868 Is There Too Much CSS Now? https://css-tricks.com/is-there-too-much-css-now/ https://css-tricks.com/is-there-too-much-css-now/#comments Mon, 24 Oct 2022 12:59:16 +0000 https://css-tricks.com/?p=374364 As front-end developers, we’ve wished for a lot of things over the years — ways to center things in CSS, encapsulate styles, set an element’s aspect ratio, get finer-grained control over our colors, select an element based on its children’s …


Is There Too Much CSS Now? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
As front-end developers, we’ve wished for a lot of things over the years — ways to center things in CSS, encapsulate styles, set an element’s aspect ratio, get finer-grained control over our colors, select an element based on its children’s properties, manage layers of specificity, allow elements to respond to the width of their parents… the list goes on and on.

And now that we got all we wished for and more, some of us are asking — do we now have too much CSS?

The dark times

If you, like me, came up in web development during CSS’s infancy, the idea of having too much of it seems ludicrous.

Back in the days, virtually the entire job description of a front-end developer consisted of dealing with CSS’s limitations. The clearfix hack to clear floats, the 100% padding hack to make square divs, not to mention semi-randomly applying unrelated properties to trick Internet Explorer into doing your bidding.

At the time, the browser was a devious foe to be defeated through sheer cunning and arcane incantations. Today, the perfect property is waiting and just a copy-paste away on MDN.

The new era of CSS

But today things are vastly different: not only are things moving much faster, but browser vendors actually care about making developers happy! I know, I couldn’t believe it either. But I run the yearly State of CSS developer survey (which is open now by the way — go take it!) and I know for a fact that browser development teams use the survey results (among many other data points) to inform their roadmap.

Beyond this, Google has also helped finance my work on the survey, and even hired Lea Verou to take the lead on selecting this year’s survey questions.

It’s not just Google. It’s become fashionable to bash Safari and Apple in general (sometimes deservedly so), but you can’t deny how passionate someone like Jen Simmons is about listening to developers and improving the web.

And not only are browser vendors improving CSS on their own; they’re even collaborating across battle lines with initiatives such as Interop 2023 to help reduce inconsistencies and incompatibilities between browsers.

Too much of a good thing?

The result of all this is that we are now faced with an embarrassment of CSS riches, and it can be hard to catch up. CSS Grid started being supported in major browsers almost five years ago, yet I still check a reference every time I use it. And as cool as subgrid seems, I’ve yet to even try it out.

During the process of selecting which CSS features to include or not in the State of CSS, Lea and myself considered many features, but we also rejected quite a few. Some examples of the feature we didn’t include are:

  • The linear() easing function, which lets you define easing curves with more granularity. 
  • The env() function, which lets you use variables defined by the browser or device. 
  • The scrollbar-width property, which helps control a scrollbar’s appearance. 
  • The margin-trim property, which lets you control how a container’s children’s margins behave. 

These are all potentially very useful, and would’ve all been big news during the CSS drought of past years. But in today’s context they have to fight for attention with much larger announcements, like the has() selector or CSS nesting!

Not excited

As Silvestar Bistrović writes, he is “not that excited about all these new CSS features.” This found an echo on Twitter, with Sara Soueidan stating that what she cares about is “practicality, not how shiny a feature looks at the moment.”

This may seem like a negative attitude, but I think it’s understandable. Nobody can be expected to keep up with so many new features!

Another unintended (or maybe, intended?) consequence is that the more complex CSS becomes, the more it raises the bar for any new company wanting to develop a browser engine — to say nothing of the added workload when it comes to maintaining and documenting all these new features. 

CSS overreach

There’s also the very valid concern that CSS might be branching out into areas it’s not quite suitable to handle. That’s another thing Sara Soueidan pointed out when reacting to the new CSS Toggles experimental implementation (here’s a ticket discussing it):

Many have made the very reasonable point that this kind of behavior would be best handled by a new HTML element instead of managing toggle state purely through CSS, and that CSS may not be the best medium to ensure accessibility issues are properly addressed. 

When CSS takes over something that was previously handled through JavaScript, this is generally seen as a good thing, as it often reduces the amount of code the browser has to load. So, I’m cautiously optimistic about CSS Toggles and trust that the CSS Working Group will properly address the concerns of the community. But there may yet come a day when we start to worry that CSS may be expanding beyond its borders and encroaching on HTML and JavaScript’s responsibilities.

New expectations

And maybe this is what needs to change: maybe we should drop the expectation that CSS developers have to know all of CSS? 

This expectation stems from the days where CSS was an afterthought, that little annoying syntax you had to learn to make your button blue and bold just like the client asked. But I think we need to accept that today’s CSS might just be way too vast for a single person to master, especially in addition to other front-end duties.

As Michelle Barker puts it:

And that’s where I, myself, land in the end. I’ve made my peace with the fact that I will probably never use — or even know about — all possible CSS features. And this is coming from someone who runs a survey about CSS!

But these new features will surely be useful to someone. Someone will write blog posts about them, create cool CodePens with them, give talks about them. That person will be a cool, young, energetic developer who still have all their hair. In other words, it won’t be me — and that’s fine. 

And maybe you’re worried that this new developer will be overwhelmed by all the stuff they have to learn at once. But do keep in mind all the things they won’t have to learn about, precisely because it’s been replaced by these newer alternatives. I know I’d take that deal anytime.

But think about it: in the past couple years, not only have we seen a huge increase in the number of devices we need to cater to, we’ve also started to recognize that we all consume the web in slightly different manners, whether due to disabilities, current context, or just personal preferences. Shouldn’t CSS adapt to this new reality?

Now, I have to confess this has all made me feel a bit nostalgic… so excuse me while I go clear a couple floats, just for old time’s sake.


As I mentioned, the yearly State of CSS survey is now open. Whether you think there’s too much CSS or not, the survey is a great way to let browser developers know how you feel, so go fill it out if you have 10 minutes. 


Is There Too Much CSS Now? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/is-there-too-much-css-now/feed/ 17 374364
Committing CSS Crimes https://css-tricks.com/committing-css-crimes/ https://css-tricks.com/committing-css-crimes/#respond Tue, 09 Aug 2022 13:01:44 +0000 https://css-tricks.com/?p=367369 The time for CSS-Tricks is over. Now is the time for CSS Crimes!

In this current landscape of content service providers, users are often limited to expressing themselves in text, links, and images. Sanitization rules tend to strip out HTML, …


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

]]>
The time for CSS-Tricks is over. Now is the time for CSS Crimes!

In this current landscape of content service providers, users are often limited to expressing themselves in text, links, and images. Sanitization rules tend to strip out HTML, JavaScript, and various attributes.

Social media service Cohost allows users to have greater freedom with markup and inline styles than we may be typically used to. Some users have taken advantage of this freedom to commit CSS Crimes! It has resulted in creative recreations of familiar interfaces and interactive games by using properties in unconventional ways.

Blackle Mori created a contraption where pulling a handle slowly turns a series of gears, pulleys, and chains. Eventually an aperture opens to reveal the site’s mascot (“eggbug”) and the proclamation “Good Job!”. I have stared at this in Developer Tools and it is an amazing combination of grid, resize, transform, and calc(). I ended up adding a border to all <div>s to try and get a better understanding of how each individual element moved.

There have been situations in the past where I have been restricted from using the full toolkit of HTML, JavaScript, and CSS. There have been many instances of using decorative CSS shapes to get around images. I have used :hover as a workaround for mouseenter and mouseleave. I have used input:checked as a sibling selector for toggling.

While CSS Crimes are probably not something you would want to employ on a regular basis, we should embrace experiments within constraints that can foster creative solutions.

To Shared LinkPermalink on CSS-Tricks


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

]]>
https://css-tricks.com/committing-css-crimes/feed/ 0 367369
A Lightweight Masonry Solution https://css-tricks.com/a-lightweight-masonry-solution/ https://css-tricks.com/a-lightweight-masonry-solution/#comments Mon, 03 Aug 2020 14:00:00 +0000 https://css-tricks.com/?p=317422 Back in May, I learned about Firefox adding masonry to CSS grid. Masonry layouts are something I’ve been wanting to do on my own from scratch for a very long time, but have never known where to start. So, naturally, …


A Lightweight Masonry Solution originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Back in May, I learned about Firefox adding masonry to CSS grid. Masonry layouts are something I’ve been wanting to do on my own from scratch for a very long time, but have never known where to start. So, naturally, I checked the demo and then I had a lightbulb moment when I understood how this new proposed CSS feature works.

Support is obviously limited to Firefox for now (and, even there, only behind a flag), but it still offered me enough of a starting point for a JavaScript implementation that would cover browsers that currently lack support.

The way Firefox implements masonry in CSS is by setting either grid-template-rows (as in the example) or grid-template-columns to a value of masonry.

My approach was to use this for supporting browsers (which, again, means just Firefox for now) and create a JavaScript fallback for the rest. Let’s look at how this works using the particular case of an image grid.

First, enable the flag

In order to do this, we go to about:config in Firefox and search for “masonry.” This brings up the layout.css.grid-template-masonry-value.enabled flag, which we enable by double clicking its value from false (the default) to true.

Screenshot showing the masonry flag being enabled according to the instructions above.
Making sure we can test this feature.

Let’s start with some markup

The HTML structure looks something like this:

<section class="grid--masonry">
  <img src="black_cat.jpg" alt="black cat" />
  <!-- more such images following -->
</section>

Now, let’s apply some styles

The first thing we do is make the top-level element a CSS grid container. Next, we define a maximum width for our images, let’s say 10em. We also want these images to shrink to whatever space is available for the grid’s content-box if the viewport becomes too narrow to accommodate for a single 10em column grid, so the value we actually set is Min(10em, 100%). Since responsivity is important these days, we don’t bother with a fixed number of columns, but instead auto-fit as many columns of this width as we can:

$w: Min(10em, 100%);

.grid--masonry {
  display: grid;
  grid-template-columns: repeat(auto-fit, $w);
	
  > * { width: $w; }
}

Note that we’ve used Min() and not min() in order to avoid a Sass conflict.

Well, that’s a grid!

Not a very pretty one though, so let’s force its content to be in the middle horizontally, then add a grid-gap and padding that are both equal to a spacing value ($s). We also set a background to make it easier on the eyes.

$s: .5em;

/* masonry grid styles */
.grid--masonry {
  /* same styles as before */
  justify-content: center;
  grid-gap: $s;
  padding: $s
}

/* prettifying styles */
html { background: #555 }

Having prettified the grid a bit, we turn to doing the same for the grid items, which are the images. Let’s apply a filter so they all look a bit more uniform, while giving a little additional flair with slightly rounded corners and a box-shadow.

img {
  border-radius: 4px;
  box-shadow: 2px 2px 5px rgba(#000, .7);
  filter: sepia(1);
}

The only thing we need to do now for browsers that support masonry is to declare it:

.grid--masonry {
  /* same styles as before */
  grid-template-rows: masonry;
}

While this won’t work in most browsers, it produces the desired result in Firefox with the flag enabled as explained earlier.

Screenshot showing the masonry result in Firefox alongside DevTools where we can see what's under the hood.
grid-template-rows: masonry working in Firefox with the flag enabled (Demo).

But what about the other browsers? That’s where we need a…

JavaScript fallback

In order to be economical with the JavaScript the browser has to run, we first check if there are any .grid--masonry elements on that page and whether the browser has understood and applied the masonry value for grid-template-rows. Note that this is a generic approach that assumes we may have multiple such grids on a page.

let grids = [...document.querySelectorAll('.grid--masonry')];

if(grids.length && getComputedStyle(grids[0]).gridTemplateRows !== 'masonry') {
  console.log('boo, masonry not supported 😭')
}
else console.log('yay, do nothing!')
Screenshot showing how Firefox with the flag enabled as explained above logs 'yay, do nothing!', while other browsers log 'boo, masonry not supported'.
Support test (live).

If the new masonry feature is not supported, we then get the row-gap and the grid items for every masonry grid, then set a number of columns (which is initially 0 for each grid).

let grids = [...document.querySelectorAll('.grid--masonry')];

if(grids.length && getComputedStyle(grids[0]).gridTemplateRows !== 'masonry') {
  grids = grids.map(grid => ({
    _el: grid, 
    gap: parseFloat(getComputedStyle(grid).gridRowGap), 
    items: [...grid.childNodes].filter(c => c.nodeType === 1), 
    ncol: 0
  }));
  
  grids.forEach(grid => console.log(`grid items: ${grid.items.length}; grid gap: ${grid.gap}px`))
}

Note that we need to make sure the child nodes are element nodes (which means they have a nodeType of 1). Otherwise, we can end up with text nodes consisting of carriage returns in the array of items.

Screenshot showing the number of items and the row-gap logged in the console.
Checking we got the correct number of items and gap (live).

Before proceeding further, we have to ensure the page has loaded and the elements aren’t still moving around. Once we’ve handled that, we take each grid and read its current number of columns. If this is different from the value we already have, then we update the old value and rearrange the grid items.

if(grids.length && getComputedStyle(grids[0]).gridTemplateRows !== 'masonry') {
  grids = grids.map(/* same as before */);
	
  function layout() {
    grids.forEach(grid => {
      /* get the post-resize/ load number of columns */
      let ncol = getComputedStyle(grid._el).gridTemplateColumns.split(' ').length;

      if(grid.ncol !== ncol) {
        grid.ncol = ncol;
        console.log('rearrange grid items')
      }
    });
  }
	
  addEventListener('load', e => {		
    layout(); /* initial load */
    addEventListener('resize', layout, false)
  }, false);
}

Note that calling the layout() function is something we need to do both on the initial load and on resize.

Screenshot showing the message we get when relayout is necessry.
When we need to rearrange grid items (live).

To rearrange the grid items, the first step is to remove the top margin on all of them (this may have been set to a non-zero value to achieve the masonry effect before the current resize).

If the viewport is narrow enough that we only have one column, we’re done!

Otherwise, we skip the first ncol items and we loop through the rest. For each item considered, we compute the position of the bottom edge of the item above and the current position of its top edge. This allows us to compute how much we need to move it vertically such that its top edge is one grid gap below the bottom edge of the item above.

/* if the number of columns has changed */
if(grid.ncol !== ncol) {
  /* update number of columns */
  grid.ncol = ncol;

  /* revert to initial positioning, no margin */
  grid.items.forEach(c => c.style.removeProperty('margin-top'));

  /* if we have more than one column */
  if(grid.ncol > 1) {
    grid.items.slice(ncol).forEach((c, i) => {
      let prev_fin = grid.items[i].getBoundingClientRect().bottom /* bottom edge of item above */, 
          curr_ini = c.getBoundingClientRect().top /* top edge of current item */;
						
      c.style.marginTop = `${prev_fin + grid.gap - curr_ini}px`
    })
  }
}

We now have a working, cross-browser solution!

A couple of minor improvements

A more realistic structure

In a real world scenario, we’re more likely to have each image wrapped in a link to its full size so that the big image opens in a lightbox (or we navigate to it as a fallback).

<section class='grid--masonry'>
  <a href='black_cat_large.jpg'>
    <img src='black_cat_small.jpg' alt='black cat'/>
  </a>
  <!-- and so on, more thumbnails following the first -->
</section>

This means we also need to alter the CSS a bit. While we don’t need to explicitly set a width on the grid items anymore — as they’re now links — we do need to set align-self: start on them because, unlike images, they stretch to cover the entire row height by default, which will throw off our algorithm.

.grid--masonry > * { align-self: start; }

img {
  display: block; /* avoid weird extra space at the bottom */
  width: 100%;
  /* same styles as before */
}

Making the first element stretch across the grid

We can also make the first item stretch horizontally across the entire grid (which means we should probably also limit its height and make sure the image doesn’t overflow or get distorted):

.grid--masonry > :first-child {
  grid-column: 1/ -1;
  max-height: 29vh;
}

img {
  max-height: inherit;
  object-fit: cover;
  /* same styles as before */
}

We also need to exclude this stretched item by adding another filter criterion when we get the list of grid items:

grids = grids.map(grid => ({
  _el: grid, 
  gap: parseFloat(getComputedStyle(grid).gridRowGap), 
  items: [...grid.childNodes].filter(c => 
    c.nodeType === 1 && 
    +getComputedStyle(c).gridColumnEnd !== -1
  ), 
  ncol: 0
}));

Handling grid items with variable aspect ratios

Let’s say we want to use this solution for something like a blog. We keep the exact same JS and almost the exact same masonry-specific CSS – we only change the maximum width a column may have and drop the max-height restriction for the first item.

As it can be seen from the demo below, our solution also works perfectly in this case where we have a grid of blog posts:

You can also resize the viewport to see how it behaves in this case.

However, if we want the width of the columns to be somewhat flexible, for example, something like this:

$w: minmax(Min(20em, 100%), 1fr)

Then we have a problem on resize:

The changing width of the grid items combined with the fact that the text content is different for each means that when a certain threshold is crossed, we may get a different number of text lines for a grid item (thus changing the height), but not for the others. And if the number of columns doesn’t change, then the vertical offsets don’t get recomputed and we end up with either overlaps or bigger gaps.

In order to fix this, we need to also recompute the offsets whenever at least one item’s height changes for the current grid. This means we need to also need to test if more than zero items of the current grid have changed their height. And then we need to reset this value at the end of the if block so that we don’t rearrange the items needlessly next time around.

if(grid.ncol !== ncol || grid.mod) {
  /* same as before */
  grid.mod = 0
}

Alright, but how do we change this grid.mod value? My first idea was to use a ResizeObserver:

if(grids.length && getComputedStyle(grids[0]).gridTemplateRows !== 'masonry') {
  let o = new ResizeObserver(entries => {
    entries.forEach(entry => {
      grids.find(grid => grid._el === entry.target.parentElement).mod = 1
    });
  });
  
  /* same as before */
  
  addEventListener('load', e => {
    /* same as before */
    grids.forEach(grid => { grid.items.forEach(c => o.observe(c)) })
  }, false)
}

This does the job of rearranging the grid items when necessary even if the number of grid columns doesn’t change. But it also makes even having that if condition pointless!

This is because it changes grid.mod to 1 whenever the height or the width of at least one item changes. The height of an item changes due to the text reflow, caused by the width changing. But the change in width happens every time we resize the viewport and doesn’t necessarily trigger a change in height.

This is why I eventually decided on storing the previous item heights and checking whether they have changed on resize to determine whether grid.mod remains 0 or not:

function layout() {
  grids.forEach(grid => {
    grid.items.forEach(c => {
      let new_h = c.getBoundingClientRect().height;
				
      if(new_h !== +c.dataset.h) {
        c.dataset.h = new_h;
        grid.mod++
      }
    });
			
    /* same as before */
  })
}

That’s it! We now have a nice lightweight solution. The minified JavaScript is under 800 bytes, while the strictly masonry-related styles are under 300 bytes.

But, but, but…

What about browser support?

Well, @supports just so happens to have better browser support than any of the newer CSS features used here, so we can put the nice stuff inside it and have a basic, non-masonry grid for non-supporting browsers. This version works all the way back to IE9.

Screenshot showing the IE grid.
The result in Internet Explorer

It may not look the same, but it looks decent and it’s perfectly functional. Supporting a browser doesn’t mean replicating all the visual candy for it. It means the page works and doesn’t look broken or horrible.

What about the no JavaScript case?

Well, we can apply the fancy styles only if the root element has a js class which we add via JavaScript! Otherwise, we get a basic grid where all the items have the same size.

Screenshot showing the no JS grid.
The no JavaScript result (Demo).

A Lightweight Masonry Solution originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/a-lightweight-masonry-solution/feed/ 8 317422
Background Patterns, Simplified by Conic Gradients https://css-tricks.com/background-patterns-simplified-by-conic-gradients/ https://css-tricks.com/background-patterns-simplified-by-conic-gradients/#comments Thu, 28 May 2020 14:37:00 +0000 https://css-tricks.com/?p=307270 For those who have missed the big news, Firefox now supports conic gradients!

Starting with Firefox 75, released on the April 7, we can go to about:config, look for the layout.css.conic-gradient.enabled flag and set its value to true


Background Patterns, Simplified by Conic Gradients originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
For those who have missed the big news, Firefox now supports conic gradients!

Starting with Firefox 75, released on the April 7, we can go to about:config, look for the layout.css.conic-gradient.enabled flag and set its value to true (it’s false by default and all it takes to switch is double-clicking it).

Screenshot. Shows the Firefox URL bar at `about:config`, a search for 'conic' giving the `layout.css.conic-gradient.enabled` flag as the sole result and its value set to `true`.
Enabling conic gradients in Firefox 75+

With that enabled, now we can test our CSS including conic gradients in Firefox as well.

While some of the demos in this article work just fine when using a polyfill, some use CSS variables inside the conic gradient and therefore require native support for this feature.

One thing I particularly like about conic gradients is just how much they can simplify background patterns. So let’s take a few linear-gradient() patterns from the gallery created by Lea Verou about a decade ago and see how we can now simplify them with conic-gradient!

Pyramid

Screenshot. Shows the original pyramid pattern with the code that was used to create it.
The pyramid pattern

The pattern above uses four linear gradients:

background:
  linear-gradient(315deg, transparent 75%, #d45d55 0) -10px 0,
  linear-gradient(45deg, transparent 75%, #d45d55 0) -10px 0,
  linear-gradient(135deg, #a7332b 50%, transparent 0) 0 0,
  linear-gradient(45deg, #6a201b 50%, #561a16 0) 0 0 #561a16;
background-size: 20px 20px;

That’s quite a bit of CSS and perhaps even a bit intimidating. It’s not easy to just look at this and understand how it all adds up to give us the pyramid pattern. I certainly couldn’t do it. It took me a while to get it, even though gradients are one of the CSS features I’m most comfortable with. So don’t worry if you don’t understand how those gradients manage to create the pyramid pattern because, one, it is complicated and, two, you don’t even need to understand that!

Using conic-gradient(), we can now get the same result in a much simpler manner, with a single background layer instead of four!

What I like to do when coding repeating patterns is draw equidistant vertical and horizontal lines delimiting the rectangular boxes defined by the background-size. In this case, it’s pretty obvious we have square boxes and where their limits are, but it’s a really useful technique for more complex patterns.

Annotated screenshot. Shows the rectangles (squares in this case) defined by the `background-size`.
Highlighting the pattern’s cells

By default, conic gradients start from 12 o’clock and go clockwise. However, in our case, we want to offset the start of the gradient by 45° in the clockwise direction and afterwards make every one of the four shades occupy a quarter (25%) of the available space around the midpoint of our square box.

SVG illustration. Shows how we place a conic gradient into a single pattern cell by rotating the gradient start point 45° in the clockwise (positive) direction.
A pattern cell with a conic gradient’s hard stops at every 25% starting from 45° w.r.t. the vertical axis (live).

This means our pyramid pattern can be reduced to:

$s: 20px;
background:
  conic-gradient(from 45deg, 
    #561a16 25%, 
    #6a201b 0% 50%, 
    #a7332b 0% 75%, 
    #d45d55 0%) 
    50%/ #{$s $s};

Not only does the code look simpler, but we’ve also gone from 260 bytes to 103 bytes, reducing the code needed to get this pattern by more than half.

We’re using the double position syntax as that’s also well supported these days.

We can see it in action in the Pen below:

Checkerboard

Screenshot. Shows the original checkerboard pattern with the code that was used to create it.
The checkerboard pattern

This pattern above is created with two linear gradients:

background-color: #eee;
background-image:
  linear-gradient(45deg, black 25%, transparent 25%, 
    transparent 75%, black 75%, black),
  linear-gradient(45deg, black 25%, transparent 25%, 
    transparent 75%, black 75%, black);
background-size: 60px 60px;
background-position: 0 0, 30px 30px;

Let’s see how we can simplify this CSS when replacing these linear gradients with a conic one!

Just like in the previous case, we draw vertical and horizontal lines in order to better see the rectangles defined by the background-size.

Annotated screenshot. Shows the rectangles (squares in this case) defined by the `background-size`.
Highlighting the pattern’s cells

Looking at the square highlighted in deeppink in the illustration above, we see that, in this case, our conic gradient starts from the default position at 12 o’clock. A quarter of it is black, the next quarter is dirty white and then we have repetition (the same black and then dirty white quarter slices once more).

SVG illustration. Shows how we place a conic gradient into a single pattern cell and then make it repeat after the 50% point.
A pattern cell with a conic gradient’s hard stops at every 25%, starting from the default at 12 o’clock and repeating after 50% (demo).

This repetition in the second half of the [0%, 100%] interval means we can use a repeating-conic-gradient(), which gives us the following code (bringing the compiled CSS from 263 bytes down to only 73 bytes – that’s reducing it by over 70%):

$s: 60px;
background:
  repeating-conic-gradient(#000 0% 25%, #eee 0% 50%) 
    50%/ #{$s $s};

The Pen below shows it in action:

Diagonal checkerboard

Screenshot. Shows the original diagonal checkerboard pattern with the code that was used to create it.
The diagonal checkerboard pattern

Again, we have a pattern created with two linear gradients:

background-color: #eee;
background-image: 
  linear-gradient(45deg, black 25%, transparent 25%, 
    transparent 75%, black 75%, black),
  linear-gradient(-45deg, black 25%, transparent 25%, 
    transparent 75%, black 75%, black);
background-size: 60px 60px;

We draw horizontal and vertical lines to split this pattern into identical rectangles:

Annotated screenshot. Shows the rectangles (squares in this case) defined by the `background-size`.
Highlighting the pattern’s cells

What we now have is pretty much the same checkerbox pattern as before, with the sole difference that we don’t start from the default position at 12 o’clock, but from 45° in the clockwise direction.

If you’re having trouble visualising how simply changing the start angle can make us go from the previous pattern to this one, you can play with it in the interactive demo below:

Note that this demo does not work in browsers that have no native support for conic gradients.

This means our code looks as follows:

$s: 60px;
background:
  repeating-conic-gradient(from 45deg, 
    #000 0% 25%, #eee 0% 50%) 
  50%/ #{$s $s};

We can see it in action below:

Again, not only is the code simpler to understand, but we’ve also gone from 229 bytes to only 83 bytes in the compiled CSS, reducing it by almost two-thirds!

Half-Rombes

Screenshot. Shows the original Half-Rombes pattern with the code that was used to create it.
The half-rombes pattern

This pattern was created with four linear gradients:

background: #36c;
background:
  linear-gradient(115deg, transparent 75%, rgba(255,255,255,.8) 75%) 0 0,
  linear-gradient(245deg, transparent 75%, rgba(255,255,255,.8) 75%) 0 0,
  linear-gradient(115deg, transparent 75%, rgba(255,255,255,.8) 75%) 7px -15px,
  linear-gradient(245deg, transparent 75%, rgba(255,255,255,.8) 75%) 7px -15px,
  #36c;
background-size: 15px 30px;

Just like in the previous cases, we draw equidistant vertical and horizontal lines in order to better see the repeating unit:

Annotated screenshot. Shows the rectangles (squares in this case) defined by the `background-size`.
Highlighting the pattern’s cells.

What we have here is a pattern that’s made up of congruent isosceles triangles (the angled edges are equal and the dark blue triangles are a reflection of the light blue ones) formed by the intersection of equidistant parallel lines that are either horizontal, angled clockwise, or the other way. Each of these three types of parallel lines is highlighted in the illustration below:

Illustration. Shows the equidistant parallel lines which create the pattern of isosceles triangles.
Parallel guides

Every pattern cell contains a full triangle and two adjacent triangle halves in the upper part, then a reflection of this upper part in the lower part. This means we can identify a bunch of congruent right triangles that will help us get the angles we need for our conic-gradient():

SVG illustration. Shows how we place a conic gradient into a single pattern cell by rotating the gradient start point by an angle β in the clockwise (positive) direction such that the 0% line goes through the top right corner and then all the other hard stops are either horizontal or going through the cell corners.
A pattern cell with a conic gradient’s hard stops such that they’re either horizontal or go through the cell corners, all starting from β w.r.t. the vertical axis (demo)

This illustration shows us that the gradient starts from an angle, β, away from the default conic gradient start point at 12 o’clock. The first conic slice (the top right half triangle) goes up to α, the second one (the bottom right dark triangle) up to 2·α, and the third one (the bottom light triangle) goes halfway around the circle from the start (that’s 180°, or 50%). The fourth one (the bottom left dark triangle) goes to 180° + α and the fifth one (the top left light triangle) goes to 180° + 2·α, while the sixth one covers the rest.

SVG illustration. Highlights the right triangle from where we can get α knowing the catheti and shows how we can then compute β.
Getting α and β (demo)

From the highlighted right triangle we get that:

tan(α) = (.5·h)/(.5·w) = h/w

Knowing the width (w) and height (h) of a pattern cell, we can get the angles α and β:

α = atan(h/w)
β = 90° - α

It results in the pattern that’s generated by the following code:

$w: 15px;
$h: 30px;
$a: atan($h/$w)*180deg/pi();
$b: 90deg - $a;
$c0: #36c;
$c1: #d6e0f5;

html {
  background: 
    conic-gradient(from $b, 
      $c1 0% $a, 
      $c0 0% 2*$a, 
      $c1 0% 50%, 
      $c0 0% 180deg + $a, 
      $c1 0% 180deg + 2*$a, 
      $c0 0%) 
    0 0/ #{$w $h};
}

This means going from 343 bytes to only 157 bytes in the compiled CSS. The result can be seen below:

You can tweak the pattern width ($w) and height ($h) in the Sass code in order to see how the pattern gets squished and stretched for different aspect ratios.

In the particular case where the angle between 2*$a and 50% (or 180deg) is also $a, it results that $a is 60deg, our isosceles triangles are equilateral, and our gradient can be reduced to a repeating one (and under 100 bytes in the compiled CSS):

$a: 60deg;
$b: 90deg - $a;
$w: 15px;
$h: $w*tan($a);
$c0: #36c;
$c1: #d6e0f5;

html {
  background: 
    repeating-conic-gradient(from $b, 
      $c1 0% $a, $c0 0% 2*$a) 
    0 0/ #{$w $h}
}

The live result can be seen below:

Bonus: Intersecting line backgrounds!

Screenshot. Shows the original intersecting lines pattern with the code that was used to create it.
Intersecting line background examples

While these are not repeating patterns, they’re examples of a situation where a single conic gradient achieves an effect that would have previously needed a bunch of linear ones.

What we have here is a conic-gradient() created starting from two straight lines intersecting within the rectangular box where we set the background.

SVG illustration. Shows a rectangular box and two random lines intersecting inside it. This intersection point (x,y) is the point the conic gradient goes around, while the gradient's start is from the angle β formed by the line segment closest to the top right corner with the vertical. The hard stops are at α, the angle between the start segment and the next one in clockwise order, at 50% and at 180° + α.
Bonus pattern structure (ldemo)

The gradient goes around the point of coordinates, x,y, where the two straight lines intersect. It starts from an angle, β, which is the angle of the line segment that’s closest to the top-right corner, then has hard stops at α, 50% (or 180°) and 180° + α.

If we want to have multiple elements with similar such patterns created with the help of different intersecting lines and different palettes, we have the perfect use case for CSS variables:

.panel {
  background: 
    conic-gradient(from var(--b) at var(--xy), 
      var(--c0) var(--a), var(--c1) 0% 50%, 
      var(--c2) 0% calc(180deg + var(--a)), var(--c3) 0%);
}

All we have to do is set the position (--xy), the start angle (--b), the first angle (--a) and the palette (--c0 through --c3).

.panel {
  /* same as before */
  
  &:nth-child(1) {
    --xy: 80% 65%; 
    --b: 31deg;
    --a: 121deg; 
    --c0: #be5128;
    --c1: #ce9248;
    --c2: #e4c060;
    --c3: #db9c4e
  }
  
  /* similarly for the other panels */
}

Instead of hardcoding, we could also generate these values randomly or extract them from a data object with the help of a CSS or HTML preprocessor. In this second case, we’d set these custom properties inline, which is precisely what I did in the Pen below:

Since we’re using custom properties inside the conic gradients, this demo does not work in browsers that don’t support them natively.

Well, that’s it! I hope you’ve enjoyed this article and that it gives you some ideas about how conic gradients can make your life easier.


Background Patterns, Simplified by Conic Gradients originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/background-patterns-simplified-by-conic-gradients/feed/ 4 307270
Weaving a Line Through Text in CSS https://css-tricks.com/weaving-a-line-through-text-in-css/ https://css-tricks.com/weaving-a-line-through-text-in-css/#comments Wed, 26 Feb 2020 15:04:24 +0000 https://css-tricks.com/?p=303448 Earlier this year, I came across this demo by Florin Pop, which makes a line go either over or under the letters of a single line heading. I thought this was a cool idea, but there were a few little …


Weaving a Line Through Text in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Earlier this year, I came across this demo by Florin Pop, which makes a line go either over or under the letters of a single line heading. I thought this was a cool idea, but there were a few little things about the implementation I felt I could simplify and improve at the same time.

First off, the original demo duplicates the headline text, which I knew could be easily avoided. Then there’s the fact that the length of the line going through the text is a magic number, which is not a very flexible approach. And finally, can’t we get rid of the JavaScript?

So let’s take a look into where I ended up taking this.

HTML structure

Florin puts the text into a heading element and then duplicates this heading, using Splitting.js to replace the text content of the duplicated heading with spans, each containing one letter of the original text.

Already having decided to do this without text duplication, using a library to split the text into characters and then put each into a span feels a bit like overkill, so we’re doing it all with an HTML preprocessor.

- let text = 'We Love to Play';
- let arr = text.split('');

h1(role='image' aria-label=text)
  - arr.forEach(letter => {
    span.letter #{letter}
  - });

Since splitting text into multiple elements may not work nicely with screen readers, we’ve given the whole thing a role of image and an aria-label.

This generates the following HTML:

<h1 role="image" aria-label="We Love to Play">
  <span class="letter">W</span>
  <span class="letter">e</span>
  <span class="letter"> </span>
  <span class="letter">L</span>
  <span class="letter">o</span>
  <span class="letter">v</span>
  <span class="letter">e</span>
  <span class="letter"> </span>
  <span class="letter">t</span>
  <span class="letter">o</span>
  <span class="letter"> </span>
  <span class="letter">P</span>
  <span class="letter">l</span>
  <span class="letter">a</span>
  <span class="letter">y</span>
</h1>

Basic styles

We place the heading in the middle of its parent (the body in this case) by using a grid layout:

body {
  display: grid;
  place-content: center;
}
Screenshot of grid layout lines around the centrally placed heading when inspecting it with Firefox DevTools.
The heading doesn’t stretch across its parent to cover its entire width, but is instead placed in the middle.

We may also add some prettifying touches, like a nice font or a background on the container.

Next, we create the line with an absolutely positioned ::after pseudo-element of thickness (height) $h:

$h: .125em;
$r: .5*$h;

h1 {
  position: relative;
  
  &::after {
    position: absolute;
    top: calc(50% - #{$r}); right: 0;
    height: $h;
    border-radius: 0 $r $r 0;
    background: crimson;
  }
}

The above code takes care of the positioning and height of the pseudo-element, but what about the width? How do we make it stretch from the left edge of the viewport to the right edge of the heading text?

Line length

Well, since we have a grid layout where the heading is middle-aligned horizontally, this means that the vertical midline of the viewport coincides with that of the heading, splitting both into two equal-width halves:

SVG illustration. Shows how the vertical midline of the viewport coincides with that of the heading and splits both into equal width halves.
The middle-aligned heading.

Consequently, the distance between the left edge of the viewport and the right edge of the heading is half the viewport width (50vw) plus half the heading width, which can be expressed as a % value when used in the computation of its pseudo-element’s width.

So the width of our ::after pseudo-element is:

width: calc(50vw + 50%);

Making the line go over and under

So far, the result is just a crimson line crossing some black text:

What we want is for some of the letters to show up on top of the line. In order to get this effect, we give them (or we don’t give them) a class of .over at random. This means slightly altering the Pug code:

- let text = 'We Love to Play';
- let arr = text.split('');

h1(role='image' aria-label=text)
  - arr.forEach(letter => {
    span.letter(class=Math.random() > .5 ? 'over' : null) #{letter}
  - });

We then relatively position the letters with a class of .over and give them a positive z-index.

.over {
  position: relative;
  z-index: 1;
}

My initial idea involved using translatez(1px) instead of z-index: 1, but then it hit me that using z-index has both better browser support and involves less effort.

The line passes over some letters, but underneath others:

Animate it!

Now that we got over the tricky part, we can also add in an animation to make the line enter in. This means having the crimson line shift to the left (in the negative direction of the x-axis, so the sign will be minus) by its full width (100%) at the beginning, only to then allow it to go back to its normal position.

@keyframes slide { 0% { transform: translate(-100%); } }

I opted to have a bit of time to breathe before the start of the animation. This meant adding in the 1s delay which, in turn, meant adding the backwards keyword for the animation-fill-mode, so that the line would stay in the state specified by the 0% keyframe before the start of the animation:

animation: slide 2s ease-out 1s backwards;

A 3D touch

Doing this gave me another idea, which was to make the line go through every single letter, that is, start above the letter, go through it and finish underneath (or the other way around).

This requires real 3D and a few small tweaks.

First off, we set transform-style to preserve-3d on the heading since we want all its children (and pseudo-elements) to a be part of the same 3D assembly, which will make them be ordered and intersect according to how they’re positioned in 3D.

Next, we want to rotate each letter around its y-axis, with the direction of rotation depending on the presence of the randomly assigned class (whose name we change to .rev from “reverse” as “over” isn’t really suggestive of what we’re doing here anymore).

However, before we do this, we need to remember our span elements are still inline ones at this point and setting a transform on an inline element has absolutely no effect.

To get around this issue, we set display: flex on the heading. However, this creates a new issue and that’s the fact that span elements that contain only a space (" ") get squished to zero width.

Screenshot showing how the span containing only a space gets squished to zero width when setting `display: flex` on its parent.
Inspecting a space only <span> in Firefox DevTools.

A simple fix for this is to set white-space: pre on our .letter spans.

Once we’ve done this, we can rotate our spans by an angle $a… in one direction or the other!

$a: 2deg;

.letter {
  white-space: pre;
  transform: rotatey($a);
}

.rev { transform: rotatey(-$a); }

Since rotation around the y-axis squishes our letters horizontally, we can scale them along the x-axis by a factor ($f) that’s the inverse of the cosine of $a.

$a: 2deg;
$f: 1/cos($a)

.letter {
  white-space: pre;
  transform: rotatey($a) scalex($f)
}

.rev { transform: rotatey(-$a) scalex($f) }

If you wish to understand the why behind using this particular scaling factor, you can check out this older article where I explain it all in detail.

And that’s it! We now have the 3D result we’ve been after! Do note however that the font used here was chosen so that our result looks good and another font may not work as well.


Weaving a Line Through Text in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/weaving-a-line-through-text-in-css/feed/ 10 303448
Various Methods for Expanding a Box While Preserving the Border Radius https://css-tricks.com/various-methods-for-expanding-a-box-while-preserving-the-border-radius/ https://css-tricks.com/various-methods-for-expanding-a-box-while-preserving-the-border-radius/#comments Fri, 06 Sep 2019 14:19:05 +0000 http://css-tricks.com/?p=292177 I’ve recently noticed an interesting change on CodePen: on hovering the pens on the homepage, there’s a rectangle with rounded corners expanding in the back.

Expanding box effect on the CodePen homepage.

Being the curious creature that I am, I …


Various Methods for Expanding a Box While Preserving the Border Radius originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I’ve recently noticed an interesting change on CodePen: on hovering the pens on the homepage, there’s a rectangle with rounded corners expanding in the back.

Animated gif recording the CodePen expanding box effect on hover.
Expanding box effect on the CodePen homepage.

Being the curious creature that I am, I had to check how this works! Turns out, the rectangle in the back is an absolutely positioned ::after pseudo-element.

Collage. On the left side, there is a DevTools screenshot showing the initial styles applied on the ::after pseudo-element. The relevant ones are those making it absolutely positioned with an offset of 1rem from the top and left and with an offset of -1rem from the right and bottom. On the right side, we have an illustration of these styles, showing the parent element box, the ::after box and the offsets between their edges.
Initial ::after styles. A positive offset goes inwards from the parent’s padding limit, while a negative one goes outwards.

On :hover, its offsets are overridden and, combined with the transition, we get the expanding box effect.

Collage. On the left side, there is a DevTools screenshot showing the :hover styles applied on the ::after pseudo-element. These are all offsets overriding the initial ones and making the boundary of the ::after shift outwards by 2rem in all directions except the right. On the right side, we have an illustration of these styles, showing the parent element box, the ::after box and the offsets between their edges.
The ::after styles on :hover.

The right property has the same value (-1rem) in both the initial and the :hover rule sets, so it’s unnecessary to override it, but all the other offsets move by 2rem outwards (from 1rem to -1rem for the top and left offsets and from -1rem to -3rem for the bottom offset)

One thing to notice here is that the ::after pseudo-element has a border-radius of 10px which gets preserved as it expands. Which got me to think about what methods we have for expanding/shrinking (pseudo-) elements while preserving their border-radius. How many can you think of? Let me know if you have ideas that haven’t been included below, where we take a look at a bunch of options and see which is best suited for what situation.

Changing offsets

This is the method used on CodePen and it works really well in this particular situation for a bunch of reasons. First off, it has great support. It also works when the expanding (pseudo-) element is responsive, with no fixed dimensions and, at the same time, the amount by which it expands is fixed (a rem value). It also works for expanding in more than two directions (top, bottom and left in this particular case).

There are however a couple of caveats we need to be aware of.

First, our expanding element cannot have position: static. This is not a problem in the context of the CodePen use case since the ::after pseudo-element needs to be absolutely positioned anyway in order to be placed underneath the rest of this parent’s content.

Second, going overboard with offset animations (as well as, in general, animating any property that affects layout with box properties the way offsets, margins, border widths, paddings or dimensions do) can negatively impact performance. Again, this is not something of concern here, we only have a little transition on :hover, no big deal.

Changing dimensions

Instead of changing offsets, we could change dimensions instead. However, this is a method that works if we want our (pseudo-) element to expand in, at most, two directions. Otherwise, we need to change offsets as well. In order to better understand this, let’s consider the CodePen situation where we want our ::after pseudo-elements to expand in three directions (top, bottom and left).

The relevant initial sizing info is the following:

.single-item::after {
  top: 1rem;
  right: -1rem;
  bottom: -1rem;
  left: 1rem;
}

Since opposing offsets (the topbottom and leftright pairs) cancel each other (1rem - 1rem = 0), it results that the pseudo-element’s dimensions are equal to those of its parent (or 100% of the parent’s dimensions).

So we can re-write the above as:

.single-item::after {
  top: 1rem;
  right: -1rem;
  width: 100%;
  height: 100%;
}

On :hover, we increase the width by 2rem to the left and the height by 4rem, 2rem to the top and 2rem to the bottom. However, just writing:

.single-item::after {
  width: calc(100% + 2rem);
  height: calc(100% + 4rem);
}

…is not enough, as this makes the height increase the downward direction by 4rem instead of increasing it by 2rem up and 2rem down. The following demo illustrates this (put :focus on or hover over the items to see how the ::after pseudo-element expands):

See the Pen by thebabydino (@thebabydino) on CodePen.

We’d need to update the top property as well in order to get the desired effect:

.single-item::after {
  top: -1rem;
  width: calc(100% + 2rem);
  height: calc(100% + 4rem);
}

Which works, as it can be seen below:

See the Pen by thebabydino (@thebabydino) on CodePen.

But, to be honest, this feels less desirable than changing offsets alone.

However, changing dimensions is a good solution in a different kind of situation, like when we want to have some bars with rounded corners that expand/shrink in a single direction.

See the Pen by thebabydino (@thebabydino) on CodePen.

Note that, if we didn’t have rounded corners to preserve, the better solution would be to use directional scaling via the transform property.

Changing padding/border-width

Similar to changing the dimensions, we can change the padding or border-width (for a border that’s transparent). Note that, just like with changing the dimensions, we need to also update offsets if expanding the box in more than two dimensions:

See the Pen by thebabydino (@thebabydino) on CodePen.

In the demo above, the pinkish box represents the content-box of the ::after pseudo-element and you can see it stays the same size, which is important for this approach.

In order to understand why it is important, consider this other limitation: we also need to have the box dimensions defined by two offsets plus the width and the height instead of using all four offsets. This is because the padding/ border-width would only grow inwards if we were to use four offsets rather than two plus the width and the height.

See the Pen by thebabydino (@thebabydino) on CodePen.

For the same reason, we cannot have box-sizing: border-box on our ::after pseudo-element.

See the Pen by thebabydino (@thebabydino) on CodePen.

In spite of these limitations, this method can come in handy if our expanding (pseudo-) element has text content we don’t want to see moving around on :hover as illustrated by the Pen below, where the first two examples change offsets/ dimensions, while the last two change paddings/ border widths:

See the Pen by thebabydino (@thebabydino) on CodePen.

Changing margin

Using this method, we first set the offsets to the :hover state values and a margin to compensate and give us the initial state sizing:

.single-item::after {
  top: -1rem;
  right: -1rem;
  bottom: -3rem;
  left: -1rem;
  margin: 2rem 0 2rem 2rem;
}

Then we zero this margin on :hover:

.single-item:hover::after { margin: 0 }

See the Pen by thebabydino (@thebabydino) on CodePen.

This is another approach that works great for the CodePen situation, though I cannot really think of other use cases. Also note that, just like changing offsets or dimensions, this method affects the size of the content-box, so any text content we may have gets moved and rearranged.

Changing font size

This is probably the trickiest one of all and has lots of limitations, the most important of which being we cannot have text content on the actual (pseudo-) element that expands/shrinks — but it’s another method that would work well in the CodePen case.

Also, font-size on its own doesn’t really do anything to make a box expand or shrink. We need to combine it with one of the previously discussed properties.

For example, we can set the font-size on ::after to be equal to 1rem, set the offsets to the expanded case and set em margins that would correspond to the difference between the expanded and the initial state.

.single-item::after {
  top: -1rem;
  right: -1rem;
  bottom: -3rem;
  left: -1rem;
  margin: 2em 0 2em 2em;
  font-size: 1rem;
}

Then, on :hover, we bring the font-size to 0:

.single-item:hover::after { font-size: 0 }

See the Pen by thebabydino (@thebabydino) on CodePen.

We can also use font-size with offsets, though it gets a bit more complicated:

.single-item::after {
  top: calc(2em - 1rem);
  right: -1rem;
  bottom: calc(2em - 3rem);
  left: calc(2em - 1rem);
  font-size: 1rem;
}

.single-item:hover::after { font-size: 0 }

Still, what’s important is that it works, as it can be seen below:

See the Pen by thebabydino (@thebabydino) on CodePen.

Combining font-size with dimensions is even hairier, as we also need to change the vertical offset value on :hover on top of everything:

.single-item::after {
  top: 1rem;
  right: -1rem;
  width: calc(100% + 2em);
  height: calc(100% + 4em);
  font-size: 0;
}

.single-item:hover::after {
  top: -1rem;
  font-size: 1rem
}

Oh, well, at least it works:

See the Pen by thebabydino (@thebabydino) on CodePen.

Same thing goes for using font-size with padding/border-width:

.single-item::after {
  top: 1rem;
  right: -1rem;
  width: 100%;
  height: 100%;
  font-size: 0;
}

.single-item:nth-child(1)::after {
  padding: 2em 0 2em 2em;
}

.single-item:nth-child(2)::after {
  border: solid 0 transparent;
  border-width: 2em 0 2em 2em;
}

.single-item:hover::after {
  top: -1rem;
  font-size: 1rem;
}

See the Pen by thebabydino (@thebabydino) on CodePen.

Changing scale

If you’ve read pieces on animation performance, then you’ve probably read it’s better to animate transforms instead of properties that impact layout, like offsets, margins, borders, paddings, dimensions — pretty much what we’ve used so far!

The first issue that stands out here is that scaling an element also scales its corner rounding, as illustrated below:

See the Pen by thebabydino (@thebabydino) on CodePen.

We can get around this by also scaling the border-radius the other way.

Let’s say we scale an element by a factor $fx along the x axis and by a factor $fy along the y axis and we want to keep its border-radius at a constant value $r.

This means we also need to divide $r by the corresponding scaling factor along each axis.

border-radius: #{$r/$fx}/ #{$r/$fy};
transform: scale($fx, $fy)

See the Pen by thebabydino (@thebabydino) on CodePen.

However, note that with this method, we need to use scaling factors, not amounts by which we expand our (pseudo-) element in this or that direction. Getting the scaling factors from the dimensions and expansion amounts is possible, but only if they’re expressed in units that have a certain fixed relation between them. While preprocessors can mix units like in or px due to the fact that 1in is always 96px, they cannot resolve how much 1em or 1% or 1vmin or 1ch is in px as they lack context. And calc() is not a solution either, as it doesn’t allow us to divide a length value by another length value to get a unitless scale factor.

This is why scaling is not a solution in the CodePen case, where the ::after boxes have dimensions that depend on the viewport and, at the same time, expand by fixed rem amounts.

But if our scale amount is given or we can easily compute it, this is an option to consider, especially since making the scaling factors custom properties we then animate with a bit of Houdini magic can greatly simplify our code.

border-radius: calc(#{$r}/var(--fx))/ calc(#{$r}/var(--fy));
transform: scale(var(--fx), var(--fy))

Note that Houdini only works in Chromium browsers with the Experimental Web Platform features flag enabled.

For example, we can create this tile grid animation:

Looping tile grid animation (Demo, Chrome with flag only)

The square tiles have an edge length $l and with a corner rounding of $k*$l:

.tile {
  width: $l;
  height: $l;
  border-radius: calc(#{$r}/var(--fx))/ calc(#{$r}/var(--fy));
  transform: scale(var(--fx), var(--fy))
}

We register our two custom properties:

CSS.registerProperty({
  name: '--fx', 
  syntax: '<number>', 
  initialValue: 1, 
  inherits: false
});

CSS.registerProperty({
  name: '--fy', 
  syntax: '<number>', 
  initialValue: 1, 
  inherits: false
});

And we can then animate them:

.tile {
  /* same as before */
  animation: a $t infinite ease-in alternate;
  animation-name: fx, fy;
}

@keyframes fx {
  0%, 35% { --fx: 1 }
  50%, 100% { --fx: #{2*$k} }
}

@keyframes fy {
  0%, 35% { --fy: 1 }
  50%, 100% { --fy: #{2*$k} }
}

Finally, we add in a delay depending on the horizontal (--i) and vertical (--j) grid indices in order to create a staggered animation effect:

animation-delay: 
  calc((var(--i) + var(--m) - var(--j))*#{$t}/(2*var(--m)) - #{$t}), 
  calc((var(--i) + var(--m) - var(--j))*#{$t}/(2*var(--m)) - #{1.5*$t})

Another example is the following one, where the dots are created with the help of pseudo-elements:

Looping spikes animation (Demo, Chrome with flag only)

Since pseudo-elements get scaled together with their parents, we need to also reverse the scaling transform on them:

.spike {
  /* other spike styles */
  transform: var(--position) scalex(var(--fx));

  &::before, &::after {
    /* other pseudo styles */
    transform: scalex(calc(1/var(--fx)));
  }
}

Changing… clip-path?!

This is a method I really like, even though it cuts out pre-Chromium Edge and Internet Explorer support.

Pretty much every usage example of clip-path out there has either a polygon() value or an SVG reference value. However, if you’ve seen some of my previous articles, then you probably know there are other basic shapes we can use, like inset(), which works as illustrated below:

Illustration showing what the four values of the inset() function represent. The first one is the offset of the top edge of the clipping rectangle with respect to the top edge of the border-box. The second one is the offset of the right edge of the clipping rectangle with respect to the right edge of the border-box. The third one is the offset of the bottom edge of the clipping rectangle with respect to the bottom edge of the border-box. The fourth one is the offset of the left edge of the clipping rectangle with respect to the left edge of the border-box.
How the inset() function works. (Demo)

So, in order to reproduce the CodePen effect with this method, we set the ::after offsets to the expanded state values and then cut out what we don’t want to see with the help of clip-path:

.single-item::after {
  top: -1rem;
  right: -1rem;
  bottom: -3em;
  left: -1em;
  clip-path: inset(2rem 0 2rem 2rem)
}

And then, in the :hover state, we zero all insets:

.single-item:hover::after {
  clip-path: inset(0)
}

This can be seen in action below:

See the Pen by thebabydino (@thebabydino) on CodePen.

Alright, this works, but we also need a corner rounding. Fortunately, inset() lets us specify that too as whatever border-radius value we may wish.

Here, a 10px one for all corners along both directions does it:

.single-item::after {
  /* same styles as before */
  clip-path: inset(2rem 0 2rem 2rem round 10px)
}

.single-item:hover::after {
  clip-path: inset(0 round 10px)
}

And this gives us exactly what we were going for:

See the Pen by thebabydino (@thebabydino) on CodePen.

Furthermore, it doesn’t really break anything in non-supporting browsers, it just always stays in the expanded state.

However, while this is method that works great for a lot of situations — including the CodePen use case — it doesn’t work when our expanding/shrinking elements have descendants that go outside their clipped parent’s border-box, as it is the case for the last example given with the previously discussed scaling method.


Various Methods for Expanding a Box While Preserving the Border Radius originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/various-methods-for-expanding-a-box-while-preserving-the-border-radius/feed/ 3 292177
Color Inputs: A Deep Dive into Cross-Browser Differences https://css-tricks.com/color-inputs-a-deep-dive-into-cross-browser-differences/ https://css-tricks.com/color-inputs-a-deep-dive-into-cross-browser-differences/#comments Fri, 12 Jul 2019 15:09:59 +0000 http://css-tricks.com/?p=288072 In this article, we’ll be taking a look at the structure inside <input type='color'> elements, browser inconsistencies, why they look a certain way in a certain browser, and how to dig into it. Having a good understanding of this input …


Color Inputs: A Deep Dive into Cross-Browser Differences originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
In this article, we’ll be taking a look at the structure inside <input type='color'> elements, browser inconsistencies, why they look a certain way in a certain browser, and how to dig into it. Having a good understanding of this input allows us to evaluate whether a certain cross-browser look can be achieved and how to do so with a minimum amount of effort and code.

Here’s exactly what we’re talking about:

But before we dive into this, we need to get into…

Accessibility issues!

We’ve got a huge problem here: for those who completely rely on a keyboard, this input doesn’t work as it should in Safari and in Firefox on Windows, but it does work in Firefox on Mac and Linux (which I only tested on Fedora, so feel free to yell at me in the comments if it doesn’t work for you using another distribution).

In Firefox on Windows, we can Tab to the input to focus it, press Enter to bring up a dialog… which we then cannot navigate with the keyboard!

I’ve tried tabbing, arrow keys, and every other key available on the keyboard… nothing! I could at least close the dialog with good old Alt + F4. Later, in the bug ticket I found for this on Bugzilla, I also discovered a workaround: Alt + Tab to another window, then Alt + Tab back and the picker dialog can be navigated with the keyboard.

Things are even worse in Safari. The input isn’t even focusable (bug ticket) if VoiceOver isn’t on. And even when using VoiceOver, tabbing through the dialog the inputs opens is impossible.

If you’d like to use <input type='color'> on an actual website, please let browsers know this is something that needs to be solved!

How to look inside

In Chrome, we need to bring up DevTools, go to Settings and, in the Preferences section under Elements, check the Show user agent shadow DOM option.

How to view the structure inside an input in Chrome.

Then, when we return to inspect our element, we can see inside its shadow DOM.

In Firefox, we need to go to about:config and ensure the devtools.inspector.showAllAnonymousContent flag is set to true.

How to view the structure inside an input in Firefox.

Then, we close the DevTools and, when we inspect our input again, we can see inside our input.

Sadly, we don’t seem to have an option for this in pre-Chromium Edge.

The structure inside

The structure revealed in DevTools differs from browser to browser, just like it does for range inputs.

In Chrome, at the top of the shadow DOM, we have a <div> wrapper that we can access using ::-webkit-color-swatch-wrapper.

Inside it, we have another <div> we can access with ::-webkit-color-swatch.

Screenshot of Chrome DevTools showing the shadow DOM of the <input type='color' data-recalc-dims=. Right at the top, we have a div which is the swatch wrapper and can be accessed using ::-webkit-color-swatch-wrapper. Inside it, there’s another div which is the swatch and can be accessed using ::-webkit-color-swatch. This div has the background-color set to the value of the parent color input.”/>
Inner structure in Chrome.

In Firefox, we only see one <div>, but it’s not labeled in any way, so how do we access it?

On a hunch, given this <div> has the background-color set to the input’s value attribute, just like the ::-webkit-color-swatch component, I tried ::-moz-color-swatch. And it turns out it works!

Screenshot of Firefox DevTools showing what's inside an <input type='color' data-recalc-dims=. Unlike in Chrome, here we only have a div which is the swatch and can be accessed using ::-moz-color-swatch. This div has the background-color set to the value of the parent color input.”/>
Inner structure in Firefox.

However, I later learned we have a better way of figuring this out for Firefox!

We can go into the Firefox DevTools Settings and, in the Inspector section, make sure the “Show Browser Styles” option is checked. Then, we go back to the Inspector and select this <div> inside our <input type='color'>. Among the user agent styles, we see a rule set for input[type='color']::-moz-color-swatch!

Animated gif. Shows how to enable viewing applied user agent styles in Firefox DevTools: Settings  data-recalc-dims= Inspector > check the Show Browser styles checkbox.”/>
Enable viewing browser styles in Firefox DevTools.

In pre-Chromium Edge, we cannot even see what kind of structure we have inside. I gave ::-ms-color-swatch a try, but it didn’t work and neither did ::-ms-swatch (which I considered because, for an input type='range', we have ::-webkit-slider-thumb and ::-moz-range thumb, but just ::-ms-thumb).

After a lot of searching, all I found was this issue from 2016. Pre-Chromium Edge apparently doesn’t allow us to style whatever is inside this input. Well, that’s a bummer.

How to look at the browser styles

In all browsers, we have the option of not applying any styles of our own and then looking at the computed styles.

In Chrome and Firefox, we can also see the user agent stylesheet rule sets that are affecting the currently selected element (though we need to explicitly enable this in Firefox, as seen in the previous section).

Screenshot collage of Chrome DevTools and Firefox DevTools showing where to look for user agent styles: Elements  data-recalc-dims= Styles in Chrome and Inspector > Styles in Firefox.”/>
Checking browser styles in Chrome and Firefox.

This is oftentimes more helpful than the computed styles, but there are exceptions and we should still always check the computed values as well.

In Firefox, we can also see the CSS file for the form elements at view-source:resource://gre-resources/forms.css.

Screenshot showing view-source:resource://gre-resources/forms.css open in Firefox to allow us seeing user agent styles for form elements.
Checking browser styles in Firefox.

The input element itself

We’ll now be taking a look at the default values of a few properties in various browsers in order to get a clear picture of what we’d really need to set explicitly in order to get a custom cross-browser result.

The first property I always think about checking when it comes to <input> elements is box-sizing. The initial value of this property is border-box in Firefox, but content-box in Chrome and Edge.

Comparative screenshots of DevTools in the three browsers showing the computed values of box-sizing for the actual input.
The box-sizing values for <input type='color'> compared in Chrome, Firefox and Edge (from top-to-bottom).

We can see that Firefox is setting it to border-box on <input type='color'>, but it looks like Chrome isn’t setting it at all, so it’s left with the initial value of content-box (and I suspect the same is true for Edge).

In any event, what it all means is that, if we are to have a border or a padding on this element, we also need to explicitly set box-sizing so that we get a consistent result across all these browsers.

The font property value is different for every browser, but since we don’t have text inside this input, all we really care about is the font-size, which is consistent across all browsers I’ve checked: 13.33(33)px. This is a value that really looks like it came from dividing 40px by 3, at least in Chrome.

Comparative screenshots of DevTools in the three browsers showing the font values for the actual input.
The font values for <input type='color'> compared in Chrome, Firefox and Edge (from top-to-bottom).

This is a situation where the computed styles are more useful for Firefox, because if we look at the browser styles, we don’t get much in terms of useful information:

Screenshot of what we get if we look at the browser styles where the font was set for Firefox. The value for the font is -moz-field, which is an alias for the look of a native text field. Expanding this to check the longhands shows us empty instead of actual values.
Sometimes the browser styles are pretty much useless (Firefox screenshot).

The margin is also consistent across all these browsers, computing to 0.

Comparative screenshots of DevTools in the three browsers showing the margin values for the actual input.
The margin values for <input type='color'> compared in Chrome, Firefox and Edge (from top-to-bottom).

The border is different for every single browser. In both Chrome and Edge, we have a solid 1px one, but the border-color is different (rgb(169, 169, 169) for Chrome and rgb(112, 112, 112) for Edge). In Firefox, the border is an outset 2px one, with a border-color of… ThreeDLightShadow?!

Comparative screenshots of DevTools in the three browsers showing the border values for the actual input.
The border values for <input type='color'> compared in Chrome, Firefox and Edge (from top-to-bottom).

What’s the deal with ThreeDLightShadow? If it doesn’t sound familiar, don’t worry! It’s a (now deprecated) CSS2 system value, which Firefox on Windows shows me to be rgb(227, 227, 227) in the Computed styles tab.

Screenshot of Computed panel search in Firefox on Windows, showing that the ThreeDLightShadow keyword computes to rgb(227, 227, 227).
Computed border-color for <input type='color'> in Firefox on Windows.

Note that in Firefox (at least on Windows), the operating system zoom level (SettingsSystemDisplayScale and LayoutChange the size of text, apps and other items) is going to influence the computed value of the border-width, even though this doesn’t seem to happen for any other property I’ve checked and it seems to be partially related to the border-style.

Screenshot showing the Windows display settings window with the zoom level options dropdown opened.
Zoom level options on Windows.

The strangest thing is the computed border-width values for various zoom levels don’t seem to make any sense. If we keep the initial border-style: outset, we have:

  • 1.6px for 125%
  • 2px for 150%
  • 1.7px for 175%
  • 1.5px for 200%
  • 1.8px for 225%
  • 1.6px for 250%
  • 1.66667px for 300%

If we set border-style: solid, we have a computed border-width of 2px, exactly as it was set, for zoom values that are multiples of 50% and the exact same computed values as for border-style: outset for all the other zoom levels.

The padding is the same for Chrome and Edge (1px 2px), while Firefox is the odd one out again.

Comparative screenshots of DevTools in the three browsers showing the padding values for the actual input.
The padding values for <input type='color'> compared in Chrome, Firefox and Edge (from top-to-bottom).

It may look like the Firefox padding is 1px. That’s what it is set to and there’s no indication of anything overriding it — if a property is overridden, then it’s shown as grey and with a strike-through.

Screenshot of Firefox DevTools highlighting how the border set on input[type='color'] overrides the one set on input and the look (grey + strike-through) of overridden properties.
Spotting overrides in Firefox.

But the computed value is actually 0 8px! Moreover, this is a value that doesn’t depend on the operating system zoom level. So, what the hairy heck is going on?!

Screenshot of Firefox DevTools showing how the computed padding value on <input type='color' data-recalc-dims= isn’t the one that was set on input, even if no override seems to be happening.”/>
Computed value for padding in Firefox doesn’t match the value that was set on input.

Now, if you’ve actually tried inspecting a color input, took a close look at the styles set on it, and your brain works differently than mine (meaning you do read what’s in front of you and don’t just scan for the one thing that interests you, completely ignoring everything else…) then you’ve probably noticed there is something overriding the 1px padding (and should be marked as such) — the flow-relative padding!

Screenshot of Firefox DevTools showing the flow-relative padding overriding the old padding due to higher specificity of selector (input[type='color'] vs. input).
Flow-relative padding overrides in Firefox.

Dang, who knew those properties with lots of letters were actually relevant? Thanks to Zoltan for noticing and letting me know. Otherwise, it probably would have taken me two more days to figure this one out.

This raises the question of whether the same kind of override couldn’t happen in other browsers and/or for other properties.

Edge doesn’t support CSS logical properties, so the answer is a “no” in that corner.

In Chrome, none of the logical properties for margin, border or padding are set explicitly for <input type='color'>, so we have no override.

Concerning other properties in Firefox, we could have found ourselves in the same situation for margin or for border, but with these two, it just so happens the flow-relative properties haven’t been explicitly set for our input, so again, there’s no override.

Even so, it’s definitely something to watch out for in the future!

Moving on to dimensions, our input’s width is 44px in Chrome and Edge and 64px in Firefox.

Comparative screenshots of DevTools in the three browsers showing the width values for the actual input.
The width values for <input type='color'> compared in Chrome, Firefox and Edge (from top-to-bottom).

Its height is 23px in all three browsers.

Comparative screenshots of DevTools in the three browsers showing the height values for the actual input.
The height values for <input type='color'> compared in Chrome, Firefox and Edge (from top-to-bottom).

Note that, since Chrome and Edge have a box-sizing of content-box, their width and height values do not include the padding or border. However, since Firefox has box-sizing set to border-box, its dimensions include the padding and border.

Comparative screenshots of DevTools in the three browsers showing the layout boxes.
The layout boxes for <input type='color'> compared in Chrome, Firefox and Edge (from top-to-bottom).

This means the content-box is 44pxx23px in Chrome and Edge and 44xpxx19px in Firefox, the padding-box is 48pxx25 in Chrome and Edge and 60pxx19px in Firefox and the border-box is 50pxx27px in Chrome and Edge and 64pxx23 in Firefox.

We can clearly see how the dimensions were set in Chrome and I’d assume they were set in the same direct way in Edge as well, even if Edge doesn’t allow us to trace this stuff. Firefox doesn’t show these dimensions as having been explicitly set and doesn’t even allow us to trace where they came from in the Computed tab (as it does for other properties like border, for example). But if we look at all the styles that have been set on input[type='color'], we discover the dimensions have been set as flow-relative ones (inline-size and block-size).

Screenshot of the Firefox user agent styles showing flow relative dimensions being set on input[type='color'].
How <input type='color'> dimensions have been set in Firefox.

The final property we check for the normal state of the actual input is background. Here, Edge is the only browser to have a background-image (set to a top to bottom gradient), while Chrome and Firefox both have a background-color set to ButtonFace (another deprecated CSS2 system value). The strange thing is this should be rgb(240, 240, 240) (according to this resource), but its computed value in Chrome is rgb(221, 221, 221).

Comparative screenshots of DevTools in the three browsers showing the background values for the actual input.
The background values for <input type='color'> compared in Chrome, Firefox and Edge (from top-to-bottom).

What’s even stranger is that, if we actually look at our input in Chrome, it sure does look like it has a gradient background! If we screenshot it and then use a picker, we get that it has a top to bottom gradient from #f8f8f8 to #ddd.

Screenshot of the input in Chrome. A very light, almost white, grey to another light, but still darker grey gradient from top to bottom can be seen as the background, not the solid background-color indicated by DevTools.
What the actual input looks like in Chrome. It appears to have a gradient, in spite of the info we get from DevTools telling us it doesn’t.

Also, note that changing just the background-color (or another property not related to dimensions like border-radius) in Edge also changes the background-image, background-origin, border-color or border-style.

Animated gif. Shows the background-image, background-origin, border-color, border-style before and after changing the seemingly unrelated background-color - their values don't get preserved following this change.
Edge: side-effects of changing background-color.

Other states

We can take a look at the styles applied for a bunch of other states of an element by clicking the :hov button in the Styles panel for Chrome and Firefox and the a: button in the same Styles panel for Edge. This reveals a section where we can check the desired state(s).

Screenshot collage highlighting the buttons that bring up the states panel in Chrome, Firefox and Edge.
Taking a look at other states in Chrome, Firefox, Edge (from top to bottom).

Note that, in Firefox, checking a class only visually applies the user styles on the selected element, not the browser styles. So, if we check :hover for example, we won’t see the :hover styles applied on our element. We can however see the user agent styles matching the selected state for our selected element shown in DevTools.

Also, we cannot test for all states like this and let’s start with such a state.

:disabled

In order to see how styles change in this state, we need to manually add the disabled attribute to our <input type='color'> element.

Hmm… not much changes in any browser!

In Chrome, we see the background-color is slightly different (rgb(235, 235, 228) in the :disabled state versus rgb(221, 221, 221) in the normal state).

Chrome DevTools screenshot showing the background being set to rgb(235, 235, 228) for a :disabled input.
Chrome :disabled styling.

But the difference is only clear looking at the info in DevTools. Visually, I can tell tell there’s a slight difference between an input that’s :disabled and one that’s not if they’re side-by-side, but if I didn’t know beforehand, I couldn’t tell which is which just by looking at them, and if I just saw one, I couldn’t tell whether it’s enabled or not without clicking it.

Disabled and enabled input side by side in Chrome. There is a slight difference in background-color, but it's pretty much impossible to tell which is which just by looking at them.
Disabled (left) versus enabled (right) <input type='color'> in Chrome.

In Firefox, we have the exact same values set for the :disabled state as for the normal state (well, except for the cursor, which realistically, isn’t going to produce different results save for exceptional cases anyway). What gives, Firefox?!

Comparison of styles set in Firefox for <input type='color' data-recalc-dims= in its normal state and its :disabled state. The padding and border set in the :disabled case are exactly the same as those set in the normal case.”/>
Firefox :disabled (top) versus normal (bottom) styling.

In Edge, both the border-color and the background gradient are different.

Chrome DevTools screenshot showing border-color and the background-image being set to slightly different values for a :disabled input.
Edge :disabled styling (by checking computed styles).

We have the following styles for the normal state:

border-color: rgb(112, 112, 112);
background-image: linear-gradient(rgb(236, 236, 236), rgb(213, 213, 213));

And for the :disabled state:

border-color: rgb(186, 186, 186);
background-image: linear-gradient(rgb(237, 237, 237), rgb(229, 229, 229));

Clearly different if we look at the code and visually better than Chrome, though it still may not be quite enough:

Disabled and enabled input side by side in Edge. There is a slight difference in background-image and a bigger difference in border-color, but it still may be difficult to tell whether an input is enabled or not at first sight without having a reference to compare.
Disabled (left) versus enabled (right) <input type='color'> in Edge.
:focus

This is one state we can test by toggling the DevTools pseudo-classes. Well, in theory. In practice, it doesn’t really help us in all browsers.

Starting with Chrome, we can see that we have an outline in this state and the outline-color computes to rgb(77, 144, 254), which is some kind of blue.

Chrome DevTools screenshot showing an outline for an input having <code data-recalc-dims=:focus.”/>
Chrome :focus styling.

Pretty straightforward and easy to spot.

Moving on to Firefox, things start to get hairy! Unlike Chrome, toggling the :focus pseudo-class from DevTools does nothing on the input element, though by focusing it (by tab click), the border becomes blue and we get a dotted rectangle within — but there’s no indication in DevTools regarding what is happening.

Animated gif. Shows how, on :focus, our input gets a blue border and a dark inner dotted rectangle.
What happens in Firefox when tabbing to our input to :focus it.

If we check Firefox’s forms.css, it provides an explanation for the dotted rectangle. This is the dotted border of a pseudo-element, ::-moz-focus-inner (a pseudo-element which, for some reason, isn’t shown in DevTools inside our input as ::-moz-color-swatch is). This border is initially transparent and then becomes visible when the input is focused — the pseudo-class used here (:-moz-focusring) is pretty much an old Firefox version of the new standard (:focus-visible), which is currently only supported by Chrome behind the Experimental Web Platform features flag.

Firefox DevTools screenshot where the inner dotted rectangle on :focus comes from: it is set as a transparent border on the ::-moz-focus-inner pseudo-element and it becomes visible when the input should have a noticeable :focus indicator.
Firefox: where the inner dotted rectangle on :focus comes from.

What about the blue border? Well, it appears this one isn’t set by a stylesheet, but at an OS level instead. The good news is we can override all these styles should we choose to do so.

In Edge, we’re faced with a similar situation. Nothing happens when toggling the :focus pseudo-class from DevTools, but if we actually tab to our input to focus it, we can see an inner dotted rectangle.

Animated gif. Shows how, on :focus, our input gets an inner dotted rectangle.
What happens in Edge when tabbing to our input to :focus it.

Even though I have no way of knowing for sure, I suspect that, just like in Firefox, this inner rectangle is due to a pseudo-element that becomes visible on :focus.

:hover

In Chrome, toggling this pseudo-class doesn’t reveal any :hover-specific styles in DevTools. Furthermore, actually hovering the input doesn’t appear to change anything visually. So it looks like Chrome really doesn’t have any :hover-specific styles?

In Firefox, toggling the :hover pseudo-class from DevTools reveals a new rule in the styles panel:

Screenshot of Firefox DevTools showing the rule set that shows up for the :hover state.
Firefox :hover styling as seen in DevTools.

When actually hovering the input, we see the background turns light blue and the border blue, so the first thought would be that light blue is the -moz-buttonhoverface value and that the blue border is again set at an OS level, just like in the :focus case.

Animated gif. Shows that, on actually hovering our <input type='color' data-recalc-dims=, it gets a light blue background and a blue border.”/>
What actually happens in Firefox on :hover.

However, if we look at the computed styles, we see the same background we have in the normal state, so that blue background is probably really set at an OS level as well, in spite of having that rule in the forms.css stylesheet.

Screenshot of Firefox DevTools showing the computed value for background-color in the :hover state.
Firefox: computed background-color of an <input type='color'> on :hover.

In Edge, toggling the :hover pseudo-class from DevTools gives our input a light blue (rgb(166, 244, 255)) background and a blue (rgb(38, 160, 218)) border, whose exact values we can find in the Computed tab:

Screenshot of Edge DevTools showing the computed value for background-color and border-color in the :hover state.
Edge: computed background-color and border-color of an <input type='color'> on :hover.
:active

Checking the :active state in the Chrome DevTools does nothing visually and shows no specific rules in the Styles panel. However, if we actually click our input, we see that the background gradient that doesn’t even show up in DevTools in the normal state gets reversed.

Screenshot of the input in :active state in Chrome. A very light, almost white, grey to another light, but still darker grey gradient from bottom to top can be seen as the background, not the solid background-color indicated by DevTools.
What the actual input looks like in Chrome in the :active state. It appears to have a gradient (reversed from the normal state), in spite of the info we get from DevTools telling us it doesn’t.

In Firefox DevTools, toggling the :active state on does nothing, but if we also toggle the :hover state on, then we get a rule set that changes the inline padding (the block padding is set to the same value of 0 it has in all other states), the border-style and sets the background-color back to our old friend ButtonFace.

Screenshot of Firefox DevTools showing the rule set that shows up for the :active state.
Firefox :active styling as seen in DevTools.

In practice, however, the only thing that matches the info we get from DevTools is the inline shift given by the change in logical padding. The background becomes a lighter blue than the :hover state and the border is blue. Both of these changes are probably happening at an OS level as well.

Animated gif. Shows that, on actually clicking our <input type='color' data-recalc-dims=, it gets a light blue background and a blue border in addition to sliding 1 pixel in the inline direction as a result of changing the inline padding.”/>
What actually happens in Firefox in an :active state.

In Edge, activating the :active class from DevTools gives us the exact same styles we have for the :hover state. However, if we have both the :hover and the :active states on, things change a bit. We still have a light blue background and a blue border, but both are darker now (rgb(52, 180, 227) for the background-color and rgb(0, 137, 180) for the border-color):

Screenshot of Edge DevTools showing the computed value for background-color and border-color in the :active state.
The computed background-color and border-color of an <input type='color'> on :active viewed in Edge.

This is the takeaway: if we want a consistent cross-browser results for <input type='color'>, we should define our own clearly distinguishable styles for all these states ourselves because, fortunately, almost all the browser defaults — except for the inner rectangle we get in Edge on :focus — can be overridden.

The swatch wrapper

This is a component we only see in Chrome, so if we want a cross-browser result, we should probably ensure it doesn’t affect the swatch inside — this means ensuring it has no margin, border, padding or background and that its dimensions equal those of the actual input’s content-box.

In order to know whether we need to mess with these properties (and maybe others as a result) or not, let’s see what the browser defaults are for them.

Fortunately, we have no margin or border, so we don’t need to worry about these.

Chrome DevTools screenshot showing the margin and border values for the swatch wrapper.
The margin and border values for the swatch wrapper in Chrome.

We do however have a non-zero padding (of 4px 2px), so this is something we’ll need to zero out if we want to achieve a consistent cross-browser result.

Chrome DevTools screenshot showing the padding values for the swatch wrapper.
The padding values for the swatch wrapper in Chrome.

The dimensions are both conveniently set to 100%, which means we won’t need to mess with them.

Chrome DevTools screenshot showing the size values for the swatch wrapper.
The size values for the swatch wrapper in Chrome.

Something we need to note here is that we have box-sizing set to border-box, so the padding gets subtracted from the dimensions set on this wrapper.

Chrome DevTools screenshot showing the box-sizing value for the swatch wrapper.
The box-sizing value for the swatch wrapper in Chrome.

This means that while the padding-box, border-box and margin-box of our wrapper (all equal because we have no margin or border) are identical to the content-box of the actual <input type='color'> (which is 44pxx23px in Chrome), getting the wrapper’s content-box involves subtracting the padding from these dimensions. It results that this box is 40pxx15px.

Chrome DevTools screenshot showing the box model for the swatch wrapper.
The box model for the swatch wrapper in Chrome.

The background is set to transparent, so that’s another property we don’t need to worry about resetting.

Chrome DevTools screenshot showing the background values for the swatch wrapper.
The background values for the swatch wrapper in Chrome.

There’s one more property set on this element that caught my attention: display. It has a value of flex, which means its children are flex items.

Chrome DevTools screenshot showing the display value for the swatch wrapper.
The display value for the swatch wrapper in Chrome.

The swatch

This is a component we can style in Chrome and Firefox. Sadly, Edge doesn’t expose it to allow us to style it, so we cannot change properties we might want to, such as border, border-radius or box-shadow.

The box-sizing property is one we need to set explicitly if we plan on giving the swatch a border or a padding because its value is content-box in Chrome, but border-box in Firefox.

Comparative screenshots of DevTools in the two browsers showing the computed values of box-sizing for the swatch component.
The box-sizing values for the swatch viewed in Chrome (top) and Firefox (bottom).

Fortunately, the font-size is inherited from the input itself so it’s the same.

Comparative screenshots of DevTools in the two browsers showing the computed values of font-size for the swatch component.
The font-size values for the swatch viewed in Chrome (top) and Firefox (bottom).

The margin computes to 0 in both Chrome and Firefox.

Comparative screenshots of DevTools in the two browsers showing the computed values of margin for the swatch component.
The margin values for the swatch viewed in Chrome (top) and Firefox (bottom).

This is because most margins haven’t been set, so they end up being 0 which is the default for <div> elements. However, Firefox is setting the inline margins to auto and we’ll be getting to why that computes to 0 in just a little moment.

Screenshot of Firefox DevTools.
The inline margin for the swatch being set to auto in Firefox.

The border is solid 1px in both browsers. The only thing that differs is the border-color, which is rgb(119, 119, 119) in Chrome and grey (or rgb(128, 128, 128), so slightly lighter) in Firefox.

Comparative screenshots of DevTools in the two browsers showing the computed values of border for the swatch component.
The border values for the swatch viewed in Chrome (top) and Firefox (bottom).

Note that the computed border-width in Firefox (at least on Windows) depends on the OS zoom level, just as it is in the case of the actual input.

The padding is luckily 0 in both Chrome and Firefox.

Comparative screenshots of DevTools in the two browsers showing the computed values of padding for the swatch component.
The padding values for the swatch viewed in Chrome (top) and Firefox (bottom).

The dimensions end up being exactly what we’d expect to find, assuming the swatch covers its parent’s entire content-box.

Comparative screenshots of DevTools in the two browsers showing the box model for the swatch component.
The box model for the swatch viewed in Chrome (top) and Firefox (bottom).

In Chrome, the swatch parent is the <div> wrapper we saw earlier, whose content-box is 4pxx15px. This is equal to the margin-box and the border-box of the swatch (which coincide as we have no margin). Since the padding is 0, the content-box and the padding-box for the swatch are identical and, subtracting the 1px border, we get dimensions that are 38pxx13px.

In Firefox, the swatch parent is the actual input, whose content-box is 44pxx19px one. This is equal to the margin-box and the border-box of the swatch (which coincide as we have no margin). Since the padding is 0, the content-box and the padding-box for the swatch are identical and, subtracting the 1px border, we get that their dimensions are 42pxx17px.

In Firefox, we see that the swatch is made to cover its parent’s content-box by having both its dimensions set to 100%.

Comparative screenshots of DevTools in the two browsers showing the size values for the swatch component.
The size values for the swatch viewed in Chrome (top) and Firefox (bottom).

This is the reason why the auto value for the inline margin computes to 0.

But what about Chrome? We cannot see any actual dimensions being set. Well, this result is due to the flex layout and the fact that the swatch is a flex item that’s made to stretch such that it covers its parent’s content-box.

Chrome DevTools screenshot showing the flex value for the swatch wrapper.
The flex value for the swatch wrapper in Chrome.

Final thoughts

Phew, we covered a lot of ground here! While it may seem exhaustive to dig this deep into one specific element, this is the sort of exercise that illustrates how difficult cross-browser support can be. We have our own styles, user agent styles and operating system styles to traverse and some of those are always going to be what they are. But, as we discussed at the very top, this winds up being an accessibility issue at the end of the day, and something to really consider when it comes to implementing a practical, functional application of a color input.

Remember, a lot of this is ripe territory to reach out to browser vendors and let them know how they can update their implementations based on your reported use cases. Here are the three tickets I mentioned earlier where you can either chime in or reference to create a new ticket:


Color Inputs: A Deep Dive into Cross-Browser Differences originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/color-inputs-a-deep-dive-into-cross-browser-differences/feed/ 6 288072
Restricting a (pseudo) element to its parent’s border-box https://css-tricks.com/restricting-a-pseudo-element-to-its-parents-border-box/ https://css-tricks.com/restricting-a-pseudo-element-to-its-parents-border-box/#comments Tue, 02 Jul 2019 14:19:04 +0000 http://css-tricks.com/?p=287192 Have you ever wanted to ensure that nothing of a (pseudo) element gets displayed outside its parent’s border-box? In case you’re having trouble picturing what that looks like, let’s say we wanted to get the following result with minimal …


Restricting a (pseudo) element to its parent’s border-box originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Have you ever wanted to ensure that nothing of a (pseudo) element gets displayed outside its parent’s border-box? In case you’re having trouble picturing what that looks like, let’s say we wanted to get the following result with minimal markup and avoiding brittle CSS.

Screenshot of the result we want to get, highlighting the fact that even though an element has both a padding and a border, its descendant gets clipped to the limit of its border.
The desired result.

This means we cannot add any elements just for visual purposes and we cannot create shapes out of multiple pieces, whether that’s directly or via masks. We also want to avoid long, long lists of anything (think something like tens of background layers or box shadows or points inside a polygon() function) in our generated code because, while the results can be fun, it’s not really practical to do something like that!

How do you think we can achieve this, given the parts the arrows point towards? Fancy giving it a try before checking my solution below? It’s one of those things that seems simple at first, but once you actually try it, you discover it’s much trickier.

Markup

Each item is a paragraph (<p>) element. I was lazy and generated them with Pug out of an array of objects which hold the item’s gradient stop list and its paragraph text:

- var data = [
-   {
-     slist: ['#ebac79', '#d65b56'], 
-     ptext: 'Pancake muffin chocolate syrup brownie.'
-   }, 
-   {
-     slist: ['#90cbb7', '#2fb1a9'], 
-     ptext: 'Cake lemon berry muffin plum macaron.'
-   }, 
-   {
-     slist: ['#8a7876', '#32201c'], 
-     ptext: 'Wafer apple tart pie muffin gingerbread.'
-   }, 
-   {
-     slist: ['#a6c869', '#37a65a'], 
-     ptext: 'Liquorice plum topping chocolate lemon.'
-   }
- ].reverse();
- var n = data.length;

while n--
  p(style=`--slist: ${data[n].slist}`) #{data[n].ptext}

This generates the following unspectacular HTML:

<p style='--slist: #ebac79, #d65b56'>Pancake muffin chocolate syrup brownie.</p>
<p style='--slist: #90cbb7, #2fb1a9'>Cake lemon berry muffin plum macaron.</p>
<p style='--slist: #8a7876, #32201c'>Wafer apple tart pie muffin gingerbread.</p>
<p style='--slist: #a6c869, #37a65a'>Liquorice plum topping chocolate lemon.</p>

Basic styles

For the paragraph elements, we set a font, dimensions and a border-radius that’s half the height value:

$w: 26em;
$h: 5em;

p {
  width: $w; height: $h;
  border-radius: .5*$h;
  background: silver;
  font: 1.5em/ 1.375 trebuchet ms, verdana, sans-serif;
}

We’ve also set a dummy background so we can see their limits:

See the Pen by thebabydino (@thebabydino) on CodePen.

Paragraph background

We have three top-to-bottom gradients, which means we can place each of them within the limits of a different layout box: the top gradient layer is limited to the content-box, the middle one to the padding-box and the bottom one to the border-box. If you need an in-depth refresher on this technique, check out this article, but the basic idea is you picture these layout boxes as nested rectangles.

Illustration showing the layout boxes. The outermost box is the border-box. Inside it, a border-width away from the border limit, we have the padding-box. And finally, inside the padding-box, a padding away from the padding limit, we have the content-box.
The layout boxes. (Demo)

This is pretty much how browser DevTools presents them.

Screenshot collage showing the graphical representation of the layout boxes in browsers' DevTools.
The layout boxes as shown by Chrome (left) vs. Firefox (middle) vs. Edge (right).

You may be wondering why we wouldn’t layer gradients with different sizes given by their background-size and that have background-repeat: no-repeat. Well, this is because we only get rectangles without rounded corners this way.

Using the background-clip method, if we have a border-radius, our background layers will follow that. Meanwhile, the actual border-radius we set is being used to round the corners of the border-box; that same radius minus the border-width rounding the corners of the padding-box. Then we’re subtracting the padding as well to round the corners of the content-box.

See the Pen by thebabydino (@thebabydino) on CodePen.

So let’s code this!

We set a transparent border and a padding. We make sure they get subtracted from the dimensions we’ve set by switching to box-sizing: border-box. Finally, we layer three gradients: the top one restricted to the content-box, the middle one to the padding-box and the bottom one to the border-box.

p {
  /* same styles as before */
  display: flex;
  align-items: center;
  justify-content: space-between;
  box-sizing: border-box;
  border: solid $b transparent;
  padding: $p;
  background: 
    linear-gradient(#dbdbdb, #fff) content-box, 
    linear-gradient(var(--slist)) padding-box, 
    linear-gradient(#fff, #dcdcdc) border-box;
  text-indent: 1em;
}

We’ve also set a flex layout and a text-indent to move the text content away from the banner edges:

See the Pen by thebabydino (@thebabydino) on CodePen.

Numbering

Before we move on to the tricky part, let’s get the paragraph numbers out of the way!

We add them using a counter we set as the content value on the :after pseudo-element. We first make this :after a square whose edge length equals the paragraph height (which is $h) minus the top and bottom border-width (both equal to $b). Then we turn this square into a circle by setting border-radius: 50% on it. We make it inherit its parent’s box-sizing and border and we then set its background in a similar manner as we did for its parent.

$d: $h - 2*$b;

p {
  /* same styles as before */
  counter-increment: c;

  &:after {
    box-sizing: inherit;
    border: inherit;
    width: $d; height: $d;
    border-radius: 50%;
    box-shadow: 
      inset 0 0 1px 1px #efefef, 
      inset 0 #{-$b} rgba(#000, .1);
    background: 
      linear-gradient(var(--slist)) padding-box, 
      linear-gradient(#d0d0d0, #e7e7e7) border-box;
    color: #fff;
    content: counter(c, decimal-leading-zero);
  }
}

Alright, this is starting to look like something!

See the Pen by thebabydino (@thebabydino) on CodePen.

We still need to make a few tweaks to the CSS of this :after pseudo-element — a margin-right that’s minus its parent’s padding and adjustments to its inner layout so we have the number dead in the middle. That’s pretty much it for the numbering part!

p {
  /* same styles as before */

  &:after {
    /* same styles as before */
    display: grid;
    place-content: center;
    margin-right: -$p;
    text-indent: 0;
  }
}

We’re getting closer!

See the Pen by thebabydino (@thebabydino) on CodePen.

The tricky part

And we’re finally here!

We start off by using the :before pseudo-element, absolutely positioning it on the right side and making it a square whose edge length equals its parent’s height:

p {
  /* same styles as before */
  position: relative;
  outline: solid 2px orange;

  &:before {
    position: absolute;
    right: -$b;
    width: $h;
    height: $h;
    outline: solid 2px purple;
    content: '';
  }
}

We’ve also given both this pseudo-element and its parent some dummy outlines so that we can check the alignment:

See the Pen by thebabydino (@thebabydino) on CodePen.

Alright, we now give this :before a dummy background, rotate it, and then give it a border-radius and a nice box-shadow:

p {
  /* same styles as before */

  &:before {
    /* same styles as before */
    border-radius: $b;
    transform: rotate(45deg);
    box-shadow: 0 0 7px rgba(#000, .2);
    background: linear-gradient(-45deg, orange, purple);
  }
}

And we get the following result!

See the Pen by thebabydino (@thebabydino) on CodePen.

We now have a small problem: the :before pseudo-element is absolutely positioned and is now on top of the :after pseudo-elements that holds the numbering! We can fix this by setting position: relative on the :after pseudo-element.

See the Pen by thebabydino (@thebabydino) on CodePen.

Here’s where things start getting interesting!

Restricting the background gradient

First, we need to set the stop positions on the gradient of our :before pseudo-element such that they match the bottom and top edges of the parent. This is because we want to have a certain hex value along the top edge of the parent and a certain hex value along the bottom edge of the parent.

Annotated illustration. Shows the parent paragraph and its rotated :before pseudo-element, the gradient direction and the stop lines at the positions we're looking for.
The stop positions we need to compute.

Since we’ve rotated our square :before by 45°, its top-left corner now points upward (and, conversely, its bottom-right corner points downward).

Animated .gif. Shows the square :before positioned on the right of its parent. Its top left corner and bottom right corner are highlighted as well as its vertical axis (vertical line passing through the intersection of its diagonals). Rotating our square by 45° means its top left corner now points up (and its bottom right corner points down).
How rotation changes position of the square’s corners.

A gradient to the top-left corner of a square is a gradient in the -45° direction (because the angle is at 12 o’clock and the positive direction is, just as for transforms, the clockwise one). A gradient to a corner means the 100% point is located in that corner)

Animated .gif. Shows the square :before positioned on the right of its parent. Shows the linear gradient to the top left corner. After the rotation, since the top left corner points up, the gradient direction also goes up.
How rotation changes the gradient direction.

The 50% line of a gradient always passes through the midpoint (the point at the intersection of the diagonals) of the gradient box.

The gradient box is the box within which we paint the gradient and whose size is given by the background-size. Since we haven’t set a background-size, the default for gradients is to use the entire box defined by background-origin, which is the padding-box by default. Since we don’t have a border or a padding on our :before pseudo-element, it results that all three boxes (content-box, padding-box and border-box) are equal in space between them and equal in proportion to the gradient box.

In our case, we have the following lines perpendicular to the direction of the -45°-pointing gradient line:

Annotated illustration. Shows the parent paragraph and its rotated :before pseudo-element, the gradient direction and the stop lines at 0% and 100%, at the two positions we want to get and at 50%.
Getting the relevant stop positions.
  • the 0% line, passing through the bottom right corner of the :before
  • the bottom edge of the pseudo-element’s paragraph parent
  • the 50% line which splits our square diagonally into two mirrored right isosceles triangles; given the way we’ve aligned our paragraph and its pseudo-elements, this line is also a midline for the paragraph itself, splitting it into two halves, each with a height equal to half the paragraph’s height ($h).
  • the top edge of the pseudo-element’s paragraph parent
  • the 100% line, passing through the top left corner of the :before

This means we need to restrict the -45°-pointing gradient on our :before pseudo-element between calc(50% - #{.5*$h}) (corresponding to the paragraph’s bottom edge) and calc(50% + #{.5*$h}) (corresponding to the paragraph’s top edge).

Sure enough, this does it!

linear-gradient(-45deg, orange calc(50% - #{.5*$h}), purple calc(50% + #{.5*$h}))

See the Pen by thebabydino (@thebabydino) on CodePen.

Adding a sharp transition from and to transparent at these stop positions makes it a lot more obvious they’re the correct ones:

linear-gradient(-45deg, 
      transparent calc(50% - #{.5*$h}), orange 0, 
      purple calc(50% + #{.5*$h}), transparent 0)

See the Pen by thebabydino (@thebabydino) on CodePen.

Restricting the pseudo-element itself

The next step is to prevent the :before pseudo-element from spilling outside the boundaries of its parent.

That’s easy, right? Just set overflow: hidden on the paragraph!

Well, let’s do that!

This is the result we get:

See the Pen by thebabydino (@thebabydino) on CodePen.

Oops, this is not what we were going for!

Side by side comparison of what we have using overflow: hidden (:before gets clipped to its parent's padding-box) and what we actually want to get (:before gets clipped to its parent's border-box).
What we got with overflow: hidden (left) vs. what we want (right).

The problem is overflow: hidden cuts out everything outside an element’s padding-box, but what we want here is to cut the parts of the :before pseudo-element that are outside the border-box, which is larger than the padding-box in our case because we have a non-zero border we cannot ditch (and solve the issue by making the border-box equal to the padding-box) because we need three background layers on our paragraph: the top one covering the content-box, the middle one covering the padding-box and the bottom one covering the border-box.

The solution? Well, if you’ve taken a peek at the tags, you’ve probably guessed by now: use clip-path instead!

Pretty much every article and demo using clip-path out there makes use of either an SVG reference or the polygon() shape function, but these are not the only options we have!

Another possible shape function (and the one we’ll be using here) is inset(). This function specifies a clipping rectangle defined by the distances from the top, right, bottom and left edges. Edges of what? Well, by default1, that’s the edges of the border-box, which is exactly what we need here!

Illustration showing what the four values of the inset() function represent. The first one is the offset of the top edge of the clipping rectangle with respect to the top edge of the border-box. The second one is the offset of the right edge of the clipping rectangle with respect to the right edge of the border-box. The third one is the offset of the bottom edge of the clipping rectangle with respect to the bottom edge of the border-box. The fourth one is the offset of the left edge of the clipping rectangle with respect to the left edge of the border-box.
How the inset() function works. (Demo)

So let’s ditch overflow: hidden and use clip-path: inset(0) instead. This is the result we get:

See the Pen by thebabydino (@thebabydino) on CodePen.

This is better, but not quite what we want because it doesn’t take into account the paragraph’s border-radius. Fortunately, inset() also lets us specify a rounding that can take any border-radius value we want. No joke, any valid border-radius value works — for example, this:

clip-path: inset(0 round 15% 75px 35vh 13vw/ 3em 5rem 29vmin 12.5vmax)

We only need something much simpler though:

$r: .5*$h;

p {
  /* same styles as before */
  border-radius: $r;
  clip-path: inset(0 round $r)
}

And now we finally get the result we wanted:

See the Pen by thebabydino (@thebabydino) on CodePen.

Final touches

Since we don’t want a purple-orange gradient on the :before, we replace those with the actual values we need. We then place the paragraphs in the middle because that looks better. Finally, we give our paragraphs a shadow by setting a drop-shadow() on the body (we cannot use box-shadow on the paragraphs themselves because we’ve used clip-path, which clips out the box-shadow so we wouldn’t see it anyway). And that’s it!

See the Pen by thebabydino (@thebabydino) on CodePen.


  1. We should be able to change this <geometry-box> value, but Chrome does not implement this part of the spec. There is an issue open for this which you can star or where you can leave a comment with your use cases for being able to change the default value.

Restricting a (pseudo) element to its parent’s border-box originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/restricting-a-pseudo-element-to-its-parents-border-box/feed/ 6 287192
Which CSS IS AWESOME makes the most sense if you don’t know CSS well? https://css-tricks.com/which-css-is-awesome-makes-the-most-sense-if-you-dont-know-css-well/ https://css-tricks.com/which-css-is-awesome-makes-the-most-sense-if-you-dont-know-css-well/#comments Thu, 27 Jun 2019 21:29:11 +0000 http://css-tricks.com/?p=291843 Peter-Paul posted this question:


Which CSS IS AWESOME makes the most sense if you don’t know CSS well? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Peter-Paul posted this question:

Note the interesting caveat: only vote in the poll if you don’t know CSS well.

The winning answer was D! You gotta wonder if the result would have been different if the request for non-CSS experts wasn’t there.

I like to think I know CSS OK, so I didn’t vote. My brain goes like this:

  1. I think he’s asking “by default,” so the answer may assume there’s no other CSS doing anything to that text.
  2. I wish I knew why the box was that particular width, but I guess I’ll just assume it’s a set width.
  3. It’s not B because ellipsis stuff requires extra stuff, and doesn’t work on multiple lines like that — unless we’re talking line clamping, which is even weirder.
  4. It’s not C because that requires hiding overflow which is never really a default — that is, except off the top and left of the browser window, I guess. Or in an iframe.
  5. It’s not D because words just don’t break like that unless you do pretty specific stuff.
  6. A actually makes decent sense. It’s weird to look at, but I’ve been dealing with stuff busting out of containers my whole career. C’est la vie.

Remember, we’ve done a deep dive into CSS IS AWESOME before and how it interestingly captures the weirdness of CSS.


Which CSS IS AWESOME makes the most sense if you don’t know CSS well? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/which-css-is-awesome-makes-the-most-sense-if-you-dont-know-css-well/feed/ 5 291843
Perceived Velocity through Version Numbers https://css-tricks.com/perceived-velocity-through-version-numbers/ https://css-tricks.com/perceived-velocity-through-version-numbers/#comments Mon, 29 Apr 2019 23:28:43 +0000 http://css-tricks.com/?p=286957 HTML5 and CSS3 were big. So big that they were buzzwords that actually meant something and were a massive success story in pushing web technology forward. JavaScript names their big releases now too: ES6, ES7, ES8… and it seems …


Perceived Velocity through Version Numbers originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
HTML5 and CSS3 were big. So big that they were buzzwords that actually meant something and were a massive success story in pushing web technology forward. JavaScript names their big releases now too: ES6, ES7, ES8… and it seems like it will keep going that way.

But HTML and CSS are done with that game. Shortly after the whole HTML5/CSS3 thing, the message was that there will be no HTML6/CSS4. There are reasons for that, like perhaps it’s healthier for CSS modules to evolve independently of some global versioning number.

That said… as Dave says:

… the lull in excitement since those days is palpable….

People aren’t equally excited about the big three languages of the web.

I’m on a bit of a quest to understand why these three technologies built to work together are so unequally yoked in popularity and their communities polarized from one another. One end of the spectrum experiences a boom while the other experiences a bust. The rising tide does not lift all boats.

Surely a major version number release for HTML and CSS could spark a ton of fresh enthusiasm.

I’ll help. HTML6 could have HTML imports for web components, <include>, and a multi-select. CSS4 gets container queries, subgrid, standardized form control styling, and transitions to auto dimensions.

To Shared LinkPermalink on CSS-Tricks


Perceived Velocity through Version Numbers originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/perceived-velocity-through-version-numbers/feed/ 4 286957
#171: Movable Stacked Card Row in CSS https://css-tricks.com/video-screencasts/171-movable-stacked-card-row-in-css/ Mon, 29 Apr 2019 14:38:00 +0000 http://css-tricks.com/?page_id=286923 The same basic idea as the cards on the homepage of v17 of this site.

See the Pen
Moving Cards
by Chris Coyier (@chriscoyier)
on CodePen.…


#171: Movable Stacked Card Row in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
The same basic idea as the cards on the homepage of v17 of this site.

See the Pen
Moving Cards
by Chris Coyier (@chriscoyier)
on CodePen.


#171: Movable Stacked Card Row in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
286923
That Time I Tried Browsing the Web Without CSS https://css-tricks.com/that-time-i-tried-browsing-the-web-without-css/ https://css-tricks.com/that-time-i-tried-browsing-the-web-without-css/#comments Wed, 24 Apr 2019 14:12:56 +0000 http://css-tricks.com/?p=286429 CSS is what gives every website its design. Websites sure aren’t very fun and friendly without it! I’ve read about somebody going a week without JavaScript and how the experience resulted in websites that were faster, though certain aspects of …


That Time I Tried Browsing the Web Without CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
CSS is what gives every website its design. Websites sure aren’t very fun and friendly without it! I’ve read about somebody going a week without JavaScript and how the experience resulted in websites that were faster, though certain aspects of them would not function as expected.

But CSS. Turning off CSS while browsing the web wouldn’t exactly make the web far less usable… right? Or, like JavaScript, would some features not work as expected? Out of curiosity, I decided to give it a whirl and rip the CSS flesh off the HTML skeleton while browsing a few sites.

Why, you might ask? Are there any non-masochistic reasons for turning off CSS? Heydon Pickering once tweeted that disabling CSS is a good way to check some accessibility standards:

  1. Common elements like headings, lists, and form controls are semantic and still look good.
  2. A visual hierarchy is still established with default styles.
  3. The content can still be read in a logical order.
  4. Images still exist as <img> tags rather than getting lost as CSS backgrounds.

A WebAIM survey from 2018 reported that 12.5% of users who rely on any sort of assisted technology browse the web with custom stylesheets, which can include doing away with every CSS declaration across a site. And, if we’re talking about slow internet connections, ditching CSS could be one way to consume content faster. There’s also the chance that CSS is disabled for reasons outside our immediate control, like when a server has hiccups of fails to load assets.

As an experiment, I used five websites and a web app without CSS, and this post will cover my experiences. It wound up being a rather eye-opening adventure for me personally, but has also informed me professionally as a developer in ways I hope you’ll see as well.

But first, here’s how to disable CSS

You’re absolutely welcome to live vicariously through me in the form of this post. But for those of you who are feeling up to the task and want to experience a style-less web, here’s how to disable CSS in various browsers:

  • Chrome: There’s actually no setting in Chrome to disable CSS, so we have to resort to an extension, like disable-HTML.
  • Firefox: View > Page Style > No Style
  • Safari: Safari > Preferences... > Show Develop menu in menu bar. Then go to the Develop dropdown and select the “Disable Styles” option.
  • Opera: Like Chrome, we need an extension, and Web Developer fits the bill.
  • Internet Explorer 11: View > Style > No style

I couldn’t find a documented way to disable CSS in Edge, but we can remove CSS from it and any other browser programmatically via the CSS Object Model API in the DevTools console:

var d = document;

for (var s in S = d.styleSheets)
    S[s].disabled = true;

for (var i in I = d.querySelectorAll("[style]"))
    I[i].style = "";

The first loop disables all external and internal styles (in <link> and <style>), and the second eliminates any inline styles. The caveat here, however, is that elements can still dynamically be given new inline styles. To instantly erase them, the best workaround is adding a timer. Something like this:

(f = function(){
    // Remove CSS
    ...

    setTimeout(f, 20);
})();

Alternatively, there are text-only browsers — such as the ancient Lynx — but expect to be living without video, images (including SVGs), and JavaScript.

Through the style-less looking glass…

For each site I surfed without CSS — Amazon, DuckDuckGo, GitHub, Stack Overflow, Wikipedia and contrast checker called Hex Naw — I’ll share my first impressions and put some suggestions out there that might help with the experience.

Get ready, because things might get a bit… appalling. 😱


Website 1: Amazon.com
The Amazon.com homepage with and without CSS
The Amazon.com homepage with CSS (left) and without CSS (right).

There’s no real need for an introduction here. Not only is Amazon a household staple for so many of us, it also powers a huge chunk of the web, thanks to their ubiquitous Amazon Web Services platform.

There’s a vast number of things going on here, so I’ll explore the style-less stuff that gets in my path while finding a product and pretending to purchase it.

The Amazon.com results for a “mac mini” search query
The Amazon.com results for a “mac mini” search query.

On the homepage, I immediately see a sprite sheet used by the site. It’s really in place of where the logo could be, thus making it tough to know whether or not those images are intended to be there. Each sprite contains multiple versions of the logo, and even if I could see the “Amazon” word mark in it, it’s surprisingly that it’s not the global home link. If you’re curious where the home link really is, it’s this structure of spans where the logo is served up as background image… in CSS:

<a href="/ref=nav_logo" class="nav-logo-link" aria-label="Amazon" tabindex="6">
  <span class="nav-sprite nav-logo-base"></span>
  <span class="nav-sprite nav-logo-ext"></span>
  <span class="nav-sprite nav-logo-locale"></span>
</a>

The next problem that arises is that the “Skip to main content” link doesn’t look like a typical skip link, yet it works like one. It turns out to be an <a> element without an href, and JavaScript (yes, I did leave that enabled) is used to mimic anchor functionality.

When I start a search, I have to look further below the “Get started” link to see the suggestions. Under the “Your Lists” and “Your Account” items, it becomes difficult to tell the links apart. They appear all strung together as if they were one super long mega link. I believe it would have been more effective to use a semantic unordered list in this scenario to maintain a sense of hierarchy.

Under all those search suggestions, however, the account and navigation links are easier to read since they’re separated by some space.

Interestingly, the carousel lower down the page is still somewhat functional. If I click the “Previous page” or “Next page” options, the order of the images is changed. However, hopping between those options required me to scroll.

A split view of the carousel on the amazon.com homepage. First is initial state, second shows previous page on top, and third shows next page on top.
The carousel appears with its pages stack on top of another. The previous or next page shows up on top.

Skipping down a bit further, there’s an advertisement element. It contains an “Ad feedback” string that looks static just like what we saw with the “Skip to main content” link earlier. Well, I clicked it anyway and it revealed a form for sharing feedback on the advertisement relevance.

Blue curvy arrow showing destination to ad feedback form when clicking Ad Feedback text under ad
To make the call to action clearer, “Ad feedback” should be a link or button.

You may have missed it, but there’s a blank button above the two groups of form labels and the radios buttons are out of place. The structure is confusing because I don’t know which labels belong to which radio buttons. I mean, I guess I could assume that the first label goes with the first radio input, but that’s exactly what it is: a guess.

What’s also confusing is that there are Submit buttons between the “Close Window,” “Cancel,” and “Send Feedback” options at the bottom of the form. If I press any of these, I’m taken back to the ad. Now, suppose I were blind and using a screen reader to navigate this same part, even with the presence of CSS. I would be told “Submit, button” for two of the buttons and would therefore have zero clue what to do without guessing. It’s another good reminder about the importance of semantics when handling markup (button labels in this case) and being mindful of how much reliance is placed on JavaScript to override web defaults.

Doing a search — let’s say for “Mac Minis” — I can still access and understand the product ratings since they are displayed as text (instead of the tooltips they are otherwise) in place of stars. This is a good example of using a solid textual fallback when an image is used as visual content, but is served as a background image in CSS.

Messy results page displaying sponsored products on top of normal products
The page required me to scroll a while to get to the actual search results. Notice that ginormous overlay of a sponsored product.

Having chosen the Mac Mini with Intel Core i3, I’m greeted by other Mac products above the product I’ve selected and have to navigate beyond them to select the quantity I want to purchase.

Part of product page showing Amazon Prime membership info
The product page displays Amazon Prime membership info slapped between the quantity selection and purchase buttons.

Scroll down, and an “Add to Cart” button is displayed next to a label bearing the same content. That’s redundant and probably unnecessary since a <button> element is capable of holding its own label:

<button>Add to Cart</button>

Next up, we have an offer for an Amazon Prime membership. That’s all fine and dandy, but notice that it’s inserted between the product I’m purchasing and the “Buy Now” button. I have a really hard time knowing whether clicking “Buy Now” is going to add the Mac Mini to checkout, or whether I’m purchasing Amazon Prime instead.

I also wanted to play around a bit, so I tried removing the Mac Mini from my cart once I figured out how to add it. It took me like ten seconds to locate the cart so I could edit it. Turns out it was directly next to “Proceed to checkout (1 item)” link but rams right up alongside it so it all looks like a single link.

Part of shopping cart page showing a Mac Mini added, Cart and Proceed to Checkout links together, and gift card offer with cost after deduction

Overall, it wasn’t difficult to find a product. On the other hand, the path to checkout became more of a headache as I proceeded. There was some poor semantic- and accessibility-related practices that caused confusion, and important buttons and links became more difficult to find.

👍 What the Site Does Well 💡 What the Site Can Improve
Carousels are functional even without styling. The logo relies on a background image, obscuring the path back home.
The content hierarchy is still generally helpful for knowing where we are on a page. Many links and anchors rely on JavaScript and do not appear to be interactive.
The order of elements remains roughly in tact. Links often bump up against each other or are placed outside where they would be relevant.
Great use of fallbacks for product rating that rely on background images. Button labels are either misleading or repetitive.
Form elements fail to align themselves properly.
There’s a rough journey to check out.
Website 2: DuckDuckGo
The DuckDuckGo homepage with and without CSS
The DuckDuckGo homepage with CSS (left) and without CSS (right).

Have you used DuckDuckGo before? I assume many folks reading CSS-Tricks have, but for those who may be hearing of it for the first time, it’s an alternative to Google search with an emphasis on user privacy.

So, getting started with this is a little misleading because the DuckDuckGo homepage is super simple. Not much can go wrong there, right? Well, it’s a little more involved than that since we’re dealing with search results, content hierarchy and relevance once we get into making search queries.

Top of DuckDuckGo homepage

Right off the bat, what I’m greeted with is a lot more content than I would have expected for such a simple lander. At it’s not totally clear what website this is by scanning the website. The first mention of the product name is the fourth item in the first unordered list and it’s a call to action to “Spread DuckDuckGo.” The logo is totally missing, which obviously means it’s used as a background… in CSS.

Speaking of that unordered list, I assume what I’m seeing belongs in the header, and there’s no skip navigation. We have a triple arrow icon (is that a mobile menu or a menu to hide the least important items, or something else?), followed by privacy-related content, social media links, something that looks like one link but is actually two links for “About DuckDuckGo” and “Learn More.”

Finally, toward the very bottom is where the primary use case for the site actually comes up: the search bar. I assume the “S” label means “Search” and the “X” label is shorthand to clear the search field.

Alright, onto performing a search. It’s super cool that I can still see auto-suggestions and use the up and down arrow keys to highlight each one. Clearing the field though, the suggestions don’t disappear until after I refresh the page.

Performing a search and checking out the auto-suggestions
Performing a search and checking out the auto-suggestions.

Everything in the Settings menu are items in a list including what should be headings — “Settings,” “Privacy Essentials,” “Why Privacy,” “Who We Are,” and “Keep in Touch.” These are very likely part of a mobile men if CSS was enabled, perhaps triggered by that triple arrow link thing at the top. In that menu, I see four blank bullet points between “Settings” and “More Themes.”

Orange arrows pointing to run-on links, unclear button labels, and empty list items
The DuckDuckGo homepage exposed a few glaring usability issues right off the bat.

Coming here as a new user, I have no idea what those empty list items are, but the bullets I highlighted in the screenshot above are actually the theme buttons. To clarify the intent, some fallback text would be helpful, and these should be radio or normal buttons instead of list items (considering their functionality).

Every block of content with an “X” — including the “Settings” — cannot be dismissed; however, clicking the “X” above an image of a hiker image does cause a chunk of content to clear off the screen — thanks to JavaScript still being enabled. What I really find awkward is the redundant numeration in the ordered list under “Switch to DuckDuckGo…” We see this:

1. 1We don’t store your personal info
2. 2We don’t follow you around with ads
3. 3We don’t track you. Ever.

Looks like some mixed use case of semantic markup with some other way to display list item numbers.

Clicking each X to find that only the third has functionality
The third “X” down has functionality.

There’s a colossal amount of white space under the hiker image until the first <h1> element. Assuming they’re either links or buttons, clicking every instance of “Add DuckDuckGo to [browser]” does nothing. Each section’s illustration causes some unnecessary horizontal scrolling, which is a common issue we’ll see in the other sites we look at.

Scrolling through white space between hiker image and first-level heading
Scrolling through white space between hiker image and first-level heading. Wheee!

After those sections, there’s a blank box and I have no idea what it is.

A blank box at the bottom of the page
A blank box that appears to have no purpose.

I cracked open DevTools and it turns out to be a <body> element in an <iframe> that holds only JavaScript for something related to POST requests. It might as well be one of those elements we should leave alone.

Following that, I see two repeated instances of “Set as Default Search Engine” wrapped around a “Set as Homepage” section.

Instructions in Safari to set the search engine as your default or your homepage
The instructions in Safari to set the search engine as your default or your homepage. Instructions may differ from one browser to another.

These must have been the instructions that popped up when I clicked the “Add DuckDuckGo…” actions, but it shows the impact hiding and showing content can have when we’re dealing with straight markup. Instead of repeating content, the corresponding links or buttons should point to one instance. That would cut the redundancy here.

OK, time to finally get into search. The first thing I see in the search results is an empty box with an instruction to ignore the box. Okey-dokey then.

Orange arrow pointing at a tiny box on the search results page saying “Sure, OK.”
DuckDuckGo wants me to ignore a box.

Moving on, did you see that DuckDuckGo link? That must be the logo, and I wonder why this was not on the homepage. Seems like low-hanging fruit for improvement.

The search bar still functions normally with the exception of the “S” and “X” buttons that have swapped places from where they were on the homepage.

Onto the search results. I could easily distinguish one result from another. What I found quite unnecessary, yet funny, is that the “Your browser indicates if you’ve visited this link” messaging that’s located at the end of each page title. That would be super annoying from a screen reading perspective. Imagine hearing that repeated at the end of every page title. That messaging is there to be displayed alongside checkmarks that contain tooltips that hold that messaging. But, with CSS disabled, well, no checkmarks and no tooltips. As a result, all I get is an extra long heading.

Comparison of search results page with and without CSS. Extra text appears next to titles in the non-CSS version.
Search results on DuckDuckGo are still well structured with CSS disabled, but notice the messaging that is appended to each result title.

The navigation bar that is normally displayed as tabs to filter by different types of results (e.g. Images) seems to do nothing at this point because it’s hard to tell that they are filters without styling. But if I click on the Images filter, the image results are actually loaded lower down onto the page, piled right on top of the Web results, and the page becomes mega long as a result. Oh, and you might think that scrolling all the way back up (and it’s a long way up) then clicking another filter, say Videos, would replace the images, but that simply inserts video thumbnail images below the images making an already mega long page a super mega long page. Imagine the page weight of all those assets!

Well, you don’t have to. According to DevTools, images alone account for 831 requests and a total weight of 23.7 MB. Hefty!

Orange outline box encircling feedback on requests and total image weight in DevTools
The real kicker is that it’s not immediately clear that all those images have loaded visually.

The last couple of items are worth noting. Clicking the “Send feedback” link apparently does nothing. Maybe that triggered a modal with CSS? And, although the “All Regions” link does not resemble a link and I could’ve easily ignored it, I was curious enough to click it and was taken to an anchor point of a list of countries. The last two links just made their corresponding contents appear under the list country options.

Blue arrow showing destination to list of regions after clicking All Regions
The “All Regions” option is secretly acting as an anchor.

There’s a lot going on here and there are clearly opportunities for improvement. For example, there are calls to action that display as normal text that should be either be links or buttons instead. Also, we’d think the performance of a site would get better with CSS disabled, but all those loaded assets in the search results are prohibitive. That said, the search experience isn’t painful at all… that is, unless you’re digging into images or videos while doing it.

👍 What the Site Does Well 💡 What the Site Can Improve
Search is consistent and works with or without CSS. A “skip” link for would help with keyboard browsing.
The content hierarchy makes content easy to read and search results a clean experience. Non-link items in the “Settings” menu should be headings for separate unordered lists so there is a clear hierarchy for how the options are grouped.
Good use of a homepage link at the top of the search results page. Some content is either duplicated or repeated because the site relies on conditionally showing and hiding content.
Make sure that all calls to action render as links instead of plain text.
Use a fallback solution to filter the types of search results to prevent items stacking and help control hefty page weight.
Website 3: GitHub
The GitHub homepage with and without CSS
The GitHub homepage with CSS (left) and without CSS (right).

Hey, here’s a site many of us are well familiar with! Well, many of us are used to being logged into it all the time, but I’m going to surf it while logged out.

Already, there’s a skip link (yay). There’s also a mobile navigation icon that I expect will do nothing, and am proven right when I try.

Wide gap after Why GitHub? dropdown
That big gap of white? It’s an SVG icon with a white fill, according to DevTools.

Between some of the navigation items, there are unnecessarily giant gaps. If you click on these, they still function as dropdown menus. They are <details> and <summary> elements… but something feels semantically wrong. It is nice that the menu items are actually unordered list items and that native browser functionality can still take place by using a semantic way to expand content. But that SVG icon messes with me.

Before typing anything into the field, I see three instances of “Search All GitHub” and “Jump to” links. I have no idea which to click, but if I do a search, the keyword shows up in the third group.

Orange outline boxes around groups of search links
There is no clear connection between the search input and the three groups of links.

Everything else on the homepage seems fine except for a number of overly large images horizontally overflowing the window.

Scrolling down to see large images overflowing the browser window
Scrolling down to see large images overflowing the browser window.

Let’s go back to the search bar and navigate to any repo we can find. Right under the Search button, we have two nearly identical secondary navigation bars that return the repository counts, code, commits, and other meta. Without looking at the source, I have no clue what the purpose is for having two of these.

Search results for a “javascript tips” query.
Search results for a “javascript tips” query.

Repository pages still have an easy-to-follow structure and a logical hierarchy for the most part. While logged out and having my cache cleared before coming, the “Dismiss” button for the “Join GitHub today” block still performs as I’d expect. Like we saw earlier on Amazon, the tag links are difficult to tell apart because they run together as a single line.

A repository page in a logged out state.

The next two buttons — “JavaScript” and “New Pull Request” — don’t seem to do anything when I click them. I’d imagine the pull request button should be disabled while viewing as a guest since, unless it’s intended to take a user to a log in screen first… but even that doesn’t feel right. Turns out that the button is indeed disabled when CSS is active, though. Then the rest of the page is fairly easy to understand.

If you’re here mainly for managing, contributing to, or checking out repositories, you won’t face a whole lot of friction since the hierarchy plays out well. You’ll experience pretty much the same elsewhere, whether you’re looking at pull requests, issues, or individual files. Most of the hurdles live in less prominent pages on the site.

👍 What the Site Does Well 💡 What the Site Can Improve
The hierarchy and structure of many pages are really easy to follow and make logical sense. Use the height and width attributes on <img> elements and SVGs to prevent them from blowing up.
Most of the SVG icons embedded on the page are appropriately sized. Watch for empty list items.
Nice use of a skip link in the header. Ensure that button labels use full words.
Make sure links have whitespace or line breaks between them to prevent run-ons.
Website 4: Hex Naw
The Hex Naw tool with and without CSS
The Hex Naw homepage with CSS (left) and without CSS (right).

This next site is an online tool I use often to check color contrasts for accessibility. And for a site that is so big on color, there’ s probably a lot happening here with CSS, so it should get interesting.

There’s immediately a large amount of space above the navigation and no skip links. The hamburger and close buttons for the mobile layout and “X” buttons next to each color to test are oversized.

Scrolling to find missing skip links and excessive space above the navigtion
We’re missing skip links and there is excessive space above the navigation.

Oh, and check out this giant gap between the “Test Colors” button and the next section of content.

Scrolling to show large gap between Test Colors button and “yeah” and “naw” counters
It would be nice to close this gap so the “yeah” and “naw” counters are visible in the test.

One of the many nice features of this site is a checkbox that allows you to see only the colors that passed the test, rather than viewing all of the tested colors. Unfortunately, that button does nothing with CSS disabled. However, I can still see which colors work and get the definitions for contrast ratio, large text, and small text directly in the result table.

Test result section showing “Show passing colors” checked, legend for C, L, and S letters, table of results, and feedback for all failed colors

Hiding and showing the terms is probably what the button does with CSS. The bummer is that I won’t know the purpose of those single letters (e.g. S and R) after the table headers. It’s also both ironic and confusing to see that message for all failing colors after the table because, well, there are passing colors in this list. What could be done is have hide it by default but conditionally inject it later if all the colors in a single test fail.

Pulling out DevTools, it turns out some of the white space at the top is the Hex Naw logo as a SVG file. The space above that is associated with other SVG symbols used for the page. By using a default color of black for the logo, this would help reduce some of the space. I made that quick change in DevTools and it makes a noticeable difference.

Hex Naw logo colored black and highlighted in DevTools
The size of the mobile menu and “X” icons can easily be reduced and be proportional to their viewBox attributes.
The menu and X icons given a width and height of 44 pixels using the attributes
Here’s one way to reduce the size of the mobile menu and “X” icons.

The second gap of space is caused by an SVG loader that appears while calculating color contrasts. This could be helped by specifying a much smaller, yet proportional, width and height exactly like the mobile menu and “X” icons.

SVG loader icon resized to 25px by 25px
I was able to reveal and resize the SVG loader icon in DevTools.

Adding an initial width and height to each SVG would definitely reduce the need to scroll. This is also what we can do to fix the gaps we saw in GitHub’s navigation as well.

Ultimately, Hex Naw remains pretty useful without CSS. I can still test colors, get passing and failing color results, and navigate around the page. It’s just too bad I wasn’t able to work with actual colors and had to work around those extra large SVG icons.

👍 What the Site Does Well 💡 What the Site Can Improve
The site maintains good content hierarchy throughout the site. SVGs should be use a fallback fill color and use the height and width attributes.
All of the elements are written semantically. Feedback for all failing colors could be dynamically added and removed to prevent awkward messaging.
The tests themselves function properly with the exception of being able to show or hide information. Consider an alternative way to display color for the values being tested, like table cells with the background color attribute.
Website 5: Stack Overflow
The Stack Overflow homepage with and without CSS
The Stack Overflow homepage with CSS (left) and without CSS (right).

Like GitHub, Stack Overflow is one of those resources that many (if not most) of us keep in our back pocket because it helps find whether someone has already asked a development question and the answers to them.

On the page to ask a question, I see a bunch of blank bullet points above the main <textarea> element. I have no idea why those empty list items are there. I don’t see any of the formatting buttons either, but after messing around a bit, I found that they happen to be nothing more than blank list items. Perhaps fallback text or an SVG icon for each item would help identify what these are and do. They should be turned into real buttons as well.

It’s also still possible to get a list of similar questions while entering text into the title field. Every works here as expected, which is nice. Although, it is strange that the vote counts for each suggested question appears twice, once above the title as a link and again next to the title without being linked.

The “Ask a Question” page showing blank bullets and questions that may already have an answer. Strong tag tooltip is displayed for one of the bullets.
The “Ask a Question” page has a little awkward formatting, but the overall functionality is in tact and the page is relatively easy to navigate.

One of the key elements we all look for when landing on a Stack Overflow question page is that big green checkmark that indicates the correct answer out of all the submitted answers. But with CSS turned off, it’s hard to tell which answer was accepted because each answer in the list has a black checkmark. Even if the accepted answer is always at the top, there’s still no alternative or fallback indication without having to interact with the page. Additionally, there’s no indication if you have already up voted or down voted the question or any of the answers.

Answered question with black checkmarks next to an accepted answer and other answers
The question (left) next to the list of provided answers (right). We lose a lot of hierarchy when styles are taken away.

To sum up my experience on Stack Overflow, I was able to accomplish what I normally come to the site for: finding answers to a programming problem. That said, there were indeed a few opportunities for improvement and this site is a prime example of how design often relies on color to indicate hierarchy or value on a page, which was sorely missing from the question pages in this experiment.

👍 What the Site Does Well 💡 What the Site Can Improve
Almost every element is written semantically. Use clear controls to identify editing tools while asking or answering questions.
SVG icons use the width and height attributes. Consider a visual icon to distinguish the accepted answer from any other answers to a question.
Lists of answers are clear and easy to scan. Consider a different method to indicate an up vote or a down vote besides color alone.
Website 6: Wikipedia
The Wikipedia homepage with and without CSS
The Wikipedia homepage with CSS (left) and without CSS (right).

Wikipedia, the web’s primary point of reference! It’s an online staple and one of its appealing qualities is a sort of lack of design. This should make for an interesting test.

A few links down, we have a skip navigation option for the real navigation and search. The homepage header containing the globe image maintains its two column layout, and you may have guessed why: this is a table layout. While it may not be a usability issue, we know it isn’t semantic to rely on tables to create a layout. That was a relic of the way past when we didn’t have floats, flexbox, grid or any other way to handle content placement. That said, there are no noticeable usability issues or confusing elements on the page.

Let’s move on to what many of us spend the most time on in Wikipedia: an article entry. This is often the entry point to Wikipedia, especially for those of us that start by typing something into a search engine, then click on the Wikipedia search result.

Top of Wikipedia article
Notice how similar the style-less page is to the styled page, even though it becomes a single column.

The bottom line is that this page is still extremely usable and hierarchical with CSS disabled. The layout goes down to a single column, but the content still flows in a logical order and even maintains bits of styling, thanks again to a reliance on tables and inline table properties.

One issue I bumped up against is the navigation. There is a “Jump to navigation” link in the header which indeed drops me down to the navigation when I click it. In case you’re wondering, the navigation is contained in the footer, which is the reason for needing to jump to it.

Navigation menu with stranded checkboxes above “Variants” and “More”
Navigation menu with stranded checkboxes above “Variants” and “More.”

There are seemingly random checkboxes above a couple of the navigation headings (specifically for “Variants” and “More”) and they appear to serve no purpose, although the checkbox above “More” becomes displays at a certain viewport width when CSS is enabled.

There actually is one odd thing in the navigation, and it’s a label-less button between the “In other projects” and “Languages” headings.

Part of navigation showing blank button with “Language settings” tooltip and some of the languages
Hovering over the button provides a hint that it’s for language settings, but the button should at least have a title to make that clear up front.

Clicking that button, I’m still able to access the language settings, and it mostly works as expected. For example, the layout maintains a tabbed layout which is super functional.

The CSS-less possibility of switching back and forth between Display and Input tabs

In the Display tab, however, the “Language” and “Fonts” buttons do nothing. They probably are tabs as well, but at least I can see what they offer. Beside those buttons are two empty select menus that do absolutely nothing (the first one does become populated with ComicNeue, OpenDyslexic, and System font options when you check the checkbox). Looking at the “Input” tab, the writing language buttons still happen to function as tabs. I’m still able to select options other than English, Spanish, and Chinese.

Blue arrow pointing out the jump to a list of languages at the top of the page when pressing the [...] button
Pressing the […] button takes me to a list of languages at the top of the page.

The articles aren’t difficult to read at all without CSS and that’s because nearly every element is semantically correct and follows a consistent document hierarchy. One thing I did wonder was where the “Show/Hide” button that’s normally in the table of contents went. It turns out to be a lone checkbox, and the label is fake — it uses the content property on a pseudo-element in CSS to display the label.

Another issue in articles is that you have to spend time hunting images down when previewing them. Normally, clicking an image in the article sidebar will trigger a full-screen modal that contains a carousel of images. Without CSS, that carousel is gone and, in its place, is the image with a row of unlabeled buttons above it. That’s a bummer, but would be perfectly OK if the carousel wasn’t all the way down the page, opposite of where the clicked image is at the top of the page without an ability to jump down to it.

Orange arrow pointing to blank buttons above carousel, which are the controls
The image carousel is no longer contained in a modal, but at the end of the page.

I’d be careless if I didn’t mention that the Wikipedia logo was nowhere to be found on the article! It’s not even a white SVG on white. The link contains actually nothing:

<a class="mw-wiki-logo" href="/wiki/Main_Page" title="Visit the main page"></a>

Thankfully, the “Main page” link under “Navigation” is the another way back home without pressing the browser Back button. But, still feels odd to have no branding on the page when it does such a great job of it on the homepage.

Wikipedia’s HTML issues exist mostly in features I expect to be less often used rather than articles. They never hampered my reading experience in the long run.

👍 What the Site Does Well 💡 What the Site Can Improve
The site maintains a clean structure and hierarchy. The logo placement could be moved (or added, in some cases) to the top of the page without a CSS background image.
Skip links are used effectively for search and navigation. Buttons should include labels.
The article content is semantic and easy to read. The image carousel on pages could load where the trigger occurs and use proper button labels for the controls.

Ways to make CSS-less a better experience

CSS is a key component to the modern web. As we’ve seen up to this point, there are a number of sites that become next to un-unusable without it — and we’re counting some of the most recognizable and used sites in that mix. What we’ve seen is that, at best, the primary purpose for a site can still be accomplished, but there are hurdles along the way. Things like:

  • missing or semantically incorrect skip links
  • links that run together
  • oversized images that require additional scrolling
  • empty elements, like list items and button labels

Let’s see if we can compile these into a sort of list of best practices to consider for situations where CSS might be disabled or even unavailable.

Include a skip navigation link at the top of the document

Having a hidden link to skip the navigation is a must. Notice how most of the sites we looked at contained navigation links directly in the header. With CSS turned off, those navigations became long lists of links that would be so hard to tab or scroll through for any user. Having a link to skip that would make that experience much better.

The most basic HTML example I’ve seen is an anchor link that targets an ID where the main content begins.

<a href="#main">Skip to main content</a>
<!-- etc. -->
<main id="main"></main>

And, of course, we can throw a class name on that link to hide it visually so it is not displayed in the UI but still available for both keyboard users and when CSS happens to be off.

.skip-navigation {
  border: 0;
  clip: rect(1px, 1px, 1px, 1px);
  overflow: hidden;
  padding: 0;
  position: absolute;
  height: 1px;
  width: 1px;
}

/* Bonus points for adding :focus styles */
Leave whitespaces where they make sense

Another pain point we saw in a few cases were text links running together. Whether it was in the navigation, tags, or other linked up meta, we often saw links that were “glued together” in such a way that several individual links appeared to be one giant link. That’s either the result of hand-coding the links like that or an automated build task that compresses HTML and removes whitespaces in the process. Either way, the HTML winds up like this:

<a href="#">CSS</a><a href="#">JavaScript</a><a href="#">Python</a><a href="#">Swift</a>

We can keep the freedom to use spaces or line breaks though, even with CSS disabled. One idea is to lean on flexbox for positioning list elements when CSS is enabled. When CSS is disabled, the list items should stack vertically and display as list items by default.

If the items are tags and should still be separated, then traditional spacing methods like margins and padding are still great and we can rely on natural line breaks in the HTML to help with the style-less formatting. For example, here are line breaks in the HTML used to separate items, flexbox to remove spaces, then styled up in CSS to re-separated the items:

See the Pen
Handling Links in HTML Separated by Spaces or Line Breaks
by Jon Kantner (@jkantner)
on CodePen.

Use width and height attributes liberally

The biggest nuisance in this experiment may have been images exploding on the screen to the point that they dominate the content, take up an inordinate amount of space, and result in a hefty amount of scrolling for all users.

The fix here is rather straightforward because we have HTML attributes waiting for us to define them. Both images and SVG have methods for explicitly defining their width and height.

<img src="/path/to-image.jpg" width="40" height="40" />

<svg width="40px" height="40px" viewBox="0 0 200 200">
  <polygon points="80,0 120,0 120,80 200,80 200,120 120,120 120,200 80,200 80,120 0,120 0,80 80,80" />
</svg>
Prepare SVGs for a white background

Many of the large gaps on the sites we looked at looked like empty space, but they were really white SVGs that blew up to full size and blended into the white background.

So, yes, using the proper width and height attributes is a good idea to prevent monstrous icons, but we can also do something about that white-on-white situation. Using properties like fill and fill-rule as attributes will work here.

<!-- Icon will be red by default -->
<svg viewBox="-241 243 16 16" width="100px" fill="#ff0000">
  <path d="M-229.2,244c-1.7,0-3.1,1.4-3.8,2.8c-0.7-1.4-2.1-2.8-3.8-2.8c-2.3,0-4.2,1.9-4.2,4.2c0,4.7,4.8,6,8,10.6  c3.1-4.6,8-6.1,8-10.6C-225,245.9-226.9,244-229.2,244L-229.2,244z"/>
</svg>
/* ...and it’s still red when CSS is enabled */
svg {
  fill: #ff0000;
}

See the Pen
Define SVG Width Attribute
by Geoff Graham (@geoffgraham)
on CodePen.

Label those buttons!

Lastly, if buttons are initially empty, they need to have visible fallback content. If they use a background image and a title for what the do, use a span containing the title text then add aria-hidden="true" so it doesn’t sound like the screen reader is reading the button label twice (e.g. VoiceOver says, “Add button Add” instead).

<button class="btn-icon" title="Add">
  <span class="btn-label" aria-hidden="true">Add</span>
</button>

Then the CSS can be something like this:

.btn-icon {
  background: url(path/to/icon.svg) 0 0 / 100% 100%;
  height: 40px;
  width: 40px;
}

.btn-label {
  display: block;
  overflow: hidden;
  height: 0;
}

If there are <li> elements acting as buttons, they can remain, but they should be static, and the contents should be placed in a button.

Now, if the icon is an SVG, we can ensure the title tooltip can still be seen by using aria-labelledby and assigning the id to the title.

<button>
  <svg width="40px" height="40px" viewBox="0 0 200 200" aria-labelledby="btn-title">
    <title id="btn-title">Add</title>
    <polygon points="80,0 120,0 120,80 200,80 200,120 120,120 120,200 80,200 80,120 0,120 0,80 80,80" />
  </svg>
</button>

Conclusion

It can be easy to either forget or be afraid to check how a site appears when CSS isn’t available to make the UI look as good as intended. After a brief tour of the Non-CSS Web™, we saw just how important CSS is to the overall design and experience of sites, both small and large.

And, like any tool we have in our set, leaning too heavily on CSS to handle the functionality and behavior of elements can lead to poor experiences when it’s not around to do its magic. We’ve seen the same be true of sites that lean too heavily on JavaScript. This isn’t to say that we should not use them and rely on them, but to remember that they are not bulletproof on their own and need proper fallbacks to ensure an optimal experience is still available with or without our tooling.

Seen in that light, CSS is really a layer of progressive enhancement. The hierarchy, form controls, and other elements should also remain intact under their user agent styles. The look and feel, while important, is second when it comes to making sure elements are functional at their core.


That Time I Tried Browsing the Web Without CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/that-time-i-tried-browsing-the-web-without-css/feed/ 13 286429
Why CSS Needs its Own Survey https://css-tricks.com/why-css-needs-its-own-survey/ https://css-tricks.com/why-css-needs-its-own-survey/#comments Fri, 01 Mar 2019 16:45:29 +0000 http://css-tricks.com/?p=283702 2016 was only three years ago, but that’s almost a whole other era in web development terms. The JavaScript landscape was in turmoil, with up-and-comer React — as well as a little-known framework called Vue — fighting to dethrone Angular. …


Why CSS Needs its Own Survey originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
2016 was only three years ago, but that’s almost a whole other era in web development terms. The JavaScript landscape was in turmoil, with up-and-comer React — as well as a little-known framework called Vue — fighting to dethrone Angular.

Like many other developers, I felt lost. I needed some clarity, and I figured the best way to get it was simply to ask fellow coders what they used, and more importantly, what they enjoyed using. The result was the first ever edition of the now annual State of JavaScript survey.

The State of JavaScript 2018

Things have stabilized in the JavaScript world since then. Turns out you can’t really go wrong with any one of the big three frameworks, and even less mainstream options, like Ember, have managed to build up passionate communities and show no sign of going anywhere.

But while all our attention was fixated on JavaScript, trouble was brewing in CSS land. For years, my impression of CSS’s evolution was slow, incremental progress. Back then, I was pretty sure border-radius support represented the crowning, final achievement of web browser technology.

But all of a sudden, things started picking up. Flexbox came out, representing the first new and widely adopted layout method in over a decade. And Grid came shortly after that, sweeping away years of hacky grid frameworks into the gutter of bad CSS practices.

Something even crazier happened: now that the JavaScript people had stopped creating a new framework every two weeks, they decided to use all their extra free time trying to make CSS even better! And thus CSS-in-JS was born.

And now it’s 2019, and the Flexbox Cheatsheet tab I’ve kept open for the past two years has now been joined by a Grid Cheatsheet, because no matter how many times I use them, I still need to double-check the syntax. And despite writing a popular introduction to CSS-in-JS, I still lazily default to familiar Sass for new projects, promising myself that I’ll “do things properly” the next time.

All this to say that I feel just as lost and confused about CSS in 2019 as I did about JavaScript in 2016. It’s high time CSS got a survey of its own.

Starting from scratch

Coming up with the idea for a CSS survey was easy, but deciding on the questions themselves was far from straightforward. Like I said, I didn’t feel confident in my own CSS knowledge, and simply asking about Sass vs. Less for the 37th time felt like a missed opportunity…

Thankfully, the CSS Gods decided to smile down upon me: while attending the DotJS conference in France I discovered that, not only did fellow speaker Florian Rivoal live in Kyoto, Japan, just like me; but that he was a member of the CSS Working Group! In other words, one of the people who knows the most about CSS on the planet was living a few train stops away from me!

Florian was a huge help in coming up with the overall structure and content of the survey. And he also helped me realize how little I really knew about CSS.

Kyoto, Japan: a hotbed of CSS activity (Photo by Jisu Han)

You don’t know CSS

I’m not only talking about obscure CSS properties here, or even new up-and-coming ones, but about how CSS itself is developed. For example, did you know that the development of the CSS Grid spec was sponsored by Bloomberg, because they needed a way to port the layout of their famous terminal to the web?

Did you ever stop to wonder what top: 30px is supposed to mean on a circular screen, such as the one on a smartwatch? Or did you know that some people are laying out entire printed books in CSS, effectively replacing software like InDesign?

Talking with Florian really expanded my mind to how broad and interesting CSS truly is, and convinced me doing the survey was worth it.

“What do you mean, ‘Make the <table> circular’?” Photo by Artur Łuczka

About that divide…

The idea of a CSS survey became all the more important as my new-found admiration for CSS seemed to coincide with a general sentiment that HTML and CSS mastery were becoming under-appreciated skills in the face of JavaScript hegemony.

Myself, personally, I’ve always enjoyed being a generalist in the sense that I happily hop from one side of the great divide to another whenever I feel like it. At the same time, I’m also wholly convinced that the world needs specialists like Florian; people who dedicate their lives to championing and improving a single aspect of the web.

Devaluing the work the work of generalists is not only unfair, but it’s also counter-productive — after all, HTML and CSS are the foundation on which all modern JavaScript frameworks are built; and on the other hand, new patterns and approaches pioneered by CSS-in-JS libraries will hopefully find their way back into vanilla CSS sooner or later.

Thankfully, I feel like a minority of developers hold those views, and those who do generally hold them do so out of ignorance for what the “other side” really stands for more than any well-informed opinion.

So that’s where the survey comes in: I’m not saying I can fill up the divide, but maybe I can throw a couple walkways across, or distribute some jetpacks — you know, whatever works. 🚀

If that sounds good, then the first step is — you guessed it — taking the survey!

Take Survey


Why CSS Needs its Own Survey originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/why-css-needs-its-own-survey/feed/ 2 283702
CSS doesn’t suck https://css-tricks.com/css-doesnt-suck/ Tue, 15 Jan 2019 14:50:17 +0000 http://css-tricks.com/?p=281214 I’m not so protective of CSS that I’m above hearing it criticized, but I’m certainly in agreement here. CSS does not suck. I love how the post is framed to hype up current CSS features the way features of other …


CSS doesn’t suck originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I’m not so protective of CSS that I’m above hearing it criticized, but I’m certainly in agreement here. CSS does not suck. I love how the post is framed to hype up current CSS features the way features of other languages and tools are hyped:

Imagine if a tech dude walked on stage at a conference and said the following:

“This declarative language will gracefully continue on failure, allow you to write global and scoped code, and it will work across your entire front-end stack, whether it’s rendered by a framework, a CMS or a static HTML file”

… Now, if I make a slight amendment to that, the reception would probably be the exact opposite.

“CSS will gracefully continue on failure, allow you to write global and scoped code, and it will work across your entire front-end stack, whether it’s rendered by a framework, a CMS or a static HTML file”

To Shared LinkPermalink on CSS-Tricks


CSS doesn’t suck originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
281214
Slice and Dice a Disc with CSS https://css-tricks.com/slice-and-dice-a-disc-with-css/ https://css-tricks.com/slice-and-dice-a-disc-with-css/#comments Sun, 13 Jan 2019 22:05:09 +0000 http://css-tricks.com/?p=279328 I recently came across an interesting sliced disc design. The disc had a diagonal gradient and was split into horizontal slices, offset a bit from left to right. Naturally, I started to think what would the most efficient way of …


Slice and Dice a Disc with CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I recently came across an interesting sliced disc design. The disc had a diagonal gradient and was split into horizontal slices, offset a bit from left to right. Naturally, I started to think what would the most efficient way of doing it with CSS be.

Screenshot. Shows a diagonal gradient disc that has been split into eight horizontal slices, one on top of the other, with tiny gaps in between them and slightly offset to the left or right (with respect to the vertical axis of the disc) based on parity.
Sliced gradient disc.

The first thought was that this should be doable with border-radius, right? Well, no! The thing with border-radius is that it creates an elliptical corner whose ends are tangent to the edges it joins.

My second thought was to use a circle() clipping path. Well, turns out this solution works like a charm, so let’s take a close look at it!

Note that the following demos won’t work in Edge as Edge doesn’t yet support clip-path on HTML elements. It could all be emulated with nested elements with overflow: hidden in order to have cross-browser support, but, for simplicity, we dissect the clip-path method in this article.

Slicing a disc into equal parts

As far as the HTML structure goes, we generate it with a preprocessor to avoid repetition. First off, we decide upon a number of slices n. Then we pass this number to the CSS as a custom property --n. Finally, we generate the slices in a loop, passing the index of each to the CSS as another custom property --i.

- var n = 8;

style :root { --n: #{n} }

- for(var i = 0; i < n; i++)
  .slice(style=`--i: ${i}`)

Moving on to the CSS, we first decide upon a diameter $d for our disc. This is the width of our slices. The height is the diameter divided by the number of items calc(#{$d}/var(--n)).

In order to be able to tell them apart, we give our slices dummy backgrounds determined by parity.

$d: 20em;

.slice {
  --parity: 0;
  width: $d;
  height: calc(#{$d}/var(--n));
  background: hsl(36, calc(var(--parity)*100%), calc(80% - var(--parity)*30%));
  
  &:nth-of-type(2n) { --parity: 1 }
}

We also position our slices in the middle with a column flex layout on their container (the body in our case).

See the Pen by thebabydino (@thebabydino) on CodePen.

To get the disc shape we use a circle() clipping path having the radius $r equal to half the diameter .5*$d and the central point dead in the middle of the assembly. Since we set this clip-path on the slices, the position of the central point for each slice is relative to the slice itself.

Horizontally, it’s always in the middle, at 50% of the slice. Vertically, it needs to be in the middle of the assembly, so that’s where the total number of items and the item’s index which we’ve passed as CSS variables from the preprocessor code come into play.

In the middle of the assembly means at half the height of the assembly from the top of the assembly. Half the height of the assembly is half the diameter .5*$d, which is equivalent to the radius $r. But this value is relative to the whole assembly and we need one that’s relative to the current slice. In order to get this, we subtract the vertical position of the current slice relative to the assembly, that is, how far the top of the current slice is relative to the top of the assembly.

The first slice (of index --i: 0) is at the very top of the assembly, so the amount we subtract in this case is 0.

The second slice (of index --i: 1) is at one slice height from the top of the assembly (the space occupied by the first slice), so the amount we subtract in this case is 1 slice heights.

The third slice (of index --i: 2) is at two slice heights from the top of the assembly (the space occupied by the first and second slices), so the amount we subtract in this case is 2 slice heights.

In the general case, the amount we subtract for each slice is the slice’s index (--i) multiplied by one slice height.

--h: calc(#{d}/var(--n)); /* slice height */
clip-path: circle($r at 50% calc(#{$r} - var(--i)*var(--h))

See the Pen by thebabydino (@thebabydino) on CodePen.

After doing this, we can offset the slices based on parity.

--sign: calc(1 - 2*var(--parity));
transform: translate(calc(var(--sign)*2%))

We now have our sliced disc!

See the Pen by thebabydino (@thebabydino) on CodePen.

Spacing out the slices

The first thought that comes to mind here is to use a margin on each slice.

See the Pen by thebabydino (@thebabydino) on CodePen.

This may be a good result in some cases, but what if we don’t want our disc to get elongated?

Well, we have the option of limiting the background to the content-box and adding a vertical padding:

box-sizing: border-box;
padding: .125em 0;
background: hsl(36, calc(var(--parity)*100%), calc(80% - var(--parity)*30%)) 
            content-box;

Of course, in this case, we need to make sure box-sizing is set to border-box so that the vertical padding doesn’t add to the height.

See the Pen by thebabydino (@thebabydino) on CodePen.

The one little problem in this case is that it also cuts off the top of the first slice and the bottom of the last slice. This may not be an issue in some cases and we can always reset the padding-top on the :first-of-type and the padding-bottom on the :last-of-type to 0:

.slice {
  /* other styles */
  padding: .125em 0;
  
  &:first-of-type { padding-top: 0 }
  &:last-of-type { padding-bottom: 0 }
}

However, we also have a one-line solution to this problem of creating gaps in between the slices: add a mask on the container!

This mask is a repeating-linear-gradient() which creates transparent stripes of the thickness of the gap $g, repeats itself after a slice height and is limited to the disc diameter $d horizontally and to the disc diameter $d minus a gap $g vertically (so that we don’t mask out the very top and the very bottom as we also did initially with the padding approach).

mask: repeating-linear-gradient(red 0, red calc(var(--h) - #{$g}), 
                                transparent 0, transparent var(--h)) 
        50% calc(50% - #{.5*$g})/ #{$d} calc(#{$d} - #{$g})

Note that in this case we need to set the slice height variable --h on the container as we’re using it for the mask.

See the Pen by thebabydino (@thebabydino) on CodePen.

Continuous background

In order to have a continuous gradient background, we need to give this background a height equal to that of the disc and set its vertical position relative to each slice such that it always starts from the top of the assembly… wherever that may be located relative to the slice.

The top of the first slice (of index --i: 0) coincides with that of the assembly, so our background starts from 0 vertically.

The top of the second slice (of index --i: 1) is 1 slice height below that of the assembly, so its background starts from 1 slice height above vertically. Since the positive direction of the y axis is down, this means our background-position along the y axis is calc(-1*var(--h)) in this case.

The top of the third slice (of index --i: 2) is 2 slice heights below that of the assembly, so its background starts from 2 slice heights above vertically. This makes our background-position along the y axis is calc(-2*var(--h)).

We notice a pattern here: in general, the background-position along the y axis for a slice is calc(-1*var(--i)*var(--h)).

background: 
  linear-gradient(#eccc05, #c26e4c, #a63959, #4e2255, #333f3d)
 
    /* background-position */
    50% calc(-1*var(--i)*var(--h))/ 

    100% $d /* background-size */

See the Pen by thebabydino (@thebabydino) on CodePen.

But if we want a left to right gradient, then our background isn’t continuous anymore, something that becomes really obvious if we tweak the stop positions a bit in order to have abrupt changes:

background: linear-gradient(90deg, 
       #eccc05 33%, #c26e4c 0, #a63959 67%, #4e2255 0, #333f3d) 
 
    /* background-position */
    50% calc(-1*var(--i)*var(--h))/ 

    100% $d /* background-size */

See the Pen by thebabydino (@thebabydino) on CodePen.

In order to fix this issue, we set the offset as a Sass variable $o, set the horizontal background-size to the slice width (100% or $d) plus twice the offset and make sure we attach the background for the slices that move to the left (in the negative direction of the x axis, so by -$o) on the left side of the slice (background-position along the x axis is 0%) and for the slices that move to the right (in the positive direction of the x axis, so by $o) on the right side of the slice (background-position along the x axis is 100%).

$o: 2%;
transform: translate(calc(var(--sign)*#{$o}));
background: linear-gradient(90deg, 
       #eccc05 33%, #c26e4c 0, #a63959 67%, #4e2255 0, #333f3d) 
 
    /* background-position */
    calc((1 - var(--parity))*100%) calc(-1*var(--i)*var(--h))/ 

    calc(100% + #{2*$o}) $d /* background-size */

See the Pen by thebabydino (@thebabydino) on CodePen.

This works for gradients at any angle, as it can be seen in the interactive demo below – drag to change the gradient angle:

See the Pen by thebabydino (@thebabydino) on CodePen.

It also works for images, though in this case we need to remove the second background-size value so the image doesn’t get distorted, which leaves us with the caveat of getting vertical repetition if the image’s aspect ratio is greater than calc(#{$d} + #{2*$o}) : #{$d}. This isn’t the case for the square image we’re using below, but it’s still something to keep in mind.

See the Pen by thebabydino (@thebabydino) on CodePen.

Another thing to note is that above, the top of the image is attached to the top of of the assembly. If we want the middle of the image to be attached to the middle of the assembly, we need to tweak the vertical component of the background-position a bit.

First off, to attach the middle of the image to the middle of a slice, we use a background-position value of 50%. But we don’t want the middle of the image in the middle of each slice, we want it in the middle of the assembly for all slices. We already know the distance from the top of each slice to the vertical midpoint of the whole assembly – it’s the y coordinate of the clipping circle’s central point:

--y: calc(#{$r} - var(--i)*var(--h));
clip-path: circle($r at 50% var(--y))

The distance from the vertical midpoint of each slice to that of the assembly is this value --y minus half a slice’s height. So it results that the background-position we need along the y axis in order to have the vertical midpoint of the image attached to that of the assembly is calc(50% + var(--y) - .5*var(--h)).

See the Pen by thebabydino (@thebabydino) on CodePen.

Incremental slices

This means our slices don’t have the same height anymore. For example, the first one could have a unit height, the second one twice this height, the third one three times this height and so on…

The added heights of all these slices should equal the disc diameter. In other words, we should have the following equality:

h + 2*h + 3*h + ... + n*h = d

This can also be written as:

h*(1 + 2 + 3 + ... + n) = d

which makes it easier to notice something! Within the parenthesis, we have the sum of the first n natural numbers, which is always n*(n + 1)/2!

So our equality becomes:

h*n*(n + 1)/2 = d

This allows us to get the unit height h:

h = 2*d/n/(n + 1)

Applying this to our demo, we have:

--h: calc(#{2*$d}/var(--n)/(var(--n) + 1));
height: calc((var(--i) + 1)*var(--h));

See the Pen by thebabydino (@thebabydino) on CodePen.

Just like in the case of equal slices, the y coordinate of the central point of the clipping circle() is the disc radius $r minus the distance from the top of the assembly to the top of the current slice. This is the sum of the heights of all previous slices.

In the case of the first slice (--i: 0), we have no previous slice, so this sum is 0.

In the case of the second slice (--i: 1), we only have the first slice before and its height is the unit height (--h).

In the case of the third slice (--i: 2), the sum we want is that between the height of the first slice, which equals the unit height and that of the second slice, which is twice the unit height. That’s calc(var(--h) + 2*var(--h)) or calc(var(--h)*(1 + 2)).

In the case of the third slice (--i: 3), the sum is that between the height of the first slice, which equals the unit height, that of the second slice, which is twice the unit height and that of the third slice, which is three times the unit height. That’s calc(var(--h) + 2*var(--h) + 3*var(--h)) or calc(var(--h)*(1 + 2 + 3)).

Now we can see a pattern emerging! For every slice of index --i, we have that the added height of its previous slices is the unit height --h times the sum of the first --i natural numbers (and the sum of the first --i natural numbers is calc(var(--i)*(var(--i) + 1)/2)). This means our clip-path value becomes:

circle($r at 50% calc(var(--h)*var(--i)*(var(--i) + 1)/2))

We add the offset back in and we have the following result:

See the Pen by thebabydino (@thebabydino) on CodePen.

Sadly, having incremental slices means the repeating-linear-gradient() mask method of creating gaps cannot work anymore. What still works however just fine is the vertical padding method and we can set the padding values such that the top one is 0 for the first slice and the bottom one is 0 for the last slice.

padding: 
  calc(var(--i)*#{$g}/var(--n)) /* top */
  0 /* lateral */
  calc((var(--n) - 1 - var(--i))*#{$g}/var(--n)) /* bottom */

See the Pen by thebabydino (@thebabydino) on CodePen.

For a gradient background, the main idea remains the same as in the case of the equal slices. There are just two things we need to take into account.

One, the background-position along the y axis is minus the distance (in absolute value) between the top of the assembly and the top of the current slice. This distance isn’t calc(var(--i)*var(--h)) like in the case of equal slices of height --h anymore. Instead it’s, as computed a bit earlier, calc(var(--i)*(var(--i) + 1)/2*var(--h)). So the background-position along the y axis is calc(-1*var(--i)*(var(--i) + 1)/2*var(--h)).

And two, we want our background clipped to the content-box so that we keep the gaps, but we need to keep the background-origin to its initial value of padding-box so that our gradient stays continuous.

background: 
  linear-gradient(var(--a), 
                #eccc05, #c26e4c, #a63959, #4e2255, #333f3d) 

    /* background-position */
    calc((1 - var(--parity))*100%) /* x component */ 
    calc(-1*var(--i)*(var(--i) + 1)/2*var(--h)) /* y component */ / 

    /* background-size */
    calc(100% + #{2*$o}) $d 

    padding-box /* background-origin */
    content-box /* background-clip */;

See the Pen by thebabydino (@thebabydino) on CodePen.

For an image background whose midpoint is attached to the middle of our assembly, we need to take into account the fact that half a slice height isn’t the same value for all slices anymore. Now the height of a slice is calc((var(--i) + 1)*var(--h)), so this is the value we need to subtract in the formula for the y component of the background-position.

--y: calc(#{$r} - .5*var(--i)*(var(--i) + 1)*var(--h));
background: 
  url(/amur_leopard.jpg) 

    /* background-position */
    calc((1 - var(--parity))*100%) /* x component */
    calc(50% + var(--y) - .5*(var(--i) + 1)*var(--h)) /* y component */ / 

    /* background-size */
    calc(100% + #{2*$o}) 

    padding-box /* background-origin */
    content-box /* background-clip */;
clip-path: circle($r at 50% var(--y));

See the Pen by thebabydino (@thebabydino) on CodePen.

Vertical slices

We can also slice our disc along the other direction. This means removing the flex-direction: column declaration from the container and letting the flex-direction be the initial one (row), switching the width and the height, the x and y coordinates of the circular clipping path’s central point, the direction along which we shift the slices, the dimensions and x and y positions of the masking gradient, which we also need to rotate so that it goes along the x axis.

body {
  /* same as before */
  --w: calc(#{$d}/var(--n));
  mask: repeating-linear-gradient(90deg, 
                                  red 0, red calc(var(--w) - #{$g}), 
                                  transparent 0, transparent var(--w)) 
          calc(50% - #{.5*$g}) 50% / calc(#{$d} - #{$g}) #{$d}
}

.slice {
  /* same as before */
  width: var(--w); height: $d;
  transform: translatey(calc(var(--sign)*2%));
  background: hsl(36, calc(var(--parity)*100%), calc(80% - var(--parity)*30%));
  clip-path: circle($r at calc(#{$r} - var(--i)*var(--w)) 50%)
}

This gives us equal vertical slices with alternating backgrounds:

See the Pen by thebabydino (@thebabydino) on CodePen.

For the gradient case, we need to also reverse the two background dimensions and the background positions along the x and y axes:

background: 
  linear-gradient(135deg, 
      #eccc05 15%, #c26e4c, #a63959, #4e2255, #333f3d 85%) 

    /* background-position */
    calc(-1*var(--i)*var(--w)) calc((1 - var(--parity))*100%)/ 

    #{$d} calc(100% + #{2*$o}) /* background-size */

See the Pen by thebabydino (@thebabydino) on CodePen.

For incremental slices, we combine the incremental case with the vertical case, which means swapping the values we have for the previous incremental case along the two axes:

--w: calc(#{2*$d}/var(--n)/(var(--n) + 1));
width: calc((var(--i) + 1)*var(--w)); height: $d;
clip-path: circle($r at calc(#{$r} - .5*var(--i)*(var(--i) + 1)*var(--w)) 50%);

See the Pen by thebabydino (@thebabydino) on CodePen.

To create the gaps, we use the padding method. But since we’re now in the vertical case, we need horizontal paddings, on the left and on the right and to make sure the padding-left for the first slice is 0 and the padding-right for the last slice is also 0:

box-sizing: border-box;
padding: 
  0 /* top */
  calc((var(--n) - 1 - var(--i))*#{$g}/var(--n)) /* right */
  0 /* bottom */
  calc(var(--i)*#{$g}/var(--n)) /* left */;
background: 
  hsl(36, calc(var(--parity)*100%), calc(80% - var(--parity)*30%)) 
  content-box

See the Pen by thebabydino (@thebabydino) on CodePen.

Finally, we have the gradient case:

background: 
  linear-gradient(135deg, 
      #eccc05 15%, #c26e4c, #a63959, #4e2255, #333f3d 85%)
    
    /* background-position */ 
    calc(-.5*var(--i)*(var(--i) + 1)*var(--w)) /* x component */
    calc((1 - var(--parity))*100%) /* y component */ / 

    /* background-size */
    #{$d} calc(100% + #{2*$o}) 

    padding-box /* background-origin */
    content-box /* background-clip */;

See the Pen by thebabydino (@thebabydino) on CodePen.

2D case

Again, we generate it with a bit of Pug, the total number of items being the product between the number of columns and the number of rows. For simplicity, we keep the number of rows and the number of columns equal.

- var n = 8, m = Math.pow(n, 2);

style :root { --n: #{n}; --i: 0; --j: 0 }
  - for(var i = 1; i < n; i++) {
    | .tile:nth-of-type(#{n}n + #{i + 1}) { --i: #{i} }
    | .tile:nth-of-type(n + #{n*i + 1}) { --j: #{i} }
  - }
- for(var i = 0; i < m; i++)
  .tile

We’ve also passed the column and row indices (--i and --j respectively) to the CSS.

Since we’re in the 2D case, we switch from using a 1D layout (flex) to using a 2D one (grid). We also start with the disc diameter $d and, given the number of columns is equal to that of rows (--n), our disc gets divided into identical tiles of edge length --l: calc(#{$d}/var(--n)).

$d: 20em;

body {
  --l: calc(#{$d}/var(--n));
  display: grid;
  place-content: center;
  grid-template: repeat(var(--n), var(--l))/ repeat(var(--n), var(--l))
}

To create the gaps in between the tiles, we use the padding approach on the .tile elements and combine the horizontal and vertical cases such that we have the padding-top for the first row is 0, the padding-left for the first column is 0, the padding-bottom for the last row is 0 and the padding-right for the last-column is 0.

padding: 
  calc(var(--j)*#{$g}/var(--n)) /* top */
  calc((var(--n) - 1 - var(--i))*#{$g}/var(--n)) /* right */
  calc((var(--n) - 1 - var(--j))*#{$g}/var(--n)) /* bottom */
  calc(var(--i)*#{$g}/var(--n)) /* left */

Note that we’ve used the row index --j for the top to bottom direction (vertical paddings) and the column index --i from the left to right direction (lateral paddings).

To get the disc shape, we again combine the horizontal and vertical cases, using the column index --i to get the x coordinate of the circular clipping path’s central point and the row index --j to get its y coordinate.

clip-path: 
  circle($r at calc(#{$r} - var(--i)*var(--l)) 
               calc(#{$r} - var(--j)*var(--l)))

See the Pen by thebabydino (@thebabydino) on CodePen.

For a gradient background, it’s again combining the horizontal and the vertical cases and taking into account that here we have no offset at this point, which means the background-size is the disc diameter $d along both axes.

background: 
  linear-gradient(135deg, 
      #eccc05 15%, #c26e4c, #a63959, #4e2255, #333f3d 85%) 

    /* background-position */
    calc(-1*var(--i)*var(--l)) 
    calc(-1*var(--j)*var(--l)) / 

    #{$d} #{$d} /* background-size */
    padding-box /* background-origin */
    content-box /* background-clip */

See the Pen by thebabydino (@thebabydino) on CodePen.

For an image background, we remove the second background-size value so we prevent the image from getting stretched if it’s not square. We also adapt the code for attaching the image’s midpoint to that of the grid from the 1D case to the 2D case:

--x: calc(#{$r} - var(--i)*var(--l));
--y: calc(#{$r} - var(--j)*var(--l));
background: url(/amur_leopard.jpg)

    /* background-position */ 
    calc(50% + var(--x) - .5*var(--l)) 
    calc(50% + var(--y) - .5*var(--l)) / 

    #{$d} /* background-size */
    padding-box /* background-origin */ 
    content-box /* background-clip */;
clip-path: circle($r at var(--x) var(--y))

See the Pen by thebabydino (@thebabydino) on CodePen.

In the incremental case, we don’t have the same dimensions for all tiles, so we use auto sizing for grid-template:

body {
  /* same as before */
  grid-template: repeat(var(--n), auto)/ repeat(var(--n), auto)
}

Just like in the 1D case, we start by computing a unit edge length --u:

--u: calc(#{2*$d}/var(--n)/(var(--n) + 1))

We then set incremental dimensions along both axes for our tile elements:

width: calc((var(--i) + 1)*var(--u));
height: calc((var(--j) + 1)*var(--u))

We also need to adapt the coordinates of the clipping circle’s central point to the incremental case:

clip-path: 
  circle($r at calc(#{$r} - .5*var(--i)*(var(--i) + 1)*var(--u)) 
               calc(#{$r} - .5*var(--j)*(var(--j) + 1)*var(--u)))

See the Pen by thebabydino (@thebabydino) on CodePen.

For a gradient background, we adapt the equal tiles version to the incremental case. This means tweaking the background-position as we did before for the incremental slices, only this time we do it along both axes, not just along one:

background: 
  linear-gradient(135deg, 
      #eccc05 15%, #c26e4c, #a63959, #4e2255, #333f3d 85%) 

    /* background-position */
    calc(-.5*var(--i)*(var(--i) + 1)*var(--l)) 
    calc(-.5*var(--j)*(var(--j) + 1)*var(--l)) / 

    #{$d} #{$d} /* background-size */
    padding-box /* background-origin */
    content-box /* background-clip */

See the Pen by thebabydino (@thebabydino) on CodePen.

Finally, we have the image background option for the incremental 2D case:

background: url(/amur_leopard.jpg)  

    /* background-position */
    calc(50% + var(--x) - .5*(var(--i) + 1)*var(--u)) 
    calc(50% + var(--y) - .5*(var(--j) + 1)*var(--u)) / 

    #{$d} /* background-size */
    padding-box /* background-origin */
    content-box /* background-clip */

See the Pen by thebabydino (@thebabydino) on CodePen.

There are probably more variations we could be coming up with, but we’ll stop here. If you have more ideas on how to push this further, I’d love to hear about them!


Slice and Dice a Disc with CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/slice-and-dice-a-disc-with-css/feed/ 6 279328