CSS-Tricks

Subscribe to CSS-Tricks feed
Tips, Tricks, and Techniques on using Cascading Style Sheets.
Updated: 4 hours 21 min ago

Array Explorer and Object Explorer

Wed, 01/03/2018 - 19:38

Sarah made these incredibly handy interactive code reference... thingers.

The idea is perfect. I'm in this position regularly. I know what I have (i.e. an array or an object). I know what I want to do with it (i.e. get an array of all the keys in my object). But I forget the actual method name. These thingers guide you to the right method by letting you tell it what you got and what you wanna do.

Array Explorer

See the Pen Array Explorer by Sarah Drasner (@sdras) on CodePen.

Object Explorer

See the Pen Object Explorer by Sarah Drasner (@sdras) on CodePen.

Array Explorer and Object Explorer is a post from CSS-Tricks

Improving the Accessibility of 24 ways

Wed, 01/03/2018 - 14:41

I’ve been thinking recently about the nature of my work and which aspects of it I enjoy the most. In a role that will often straddle the realms of design and development, whether editing copy, evaluating the design of an interface or refactoring code, I've come to realize that my interests lie in the act of review and refinement.

My work on 24 ways is a case in point. Since Drew McLellan asked me to redesign the site in 2013, I’ve stayed on as part of the team, helping to review, edit and format articles. But I’ve also been able to fulfil the desire I expressed upon launching the redesign:

I'm a big believer in iteration and not treating a website as ever being finished. I hope what’s been released this year can act as a foundation, and that the design will evolve in the years to come.

In the intervening years, as tools have improved and best practices have matured, I've tweaked the design and refactored the code, and developed a component library in the process.

The 24 ways home page A Focus on Accessibility

This year I've been listening to people like Laura Kalbag talk about accessibility in terms of universal design, and followed blogs like Heydon Pickering’s Inclusive Components, which covers how to design and implement common interaction patterns with an inclusive mindset. All of a sudden, the thorny subject of accessibility has felt more approachable and less dogmatic.

With all this knowledge digested, I was keen to see how 24 ways would fare when put under the microscope. In this article, I will cover five areas where I was able to make improvements:

  • Page structure
  • Labelling of elements
  • Keyboard navigation
  • Aural experience
  • General usability

Before I start, a note of thanks. After making an initial set of changes, I asked my friend and accessibility expert Francis Storr to review my work. He uncovered a number of additional issues, partly the result of his experience in this area, but also from having tested the site with a range of different assistive tools.

Rethinking Page Structure

The original design had adopted a mobile-first approach. The navigation was deprioritized and placed towards the bottom of the page. To ensure that it could be accessed from the top of the page in non-JavaScript scenarios, I added a skip to navigation link. If JavaScript was available, this link was co-opted to reveal a navigation drawer, which would slide in from the top or right, depending on the width of the viewport. This resulted in the following page structure:

<header class="c-banner">…</header> <a class="c-menu" href="#menu">Jump to menu</a> <main class="c-main">…</main> <nav class="c-navigation" id="menu"> <div class="c-traverse-nav">…</div> <div class="c-navigation__drawer"/>…</div> </nav> <footer class="c-contentinfo">…</footer>

In retrospect, this was problematic in a number of ways:

  • The menu button (.c-menu) was not adjacent to the element it controlled (c-navigation-drawer). Moving focus around the page like this can be confusing, especially if it’s not managed properly.
  • All navigation on the site was grouped together, even though each set of links served a different purpose.
  • The drawer behaviour relied on having a link behave like a button. However, the first rule of ARIA states:

    If you can use a native HTML element or attribute with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so

    Last year, I updated the JavaScript so that this link would be replaced by a button, yet this complexity was a hint that my original solution was sub-optimal.

Here's the structure I arrived at today:

<header class="c-banner"> … <a class="c-banner__skip" href="#main">Skip to content</a> </header> <div class="c-menu"> <button class="c-menu__button">…</button> <div class="c-menu__drawer">…</div> </div> <main class="c-main" id="main">…</main> <nav class="c-traverse-nav">…</nav> <footer class="c-contentinfo">…</footer>

Moving the navigation towards the top of the page meant the button and drawer were now adjacent to each other. The menu button is no longer two-faced; it is and always will be a button.

A downside to this approach is that the navigation can be heard every time you visit a new page. Again, we can use a skip link, but this time one that points to the content block (#main). Rather than hide this focusable element from certain users, it becomes visible on focus.

This structure may be less ideologically pure, but it’s far more pragmatic. This became a recurring theme. Indeed, having given up any hope of the HTML5 outline algorithm ever being supported by browsers, I stopped using h1 for section headings and followed the recommendation that heading ranks should be used to convey document structure instead.

Improving Keyboard Navigation

As the most interactive component on the site, the menu was the unsurprising focus of my review. The design dictates that the navigation drawer should behave like a dialog, an interface pattern that brings with it a number of assumptions. These are described in detail in eBay's MIND pattern, but essentially a dialog draws focus away from other elements on the page and is modal; only elements within it can be operated while it is open.

I had previously cobbled together various bits of JavaScript to handle focusing (cobbling which at various points produced the odd bug such as failing to draw focus to the first element in the dialog), but had neglected to indicate the menu’s role. Having fixed these issues (adding role='dialog' when the menu is open), Francis pointed out that screen readers could still access links outside the dialog when it was open. In macOS VoiceOver for example, pressing CTRL + OPT + CMD + L to navigate links within the menu, would in fact announce every link on the page.

The solution was to mark everything outside the dialog as "inert". Rob Dodson describes this in more detail in his video Accessible Modal Dialogs. Implementing this can be a little bit fiddly, but a proposal to introduce an inert attribute would make this easier to manage. In the meantime, the proposal provides a polyfill so you can use this feature today.

I’ve found that by thinking about an interface in terms of common interaction patterns, and how they should operate in order to be widely understood, has helped me avoid complexity, and write more robust code. In fact, stepping into the world of accessibility has uncovered a wealth of useful resources, with well-written JavaScript examples a plenty. Given my difficult relationship with the web’s programming language, these have proven invaluable.

Properly Labelling Elements

A good amount of accessibility comes down to labelling things that rely on visual appearance alone to convey meaning. Much like the alt attribute allows us to describe images, aria-label (and its relations) extend this ability to other elements.

Navigation component that allows users to move between articles in a series.

Here is the markup I was using in the navigation element that allows users to traverse previous and next articles within a series:

<div class="c-traverse-nav"> <a rel="prev" href="/2016/we-need-to-talk-about-technical-debt/" data-sequence-title="We Need to Talk About Technical Debt"> <svg width="20" height="20" viewBox="0 0 200 200" role="img"> <path d="M50 100l85 85 7-7-78-78 78-78-7-7"/> </svg> <span class="u-hidden">Previous article</span> </a> <a rel="next" href="/2016/what-the-heck-is-inclusive-design/" data-sequence-title="What the Heck Is Inclusive Design?"> <span class="u-hidden">Next article</span> <svg width="20" height="20" viewBox="0 0 200 200" role="img"> <path d="M150 100l-85 85-7-7 78-78-78-78 7-7"/> </svg> </a> </div>

While I had provided text content for these links, this component still had a number of issues:

  • No indication was given as to the role these links play and their relationship to each other.
  • Using role='img' on the SVG icons, but not providing any accessible names, was akin to including images without alt attributes.
  • Useful information, in this case the title of the previous and next article, was hidden within a data- attribute. This attribute was used in the stylesheet to add content that is shown in animated ‘flaps’ that appear on hover:
.c-traverse-nav a[rel=prev]:hover::after { content: 'Previous: \A' attr(data-sequence-title); } .c-traverse-nav a[rel=next]:hover::after { content: 'Next: \A' attr(data-sequence-title); }

Meaningful content in CSS? That should have been a red flag. I revised this component as follows:

<nav class="c-traverse-nav" aria-label="Articles"> <a rel="prev" href="/2016/what-the-heck-is-inclusive-design/" aria-label="Previous: We Need to Talk About Technical Debt"> <svg width="20" height="20" viewBox="0 0 200 200" focusable="false" aria-hidden="true"> <path d="M50 100l85 85 7-7-78-78 78-78-7-7"/> </svg> </a> <a rel="next" href="/2016/what-the-heck-is-inclusive-design/" aria-label="Next: What the Heck Is Inclusive Design?"> <svg width="20" height="20" viewBox="0 0 200 200" focusable="false" aria-hidden="true"> <path d="M150 100l-85 85-7-7 78-78-78-78 7-7"/> </svg> </a> </nav>

The first thing I did was give this set of links a label. I originally choose Articles navigation. However, in testing VoiceOver would announce: navigation, Articles navigation. As the nav element already describes the role, we need only provide additional information about the type of navigation this is.

Secondly, on the advice of Francis, I added focusable='false' to all inline SVG markup. This is due to a bug in IE11 where SVGs are keyboard focusable by default.

Regarding the labelling of the SVG icons, I had two choices. I could either move the hidden text content to these icons using aria-label, or remove them from the accessibility tree entirely using aria-hidden. In considering the second option, I realised I could merge the hidden text with that in the data- attribute, and use this combined information within an aria-label attribute. All of a sudden, my CSS became much simpler:

.c-traverse-nav a:hover::after { content: attr(aria-label); }

Accessible markup is useful markup.

Considering the Aural Experience

Navigating the site using a screen reader lead to me making a few other small changes as well. For example, a few links on the site include a right-pointing arrow, a visual flourish created using the following CSS:

.c-continue::after { content: ' \203A'; /* Single Right-Pointing Angle Quotation Mark */ }

However, screen-readers typically announce generated content. When these links were read out, you’d hear nonsense like this:

link, more articles by drew single right-pointing angle quotation mark.

Adding speak: none had no effect (CSS aural properties have little support). However, I could create a similar arrow using CSS borders:

.c-continue::after { display: inline-block; vertical-align: middle; height: 0.375em; width: 0.375em; border: 0.1875em solid; border-color: currentColor currentColor transparent transparent; transform: rotate(45deg); content: ''; } Continue links before and after improvements. While they look similar, the revised design sounds much better.

I also made improvements to the styling of author names in article summaries. Originally, these were distinguished from the rest of the excerpt by styling them as uppercase text. Francis pointed out that some screen readers will spell out uppercase letters (regardless of whether they appear in the HTML or have been altered by CSS) if they don’t spell a known word. For example VoiceOver and NVDA have trouble with Chris Coyier's surname, so his name would be read aloud as Chris C-O-Y-I-E-R. The simple fix was to distinguish names using emboldened text instead.

If I'm honest, I’ve been somewhat arrogant in the past, thinking that by using semantic markup and progressive enhancement, I needn’t worry too much about accessibility. While using the right elements, and considering an interface not only in visual terms is important, this is the absolute bare minimum. An understanding of different assistive technologies, and willingness to test with them, is just as important.

Reviewing General Usability

Thinking about accessibility led to me improve overall usability, too.

For this years set of articles, we no longer link to author's websites from article excerpts. This historical holdover was poorly resolved previously; if you happened to click on the author’s name you would be taken to their website, not the article as you would expect. We also included comment counts that were linked to the comment section on the article page (which itself linked to a separate comments page). Madness!

Now, each article has one link; to the article. A home page that once involved tabbing through 24×3 links, is now less noisy and much easier to navigate for everyone.

Other refinements included ensuring the site is responsive in terms of height, as well as width, ensuring the navigation menu can be dismissed when you click outside of it, and a review of overall performance. These might not be considered accessibility improvements, but I’m not so sure. To suggest this would be to think accessibility is an entirely separate concern. In fact, making changes that benefit one set of users will typically benefit all.

Creating something new will always attract attention and admiration, but there’s an under-celebrated nobility in improving what already exists. While not all changes may be visual, they can have just as much impact. I know that, had we decided to redesign the site this year, many of these improvements would not have been made. Hopefully, this overview will encourage you to look at your own projects and think about similar changes you might make.

Having a growing awareness of the issues, and expanding your knowledge of the tools available is an essential requirement of working on the web. However, don’t keep that knowledge saved up for the future; if you can, go back and fix your older projects too.

Improving the Accessibility of 24 ways is a post from CSS-Tricks

You are what you document

Tue, 01/02/2018 - 21:42

There are so many little gems in this piece by Yevgeniy Brikman all about documentation. He digs into a lot more than simply documenting code though and focuses on how we can document every phase of our work, from design to process and beyond.

Here’s my favorite lines that made me sit back and shout “Wahoo!”:

When a developer uses your code, they are really learning a new language, so choose the words in it wisely.

...programs must be written for people to read, and only incidentally for machines to execute.

I like how Yevgeniy suggests that there are kinda two different mindsets that we have to get into when writing code: one for making the dang thing work in the first place, and another for explaining how and why we did a specific way. There’s context-switching that takes place between those different stages of work.

Anyway, when seen in this light, documentation could be much more than a nice-to-have. Instead, it might just be 50% of the work.

Direct Link to ArticlePermalink

You are what you document is a post from CSS-Tricks

Additive Animation with the Web Animations API

Tue, 01/02/2018 - 15:37

These features have not landed in any stable browsers at the time of writing. However, everything discussed is already in Firefox Nightly by default and key parts are in Chrome Canary (with the Experimental Web Platform Features flag enabled), so I recommend using one of those browsers (when reading this article) to see as many of the features in action as possible.

Regardless your preferred method of animation on the web, there will be times that you need to animate the same property in separate animations. Maybe you have a hover effect that scales an image and a click event that triggers a translate — both affecting the transform. By default, those animations do not know anything about the other, and only one will visually be applied (since they are affecting the same CSS property and the other value will be overridden).

element.animate({ transform: ['translateY(0)', 'translateY(10px)'] }, 1000); /* This will completely override the previous animation */ element.animate({ transform: ['scale(1)', 'scale(1.15)'] }, 1500);

The second animation in this Web Animations API example is the only one that would be visually rendered in this example as both animations play at the same time and it was the last one defined.

Sometimes we even have grander ideas where we want a foundational animation and then based on some user interaction change in state we smoothly modify the animation a bit midway without affecting its existing duration, keyframes, or easing. CSS Animations and the current Web Animations API in stable browsers today cannot do this out of the box.

A New Option

The Web Animations specification introduces the composite property (and the related iterationComposite). The default composite is 'replace' and has the behavior we have had for years now where an actively animating property's value simply replaces any previously set value — either from a rule set or another animation.

The 'add' value is where things change from the previous norms.

element.animate({ transform: ['scale(1)', 'scale(1.5)'] }, { duration: 1000, fill: 'both' }); element.animate({ transform: ['rotate(0deg)', 'rotate(180deg)'] }, { duration: 1500, fill: 'both', composite: 'add' });

Now both animations will be seen as the browser on the fly figures out the appropriate transformation at a given point in the element's timeline accounting for both transformations. In our examples, the easing is 'linear' by default and the animations start at the same time, so we can break out what the effective transform is at any given point. Such as:

  • 0ms: scale(1) rotate(0deg)
  • 500ms: scale(1.25) rotate(60deg) (halfway through first animation, 1/3 through second)
  • 1000ms: scale(1.5) rotate(120deg) (end of first, 2/3 through second)
  • 1500ms: scale(1.5) rotate(180deg) (end of second)

See the Pen Animation Composite by Dan Wilson (@danwilson) on CodePen.

So Let's Get Creative

An individual animation does not just consist of a start state and end state — it can have its own easing, iteration count, duration, and more keyframes in the middle. While an element is mid animation you can throw an additional transformation on it with its own timing options.

See the Pen Add more transform animations by Dan Wilson (@danwilson) on CodePen.

This example lets you apply multiple animations on the same element, all affecting the transform property. To keep from going all out in this example, we limit each animation to a single transformation function at a time (such as only a scale), starting at a default value (such as scale(1) or translateX(0)), and ending at a reasonable random value on that same transformation function, repeated infinitely. The next animation will affect another single function with its own randomized duration and easing.

element.animate(getTransform(), //e.g. { transform: ['rotate(0deg), 'rotate(45deg)'] } { duration: getDuration(), //between 1000 and 6000ms iterations: Infinity, composite: 'add', easing: getEasing() //one of two options });

When each animation starts, the browser will effectively find where it is in its previously applied animations and start a new rotation animation with the specified timing options. Even if there is already a rotation going in the opposite direction, the browser will do the math to figure out how much a rotation needs to happen.
Since each animation has its own timing options, you are unlikely to see the exact same motion repeated in this example once you have added a few. This gives the animation a fresh feel as you watch it.

Since each animation in our example starts at the default value (0 for translations and 1 for scaling) we get a smooth start. If we instead had keyframes such as { transform: ['scale(.5)', 'scale(.8)'] } we would get a jump because the didn't have this scale before and all of a sudden starts its animation at half scale.

How are values added?

Transformation values follow the syntax of in the spec, and if you add a transformation you are appending to a list.

For transform animations A, B, and C the resulting computed transform value will be [current value in A] [current value in B] [current value in C]. For example, assume the following three animations:

element.animate({ transform: ['translateX(0)', 'translateX(10px)'] }, 1000); element.animate({ transform: ['translateY(0)', 'translateY(-20px)'] }, { duration:1000, composite: 'add' }); element.animate({ transform: ['translateX(0)', 'translateX(300px)'] }, { duration:1000, composite: 'add' });

Each animation runs for 1 second with a linear easing, so halfway through the animations the resulting transform would have the value translateX(5px) translateY(-10px) translateX(150px). Easings, durations, delays, and more will all affect the value as you go along.

Transforms are not the only thing we can animate, however. Filters (hue-rotate(), blur(), etc) follow a similar pattern where the items are appended to a filter list.

Some properties use a number as a value, such as opacity. Here the numbers will add up to a single sum.

element.animate({ opacity: [0, .1] }, 1000); element.animate({ opacity: [0, .2] }, { duration:1000, composite: 'add' }); element.animate({ opacity: [0, .4] }, { duration:1000, composite: 'add' });

Since each animation again is 1s in duration with a linear easing, we can calculate the resulting value at any point in that animation.

  • 0ms: opacity: 0 (0 + 0 + 0)
  • 500ms: opacity: .35 (.05 + .1 + .2)
  • 1000ms: opacity: .7 (.1 + .2 + .4)

As such, you won't be seeing much if you have several animations that include the value 1 as a keyframe. That is a max value for its visual state, so adding up to values beyond that will look the same as if it were just a 1.

See the Pen Add more opacity animations by Dan Wilson (@danwilson) on CodePen.

Similar to opacity and other properties that accept number values, properties that accept lengths, percentages, or colors will also sum to a single result value. With colors, you must remember they also have a max value, too (whether a max of 255 in rgb() or 100% for saturation/lightness in hsl()), so your result could max out to a white. With lengths, you can switch between units (such as px to vmin) as though it is inside a calc().

For more details, the specification outlines the different types of animation and how the result is calculated.

Working with Fill Modes

When you are not doing an infinite animation (whether you are using a composite or not) by default the animation will not keep its end state as the animation ends. The fill property allows us to change that behavior. If you want to have a smooth transition when you add a finite animation, you likely will want a fill mode of either forwards or both to make sure the end state remains.

See the Pen Spiral: Composite Add + Fill Forwards by Dan Wilson (@danwilson) on CodePen.

This example has an animation with a spiral path by specifying a rotation and a translation. There are two buttons that add new one second animations with an additional small translation. Since they specify fill: 'forwards' each additional translation effectively remains part of the transform list. The expanding (or shrinking) spiral adapts smoothly with each translation adjustment because it is an additive animation from translateX(0) to a new amount and remains at that new amount.

Accumulating animations

The new composite option has a third value — 'accumulate'. It is conceptually in line with 'add' except certain types of animations will behave differently. Keeping with our transform, let's start with a new example using 'add' and then discuss how 'accumulate' is different.

element.animate({ transform: ['translateX(0)', 'translateX(20px)'] }, { duration: 1000, composite: 'add' }); element.animate({ transform: ['translateX(0)', 'translateX(30px)'] }, { duration: 1000, composite: 'add' }); element.animate({ transform: ['scale(1)', 'scale(.5)'] }, { duration: 1000, composite: 'add' });

At the 1 second mark (the end of the animations), the effective value will be:

transform: translateX(20px) translateX(30px) scale(.5)

Which will visually push an element to the right 50px and then scale it down to half width and half height.

If each animation had been using 'accumulate' instead, then the result would be:

transform: translateX(50px) scale(.5)

Which will visually push an element to the right 50px and then scale it down to half width and half height.

No need for a double take, the visual results are in fact the exact same — so how is 'accumulate' any different?

Technically when accumulating a transform animation we are no longer always appending to a list. If a transformation function already exists (such as the translateX() in our example) we will not append the value when we start our second animation. Instead, the inner values (i.e. the length values) will be added and placed in the existing function.

If our visual results are the same, why does the option to accumulate inner values exist?

In the case of transform, order of the list of functions matters. The transformation translateX(20px) translateX(30px) scale(.5) is different than translateX(20px) scale(.5) translateX(30px) because each function affects the coordinate system of the functions that follow it. When you do a scale(.5) in the middle, the latter functions will also happen at the half scale. Therefore with this example the translateX(30px) will visually render as a 15px translation to the right.

See the Pen Visual Reference: Transform Coordinate Systems by Dan Wilson (@danwilson) on CodePen.

Therefore, with accumulation we can have an order that is different then when we always append the values to the list.

Accumulating for Each Iteration

I mentioned before that there is also a new related iterationComposite property. It provides the ability to do some of the behaviors we have already discussed except on a single animation from one iteration to the next.

Unlike composite, this property only has two valid values: 'replace' (the default behavior you already know and love) and 'accumulate'. With 'accumulate' values follow the already discussed accumulation process for lists (as with transform) or are added together for number based properties like opacity.

As a starting example, the visual result for the following two animations would be identical:

intervals.animate([{ transform: `rotate(0deg) translateX(0vmin)`, opacity: 0 }, { transform: `rotate(50deg) translateX(2vmin)`, opacity: .5 }], { duration: 2000, iterations: 2, fill: 'forwards', iterationComposite: 'accumulate' }); intervals2.animate([{ transform: `rotate(0deg) translateX(0vmin)`, opacity: 0 },{ transform: `rotate(100deg) translateX(4vmin)`, opacity: 1 }], { duration: 4000, iterations: 1, fill: 'forwards', iterationComposite: 'replace' //default value });

The first animation is only bumping up its opacity by .5, rotating 50 degrees, and moving 2vmin for 2000 milliseconds. It has our new iterationComposite value and is set to run for 2 iterations. Therefore, when the animation ends, it will have run for 2 * 2000ms and reached an opacity of 1 (2 * .5), rotated 100 degrees (2 * 50deg) and translated 4vmin (2 * 2vmin).

See the Pen Spiral with WAAPI iterationComposite by Dan Wilson (@danwilson) on CodePen.

Great! We just used a new property that is supported in only Firefox Nightly to recreate what we can already do with the Web Animations API (or CSS)!
The more interesting aspects of iterationComposite come into play when you combine it with other items in the Web Animations spec that are coming soon (and also already in Firefox Nightly).

Setting New Effect Options

The Web Animations API as it stands in stable browsers today is largely on par with CSS Animations with some added niceties like a playbackRate option and the ability to jump/seek to different points. However, the Animation object is gaining the ability to update the effect and timing options on already running animations.

See the Pen WAAPI iterationComposite & composite by Dan Wilson (@danwilson) on CodePen.

Here we have an element with two animations affecting the transform property and relying on composite: 'add' — one that makes the element move across the screen horizontally and one moving it vertically in a staggered manner. The end state is a little higher on the screen than the start state of this second animation, and with iterationComposite: 'accumulate' it keeps getting higher and higher. After eight iterations the animation finishes and reverses itself for another eight iterations back down to the bottom of the screen where the process begins again.

We can change how far up the screen the animation goes by changing the number of iterations on the fly. These animations are playing indefinitely, but you can change the dropdown to a different iteration count in the middle of the animation. If you are, for example, going from seven iterations to nine and you are seeing the sixth iteration currently, your animation keeps running as though nothing has changed. However, you will see that instead of starting a reverse after that next (seventh) iteration, it will continue for two more. You can also swap in new keyframes, and the animation timing will remain unchanged.

animation.effect.timing.iterations = 4; animation.effect.setKeyframes([ { transform: 'scale(1)' }, { transform: 'scale(1.2)' } ]);

Modifying animations midway may not be something you will use every day, but since it is something new at the browser level we will be learning of its possibilities as the functionality becomes more widely available. Changing iteration counts could be handy for a game when a user get a bonus round and gameplay continues longer than originally intended. Different keyframes can make sense when a user goes from some error state to a success state.

Where do we go from here?

The new composite options and the ability to change timing options and keyframes open new doors for reactive and choreographed animations. There is also an ongoing discussion in the CSS Working Group about adding this functionality to CSS, even beyond the context of animations — affecting the cascade in a new way. We have time before any of this will land in a stable major browser, but it is exciting to see new options coming and even more exciting to be able to experiment with them today.

Additive Animation with the Web Animations API is a post from CSS-Tricks

Thank You (2017 Edition)

Mon, 01/01/2018 - 16:12

As 2017 comes to a close, as we do each year, let's take a numbers-based glance back at CSS-Tricks. And more importantly, tip our collective hat to all y'all that come here and read the site.

We really do think of the site as somewhere you come and read. While a lot of people's experience with CSS-Tricks is a place that shows up in search results to find the answer to some web design/dev question (and that's awesome), another way to experience the site is to read it like a magazine. We publish an article (or a couple) nearly every day, from a variety of authors, with the hope that it's interesting and exposes us all to new ideas.

According to Google Analytics, which we've had installed and reported from anonymously since day 1 around here, we had 75 million pageviews this year. Very little of CSS-Tricks makes use of any kind of "single page app" type tech, so that's pretty accurate to people visiting and clicking around. It's down from 77 million last year. I'd like to think that's because of ad blockers, which often block Google Analytics, are up in usage for the visitors of CSS-Tricks, but it's just as likely we're just down a smidge this year. Sessions are also down a smidge at 54 million but Users steady at 21 million.

We were on a publishing role though! We published 595 posts, blowing away last year with only 442, the previous record. We also published 50 pages (i.e. snippets/videos/almanac entries) beating 43 last year. Certainly, we're in favor of quality over quantity, but I think this is a healthy publishing pace when our goal is to be read, in a sense, like a magazine. That has been more clear to me this year. We produce content with complementary goals and one of those goals is that of a magazine. We hope the site is worth reading day after day and week after week. The other is that the content lasts and is referenceable for many years to come. Hopefully thinking of things that way can be a guiding light, balancing news and reference content, while avoiding stuff that is neither.

I always wished there was an easy way to figure out what the most popular articles published that year were, but I still don't have a great way to do that. The top 10 is dominated by our big guides, things like our Guides to Grid, Flexbox, SVG, and centering.

Those top 10 make up about 15% of total traffic, which is a massive slice, but that leaves 85% of traffic as part of the "long tail". That's a satisfying thought when you're in it for the long haul as we are. Not every article a huge top 10 smash hit, but does contribute to the long tail which is a much bigger slice collectively anyway.

For the last bunch of months, we've been using Algolia for search. My plan only has 7 days of analytics retention, which isn't enough data to expose major trends. In looking at a week of data though, you can see some surprising top terms like React, SVG, grid, flexbox, font, border... Another thing that's clear is that on-site search is quite important. Algolia reports ~15,000 queries a day. I don't think that represents "user typed in a thing and submitted it" because the search happens in real-time, so each character typed can result in a query. Still, likely hundreds or low-thousands of searches a day.

I'm going to convert us back to using Google search. I think Algolia is a fantastic product, I just don't have the developer time right now to give it the work it needs.

The location of y'all seems to be spreading out internationally little by little. The United States is down to 22% of traffic from 23% and India back to down to 11% from 12% (meaning more traffic came from elsewhere). Those are the top 2, then it goes UK, Germany, Canada, France, Netherlands, Poland, Russia, Brazil. What would be really interesting is to figure out visitors per-capita. For example, Singapore has a population of 5.6 million and had 111,319 unique users, so you could say about 2% of Singaporeans visited CSS-Tricks last year. Lols probably not, but hey that's what the back-of-the-napkin math shows. Whereas with the 4.6 million unique visitors from the US compared to the 323 million population means only 1.5% has visited.

We gained about 10,000 newsletter subscribers this year for a total of 31,376. That's about a third of the entire list size. I love our newsletter. I think it has a ton of potential and is always worth reading. To be perfectly honest I'd like to see our newsletter subscriber numbers be higher than our Twitter followers, but that'll be a tall hill to climb.

Search is the origin of 86.6% of the traffic we get. Direct visits and referral links result in another 5% each. Social media just 2.5%. Whenever I look at that I'm reminded of the disproportionate amount of energy spent on it. Still, it aligns with our goal of being a publication people regularly read and being a source of news, so it feels worth it.

Speaking of social media, we rose 44,000 follows on Twitter last year, again an astonishing number, but it's down year-over-year for the last several years. 71,900 likes on Facebook, only rising about 3,000, which isn't bad considering we do hardly anything at all on Facebook. Growth is much more explosive on YouTube. We at 40,123 subscribers there from 32,174 last year, despite only posting a paultry 6 videos.

This is a classic loop in my head: maybe we should have a dedicated social media person! But even part-time employees are expensive... is it worth it? How much more potential is there? Could they add so much value they pay for themselves? What else could they do? And then the questions swirl around in my head so quickly the idea fizzles out. I guess I'll just say if that kind of thing interests you, reach out!

For once, mobile traffic is actually up. 6.2% this year from below 5% last year. Industry-wide, that's rock bottom. CSS-Tricks is just weird that way. A lot of developers b searching stuff at work, unsurprisingly. Less than 1% is tablets. 30% of the mobile traffic is iPhones.

Y'all left about 5,040 comments on the site this year, which is a smidge down year over year from the past few years, but it actually feels significantly up to me, since we've been much more stringent about what comments we publish this year. I do the vast majority of comment moderation and I trash a lot more than I used to. Anything that off-topic, rude, or unhelpful doesn't see the light of day. I hope that doesn't scare you off from commenting. In fact, I hope it encourages it. Anything on-topic and helpful will absolutely be published and won't be lost in a sea of junk.

We've had 20,238 people use our contact form. Don't worry, I love email.

Goal Review

Double newsletter subscribers. We didn't double but we grew by a third, which is pretty strong. Especially since we did little to nothing to promote it. We probably need to do a better job of promoting it and somehow incentivizing signups, especially since it's such a good way to reach people.

More pairing videos. Pretty hard fail here. The main difficulty is scheduling multiple people together, combined with the pressure of producing something worth watching. It's one thing for an audio podcast like ShopTalk where we can schedule someone and just chit-chat about tech. It's another thing to ask someone to pair with you and essentially do live coding. It's still a good idea, it just needs a more serious time commitment and leader. And honestly, probably a sponsor so that it can be worth everyone's time.

Maintain a mostly-daily publishing schedule. Check and check! This is the first year we've actually kept an honest to god schedule, structured after daily posting. We'll be moving forward with that for sure.

Assemble content in more useful ways. We got a good start on this with Guides. We haven't done a ton with them yet, but we have given ourselves a way to build these without too much effort, and I think the potential in them is fantastic.

New Goals

Publish something in a new format. We have a lot of publishing power around here with lots of writers and a solid platform. Let's use it to publish a book or something book-like.

More editorial vision. Admittedly, what we published each day is a bit random. That's not a terrible thing since the site has a niche anyway, but I'd call it a good goal to exert some editorial guidance to what we publish and when. Meaning commissioning and shepherding work that is a good fit for this site and publishing it with timing that makes some sort of sense.

Interesting sponsorship partners. The most rewarding and powerful partnerships between sponsors and publications are ones of mutual respect and admiration. There are loads of businesses out there I think are doing a terrific job of building what the build, and I'd like to forge relationships with them to promote what they do. And do that promotion in a way that we are uniquely able to do. Get in touch if you think you're a part of that company.

Create another very popular page. It's just a matter of time and topic. I'd like to find at least one web development topic that could really use a strong reference page and spend a good amount of time making what we consider a perfect fit for that, with the goal of it really resonating with developers.

Most importantly

Thank you, again, for being a part of this community.

See the Pen Text Animation with CSS - thank you screen! by Nooray Yemon (@yemon) on CodePen.

Thank You (2017 Edition) is a post from CSS-Tricks

What You Build

Sat, 12/30/2017 - 15:26

I tweeted this yesterday and it seemed to resonate with some folks:

Just a little reminder that it’s about 100 times more important what you build than how you build it.

— Chris Coyier (@chriscoyier) December 10, 2017

What I was feeling when I wrote that was a little tired of endless discussions on tech minutia and yearning for more focus on what we are building and discussion about why.

If you're a reader of this site, you and I live in the same bubble. It's a nice bubble. It's full of smart people who like to chat about web design and development. I live it and love it.

It's easy to get into a heated discussion about frameworks, what type of class names make the most sense, which optimization techniques are most important, or what part of your code base is should be responsible for styling. Those are great discussions that guide our industry.

But what is more important? The naming convention you chose or if your user can actually book a flight? Which state store library you picked or if you actually had the scarf your user was looking for? Which command line tool pulled your dependencies or whether someone was able to find and read the instructions to send in their court appeal?

I was trying to encourage people to build and think about what they are building rather than get too discouraged about the how. You're building things for people and that's such a big responsibility. One that outweighs technical choices, as important as they seem.

I enjoyed the pushback I got on it though.

Most of it centered around the fact that if you make poor tech choices, that limits the quality of what you build and slows your ability to change and adapt to changing user needs. Fair enough.

Good tech just might lead to directly better features and UX for your users. Fair enough. Good tech might be a differentiator between you and your competition. Fair enough.

My favorite was calling out the story of the three little pigs. If you aren't familiar, there is a Big Bad Wolf that is trying to eat the piggies. Each of them built a house to protect themselves. I imagine you can guess which of the pigs did better: the one who built their house out of hay, or the pig who built their house out of bricks.

Fair enough.

Drew McLellan gets into this in All That Glisters, but focuses on the old vs new tech issue:

There are so many new tools, frameworks, techniques, styles and libraries to learn. You know what? You don’t have to use them. You’re not a bad developer if you use Grunt even though others have switched to Gulp or Brunch or Webpack or Banana Sandwich. It’s probably misguided to spend lots of project time messing around with build tool fashions when your so last year build tool is already doing what you need.

And this gem:

Software, much like people, is born with a whole lot of potential and not much utility. Newborns — both digital and meaty — are exciting and cute but they also lead to sleepless nights and pools of vomit.

He goes on to say that what you are building might help dictate your tech choices. Ah yes, the what. Not only is what your things does litearlly the only thing people care about, it also helps guide tech choices.

What You Build is a post from CSS-Tricks

Auto-Sizing Columns in CSS Grid: `auto-fill` vs `auto-fit`

Fri, 12/29/2017 - 14:40

One of the most powerful and convenient CSS Grid features is that, in addition to explicit column sizing, we have the option to repeat-to-fill columns in a Grid, and then auto-place items in them. More specifically, our ability to specify how many columns we want in the grid and then letting the browser handle the responsiveness of those columns for us, showing fewer columns on smaller viewport sizes, and more columns as the screen estate allows for more, without needing to write a single media query to dictate this responsive behavior.

We're able to do that using just one line of CSS — the one-liner that reminds me of when Dumbledore just waved his wand in Horace's apartment and "the furniture flew back to its original places; ornaments reformed in midair, feathers zoomed into their cushions; torn books repaired themselves as they landed upon their shelves...".

This magical, media-query-less responsiveness is achieved using the repeat() function and the auto placement keywords.

Much has been written about this particular one-liner, so I won't be elaborating on how it works. Tim Wright has a great writeup on this that I recommend reading.

To summarize, the repeat() function allows you to repeat columns as many times as needed. For example, if you're creating a 12-columns grid, you could write the following one-liner:

.grid { display: grid; /* define the number of grid columns */ grid-template-columns: repeat(12, 1fr); }

The 1fr is what tells the browser to distribute the space between the columns so that each column equally gets one fraction of that space. That is, they're all fluid, equal-width columns. And the grid will, in this example, always have 12 columns regardless of how wide it is. This, as you have probably guessed, is not good enough, as the content will be too squished on smaller viewports.

So we need to start by specifying a minimum width for the columns, making sure they don't get too narrow. We can do that using the minmax() function.

grid-template-columns: repeat( 12, minmax(250px, 1fr) );

But the way CSS Grid works, this will cause overflow in the row. The columns will not wrap into new rows if the viewport width is too narrow to fit them all with the new minimum width requirement, because we're explicitly telling the browser to repeat the columns 12 times per row.

To achieve wrapping, we can use the auto-fit or auto-fill keywords.

grid-template-columns: repeat( auto-fit, minmax(250px, 1fr) );

These keywords tell the browser to handle the column sizing and element wrapping for us, so that the elements will wrap into rows when the width is not large enough to fit them in without any overflow. The fraction unit we used also ensures that, in case the width allows for a fraction of a column to fit but not a full column, that space will instead be distributed over the column or columns that already fit, making sure we aren't left with any empty space at the end of the row.

At first glace of the names, it might seem like auto-fill and auto-fit are opposites. But in fact, the difference between is quite subtle.

Maybe it seems like you are getting extra space at the end of the column with auto-fit. But when and how?

Let's take a look at what is really happening under the hood.

Fill or Fit? What's the difference?

In a recent CSS workshop, I summarized the difference between auto-fill and auto-fit as follows:

auto-fill FILLS the row with as many columns as it can fit. So it creates implicit columns whenever a new column can fit, because it's trying to FILL the row with as many columns as it can. The newly added columns can and may be empty, but they will still occupy a designated space in the row.

auto-fit FITS the CURRENTLY AVAILABLE columns into the space by expanding them so that they take up any available space. The browser does that after FILLING that extra space with extra columns (as with auto-fill ) and then collapsing the empty ones.

This may sound confusing at first, but it makes a lot more sense when you visualize this behavior. So we'll be doing exactly that, with the Firefox DevTools' Grid Inspector helping us visualize the size and position of our Grid items and columns.

Consider the following demo as an example.

See the Pen auto-fill vs auto-fit by Sara Soueidan (@SaraSoueidan) on CodePen.

The columns are defined using the repeat() function and have a minimum width of 100px, and a maximum set to 1fr , so that they would expand and equally share any extra space when it is available. As for the number of columns per row, we're going to use the auto-placement keywords, so that we let the browser take care of the responsiveness of the grid and will wrap the columns into new rows when needed.

The browser will place and size the columns in the first example using the auto-fill keyword, and it will use auto-fit for the second.

.grid-container--fill { grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); } .grid-container--fit { grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); }

Up to a certain point, both auto-fill and auto-fit show identical results.

But they don't have identical behavior under the hood. It just so happens that they will give the same result up to a certain viewport width.

The point at which these two keywords start exhibiting different behaviors depends on the number and size of columns defined in grid-template-columns, so it will differ from one example to another.

The difference between these two keywords is made apparent when the viewport gets wide enough to fit one (or more) extra column(s) (that) into the row. At that point, the browser is presented with two ways to handle the situation, and how it handles it largely depends on whether or not there is content to be placed into that extra column.

So, when the row can fit a new column, the browser goes like:

  1. "I have some space to fit a new column in there. Do I have any content (i.e. grid items) to go into that extra column? Yes? OK, all good. I'll add the column into the row, and it will wrap into a new row on smaller viewports."
  2. In the case where there is no content to place into a new column: "Do I allow this new column to occupy space in the row (and, therefore, affect the position and size of the rest of the rows)? or do I collapse that column and use its space to expand the other columns?"

auto-fill and auto-fit provide the answer to that last question in particular, and dictate how the browser should handle this scenario. To collapse or not to collapse, that is the question. And that is also the answer.
Whether you want it to collapse or not depends on your content, and how you want that content to behave in the context of a responsive design.

Let's see how this works. To visualize the difference between auto-fill and auto-fit, take a look at the following screen recording. I'm resizing the viewport enough to create horizontal space that's enough to fit one (or more) column(s) into the row. Remember that these two rows are identical, and have the exact same of content and column number. The only difference in this demo is that I'm using auto-fill for the first one and auto-fit for the second.

Notice what is happening there? If it's still not clear, the following recording should make it clearer:

auto-fill behavior: "fill that row up! Add as many columns as you can. I don't care if they're empty — they should all still show up. If you have enough space to add a column, add it. I don't care if it's empty or not, it's still occupying space in the row as if it were filled (as in: filled with content/grid items)."

While auto-fill fills the row with as many columns as it can, even if those columns are empty, auto-fit behaves a little differently.
auto-fit does, too, fill the row with more columns are the viewport width increases, but the only difference is that the newly added columns (and any column gaps associated with them) are collapsed. The Grid inspector is a fantastic way to visualize this. You'll notice that columns are being added when you keep your eye on the Grid line numbers, which will increase as the viewport width increases.

auto-fit behavior: "make whatever columns you have fit into the available space. Expand them as much as you need to fit the row size. Empty columns must not occupy any space. Put that space to better use by expanding the filled (as in: filled with content/grid items) columns to fit the available row space."

A useful tip to remember here is that the columns added in both cases (whether collapsed or not) are not implicit columns — that has specific meaning in the spec. In our case, we are adding/creating columns in the explicit grid in the same way as if we declared you wanted 12 columns, for example. So column number -1 will work to target the end of this grid, which it doesn't if you are creating columns in the implicit grid. Props to Rachel Andrew for this tip.

Summing Up

The difference between auto-fill and auto-fit for sizing columns is only noticeable when the row is wide enough to fit more columns in it.

If you're using auto-fit, the content will stretch to fill the entire row width. Whereas with auto-fill, the browser will allow empty columns to occupy space in the row like their non-empty neighbors — they will be allocated a fraction of the space even if they have no grid items in them, thus affecting the size/width of the latter.

Which behavior you want or prefer is completely up to you. I have yet to think of a use case where auto-fill would make more sense than auto-fit. Do you have any use cases? If you do, please feel free to share them in the comments below.

Auto-Sizing Columns in CSS Grid: `auto-fill` vs `auto-fit` is a post from CSS-Tricks

Five Design Fears to Vanquish With CSS Grid

Thu, 12/28/2017 - 13:58

CSS grid, along with a handful of other new CSS properties, are revolutionizing web design. Unfortunately, the industry hasn't embraced that revolution yet and a lot of it is centered around fear that we can trace back to problems with the current state of CSS grid tutorials.

The majority of them fall into one of two categories:

  1. Re-creating classic web design patterns. Grid is great at replicating classic web design patterns like card grids and "holy grail" pages.
  2. Playing around. Grid is also great for creating fun things like Monopoly boards or video game interfaces.

These types of tutorials are important for new technology. They're a starting point. Now is the time, as Jen Simmons says, to get out of our ruts. To do that, we must cast off our design fears.

Fear 1: Asymmetry

We've been trained in the era of frameworks that symmetric and orderly designs are better. It's true that for many applications a symmetric design or an orderly grid of items is preferred. Yet, asymmetry has the ability to capture the eye and mind in a way that symmetry never will. Asymmetry is interesting in its disorder. If you're nervous, you can always start small.

See the Pen Asymmetric Promo Grid by Bryan Robinson (@brob) on CodePen.

In this example, we have a simple promotional space. By using an asymmetric vertical and horizontal layout, we can make a stronger visual match between our icon and our button. This isn't a large space, but it's not afraid of using whitespace to draw the user's eye where we want it to go.

Speaking of whitespace...

Fear 2: Negative Space

As we left the early 2000s, we decided it was OK if users had to scroll. We began introducing whitespace into our designs and most of this fell between rows of content. This made our designs much cleaner, but is vertical whitespace the only valid option?

In this example, the design incorporates negative space to create a sense of exploration through the page. By not using traditional content rows, the user's eye is given a chance to scan and take things in at a slower pace.

See the Pen Experimental Homepage by Bryan Robinson (@brob) on CodePen.

Fear 3: Punk Rock?

There's no shortage of design talks focused on the print layouts of the 1970s. It was a time of great stability in design tooling, which allowed creativity to bloom. With that came inspired and avant-garde design work that centered around the punk-rock scene.

So my question is this: Can we be punk rockers in web design?

In this example, the design doesn't care about your preconceptions. Text overlap is a bug? Nope, it's a feature. Images shouldn't compete with each other? Survival of the fittest!

See the Pen Grid Overlap and Punk Rock Meditation by Bryan Robinson (@brob) on CodePen.

As this example asks, is this a good idea? It's completely up for debate. What I know is this: our tools have matured and become more stable; now is the time for experimentation. Do we want the web to look the same year after year, or do we want to dream up new and exciting patterns?

Fear 4: New Sources of Inspiration

Sources of inspiration shouldn't cause fear, but they do often cause headaches. Remember, inspiration doesn't mean a 1:1 translation of a concept.

Punk rock graphic design

I mentioned earlier the amazing designs that came out of the '70s and '80s. Here are some links to continue researching punk-rock design:

Vintage movie graphic design

Studying film in college gave me a great appreciation for vintage movie graphic design. One of my professors once told me: "You should be able to tell the tone and subject of a film by its title cards."

This is especially true of post-World War II films. Their title sequences and posters are a treasure trove of design ideas for setting a scene.

Learn how to create graphic design grids

Graphic designers have been using grids for layout for centuries, and there's a lot of great literature on the creation of these grids:

Fear 5: Fallbacks

It's true that Grid has only 74% support in the U.S. (at the time of this writing).

That should not stop you from pushing your designs forward. There are plenty of strategies for starting with support for all browsers and then pushing forward into new patterns.

What's next?

It falls to each of us to push our industry forward. The technology is in place to challenge ourselves to create new and interesting designs. This doesn't have to be as pointed and intense as some of these examples. It starts by realizing we can do amazing things … or we can stagnate.

How will you push the industry forward?

Five Design Fears to Vanquish With CSS Grid is a post from CSS-Tricks

​Wix.com: Make the Web Your Playground

Thu, 12/28/2017 - 13:57

(This is a sponsored post.)

Here's something you can do: kick off 2018 with your own professional website. The only thing you'll need to get started is your imagination, a little free time, and an innovative website builder.

Wix is the world's most technologically advanced website builder. Sign up for Wix, choose a template, and start customizing it. Whether you’re a novice, a business owner, a sophisticated designer, or a professional website builder, you’ll have full control of your website - from design prototyping to production.

Wix takes care of all the heavy lifting. You get reliable, safe, secure hosting that you'll never need to worry about. You get a custom domain name and email. To get started, all you need is a computer and a little time.

Don't underestimate what you can do with Wix. There are all kinds of advanced design features and functionality if that's something you need. The web is your playground. We’ve come along way from the website building platforms of the 90s. Now, you can create any kind of website you want and even collaborate with friends or coworkers.

Save yourself time and money, and head over to Wix.com to get started for free. Kick off 2018 strong with your own professional website and share your ideas with the world.

Direct Link to ArticlePermalink

​Wix.com: Make the Web Your Playground is a post from CSS-Tricks

Front-End Tools: My Favorite Finds of 2017

Thu, 12/28/2017 - 13:45

Another twelve months have passed and I'm sure all of you have come across some interesting new coding techniques, technologies, CSS tricks (of course!), and other stuff that will make you more productive in 2018.

As some of you might know, I curate a weekly newsletter called Web Tools Weekly, in which I feature dozens of links every week to new tools, mostly focusing on stuff that's useful for front-end developers. So it's an understatement to say that I've come across lots of new tools over the past 12 months.

As I've done in years past, I've put together a brief look at some of my favorite finds in front-end tools.

And please note that this is not a list of the "best" or "most popular" tools of 2017 – this has nothing to do with popularity or number of GitHub stars. These are tools I believe are unique, interesting, and practical – but not necessarily all that well-known. They are some of my personal favorite finds of the year, nothing more.

tlapse

When working on a new project, especially a large and lengthy one, it's easy to forget the number of changes the project's layout has gone through. tlapse is a command-line utility that allows you to set up automated screenshots of your work at specified intervals, essentially giving you a visual timeline of your development in the form of a series of screenshots.

The project has 1,100+ stars on GitHub, so it seems developers are finding a valid use for this, even though it seems a little narcissistic at first glance. Besides the novelty of being able to look back at the progress of your project, I suppose tlapse could also be used to send visual progress reports to clients, project managers, or other development team members.

You install tlapse as a global npm package:

npm install -g tlapse

Then run it in the background and start your work:

tlapse -- localhost:3000

By default, tlapse will take screenshots at one minute intervals and the screenshots will be added to the tlapse folder in the current project (i.e. where you execute the command):

Usefully, tlapse will also take a screenshot only if it detects the layout has changed in some way. So if the next scheduled screenshot is the same as the previous, it will skip it:

If you want to use a different directory or change the screenshot frequency, enter these as options along with the regular command:

tlapse --every 3m --directory ./screenshots -- localhost:3000

As the name suggests, tlapse allows you to make a time lapse video or animated GIF that demonstrates the progress of your work. Here's one I created while mock-building a Bootstrap-based layout:

Overall, this is an easy to use tool, even for those not very comfortable with the command line, and there are certainly some use cases for wanting to take screenshots of work in progress.

KUTE.js

JavaScript animation libraries are not rare. But KUTE.js caught my eye due to its main selling point: Performance. It can't be denied that if you're going to even consider complex animations in web apps today, you have to be prepared to deal with potential performance problems as a result of users accessing your app on mobile devices or on slower connections.

The moment you visit the KUTE.js home page, you're greeted with a colorful, complex, super-smooth animation, testifying to the truth of this tool's claims.

In addition to performance, two other things I like:

  • A really nice API
  • An excellent callback system

You start to build your animations by creating tween objects. For example:

var tween = KUTE.fromTo( '.box', {backgroundColor:'yellow'}, {backgroundColor:'orange'}, {complete: callback} );

The above example creates a fromTo() tween with various options. Inside fromTo() I've specified the selector for the target element, the start and end values for the property being animated, and a callback function to execute when the animation is complete.

You can also create tweens using to(), allTo(), and allFromTo(), with the latter methods letting you apply animations to collections of objects.

The callback functionality is very fine-grained, allowing you to run code (which could include calling a new animation altogether) at specified points, including:

  • When an animation starts
  • For each frame of the animation
  • When an animation is paused
  • When an animation is resumed after having been paused
  • When an animation is stopped
  • When an animation is completed

I've only scratched the surface of the features available. The documentation on the site is good, so check that out for the full API. The CodePen below is based on one of the demos from the API docs, which uses the .chain() method to chain multiple transform animations.

See the Pen Chain Transform Animations with KUTE.js by Louis Lazaris (@impressivewebs) on CodePen.

ScrollDir

Scrolling libraries have been popular for some time now. ScrollDir, from the developers at Dollar Shave Club, is a really simple, tiny, and intuitive utility to help you do a couple of simple things with scroll detection.

Once you drop in the library, in its simplest form the script just works. You don't need to call the scrollDir() method or anything like that. If you open your browser's developer tools and examine the live DOM while scrolling up and down on a page running ScrollDir, you can see what it does:

As shown in the above GIF, this utility adds a data-scrolldir attribute to the page's <html> element, which changes to one of two values, depending on scroll direction:

<!-- when the user is scrolling down --> <html data-scrolldir="down"> <!-- when the user is scrolling up --> <html data-scrolldir="up">

It defaults to "down" when the page hasn't yet been scrolled, although it seems like it could benefit from having a "neutral" class as a third optional state.

With this attribute in place, it's super easy to make custom changes to a page's layout with nothing but CSS, taking advantage of CSS's attribute selectors:

[data-scrolldir="down"] .header-banner { top: -100px; } [data-scrolldir="up"] .footer-banner { bottom: -100px; }

You can see the above code, combined with some simple CSS transitions, demonstrated in the CodePen below, which is similar to the example on the ScrollDir home page:

See the Pen ScrollDir basic demo by Louis Lazaris (@impressivewebs) on CodePen.

ScrollDir offers a few minor API options if you choose to use the non-auto version of the script. In either case it's dead simple to use and I'm sure will come in handy if you're building something that needs changes to occur on the page based on scroll direction.

CodeSandbox

Due to the popularity of web app development using libraries like React and Vue, a number of different IDEs and other code tools have come on the scene, aimed at helping developers who are working with a specific library or framework.

CodeSandbox is an online code editor for four of the current big players: React, Vue, Preact, and Svelte. This tool is somewhat in the same category as CodePen Projects, but is specifically designed for each of the four aforementioned libraries.

One of the nice features of CodeSandbox is the ability to add npm packages in the left side bar, under a pane called "Dependencies". There's a button called "Add Package" that allows you to search for packages in the npm registry:

And if your app is missing a dependency, CodeSandbox will indicate this with an error message and an option to add the required package. In the following GIF, I've pieced together this React calculator app as an example project in CodeSandbox:

Notice the project still had a missing dependency, which I was able to install instantly. Here's the CodeSandbox link to my version of that project.

Another feature that caught my eye is the ability to "peek" at the definition of a function in the code window:

Like many native IDEs, this allows you to be able to track down a function's source, for quicker debugging and whatnot. There are also some clean inline code completion features, just like a native IDE.

There are tons more features I haven't discussed here – including GitHub integration, deployment via ZEIT, and lots more – so be sure to poke around the different panels to get a feel for what you can do.

AmplitudeJS

AmplitudeJS is a dependency-free (we like that nowadays don't we?) HTML5 audio player "for the modern web". I think a lot of independent hobby-driven music makers with web development experience will appreciate this one for a number of reasons.

Amplitude allows you to build your own audio player with your own custom design and layout. To add a song list, you can add it via the main Amplitude.init() method in JSON format. Here's an example with three songs:

Amplitude.init({ songs: [ { name: "Song Name One", artist: "Artist Name", album: "Album Name", url: "/path/to/song.mp3", cover_art_url: "/path/to/artwork.jpg" }, { name: "Song Name Two", artist: "Artist Name Two", album: "Album Name Two", url: "/path/to/song.mp3", cover_art_url: "/path/to/artwork.jpg" }, { name: "Song Name Three", artist: "Artist Name Three", album: "Album Name Three", url: "/path/to/song.mp3", cover_art_url: "/path/to/artwork.jpg" } ] });

The code behind this player generates the audio using the Web Audio API, which is kind of like adding HTML5's audio element, but with nothing but JavaScript. So you could technically generate a functioning version of the AmplitudeJS player with zero HTML. See this CodePen as an example, which auto-plays the only song in the playlist and has no HTML. Even if you examine the generated DOM, there's nothing there; it's just JavaScript. In that instance, I'm using the "autoplay": true option in the init() method (the default is false, of course).

If you want to see the flexible and varied audio players that can be built with AmplitudeJS, be sure to check out the examples page. The Flat Black Player is probably my favorite for its similarity to an old-school MP3 player. I've put it into a CodePen demo below:

See the Pen LeEgyj by Louis Lazaris (@impressivewebs) on CodePen.

In terms of configuring AmplitudeJS, here are some of the highlights.

All the info you provide in the JSON can be added dynamically to the player wherever you want. For example the following HTML would display the song name, artist, album, and file URL for the currently playing track:

<p amplitude-song-info="name" amplitude-main-song-info="true"> <p amplitude-song-info="artist" amplitude-main-song-info="true"> <p amplitude-song-info="album" amplitude-main-song-info="true"> <p amplitude-song-info="url" amplitude-main-song-info="true">

Notice the amplitude-song-info attribute, which defines which bit of data you want to inject into that element. You wouldn't necessarily use paragraphs, but that's one way to do it. You can see this in action in this CodePen demo.

With the metadata features, adding a running time or time remaining indicator for the current song is easy:

<p class="amplitude-time-remaining" amplitude-main-time-remaining="true"> <p class="amplitude-current-time" amplitude-main-current-time="true">

Another great feature is the ability to work with callbacks (which is pretty much a must for any good API). Here's two of the callback options used in a simple example:

Amplitude.init({ songs: [ // songs list would be here... ], callbacks: { before_play: function() { document.querySelector('.msg').innerHTML = 'Song will now begin...'; }, after_stop: function() { document.querySelector('.msg').innerHTML = 'Song has ended!'; } } });

You can see this in action in this CodePen. I've incorporated a rudimentary play/pause button to help with the callbacks. To see the final callback, you have to wait for the song to complete (pausing doesn't trigger the after_stop callback). The button is built using nothing but a few HTML attributes, no extra scripting needed.

This is a really small sampling of what's possible with this player and how flexible it is. The docs are solid and should get you up and running with this tool in no time.

Honorable Mentions

That's a detailed look at five of my favorites from the past year. But there are lots of others worth examining that are similarly lesser-known. I've listed some of these below:

  • BunnyJS –An ES6-based front-end framework that advertises as "Simple like jQuery, better then jQuery UI, powerful like React".
  • keyframes-tool –A command line tool to convert CSS animations to a keyframes object suitable for use with the Web Animations API.
  • Konsul – A React renderer that renders to the browser's developer tools console.
  • across-tabs – Easy communication between cross-origin browser tabs.
  • svgi – A CLI tool to inspect the content of SVG files, providing information on the SVG (number of nodes, paths, containers, shapes, tree hierarchy, etc).
  • CSS in JS Playground – Play around with the code for just about any of the CSS-in-JavaScript solutions (JSS, styled-components, glamorous, etc).
What's Your Favorite Find of the Year?

So that's it. As I said at the start, this was not meant to be an awards ceremony for best tools of the year, but more of a look at some not-so-mainstream alternatives that are interesting and practical. I hope you find some of them useful. If you're interested in continuing to keep up with the influx of new tools in front-end development, be sure to subscribe to my newsletter.

Have you stumbled upon (or built) something cool over the past year that would be of interest to front-end developers? Let me know in the comments, I'd love to take a look.

Front-End Tools: My Favorite Finds of 2017 is a post from CSS-Tricks

A Sliding Nightmare: Understanding the Range Input

Wed, 12/27/2017 - 14:31

You may have already seen a bunch of tutorials on how to style the range input. While this is another article on that topic, it's not about how to get any specific visual result. Instead, it dives into browser inconsistencies, detailing what each does to display that slider on the screen. Understanding this is important because it helps us have a clear idea about whether we can make our slider look and behave consistently across browsers and which styles are necessary to do so.

Looking inside a range input

Before anything else, we need to make sure the browser exposes the DOM inside the range input.

In Chrome, we bring up DevTools, go to Settings, Preferences, Elements and make sure the Show user agent shadow DOM option is enabled.

Sequence of Chrome screenshots illustrating the steps from above.

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

Sequence of Firefox screenshots illustrating the steps from above.

For a very long time, I was convinced that Edge offers no way of seeing what's inside such elements. But while messing with it, I discovered that where there's a will and (and some dumb luck) there's a way! We need to bring up DevTools, then go to the range input we want to inspect, right click it, select Inspect Element and bam, the DOM Explorer panel now shows the structure of our slider!

Sequence of Edge screenshots illustrating the steps from above.

Apparently, this is a bug. But it's also immensely useful, so I'm not complaining.

The structure inside

Right from the start, we can see a source for potential problems: we have very different beasts inside for every browser.

In Chrome, at the top of the shadow DOM, we have a div we cannot access anymore. This used to be possible back when /deep/ was supported, but then the ability to pierce through the shadow barrier was deemed to be a bug, so what used to be a useful feature was dropped. Inside this div, we have another one for the track and, within the track div, we have a third div for the thumb. These last two are both clearly labeled with an id attribute, but another thing I find strange is that, while we can access the track with ::-webkit-slider-runnable-track and the thumb with ::-webkit-slider-thumb, only the track div has a pseudo attribute with this value.

Inner structure in Chrome.

In Firefox, we also see three div elements inside, only this time they're not nested - all three of them are siblings. Furthermore, they're just plain div elements, not labeled by any attribute, so we have no way of telling which is which component when looking at them for the first time. Fortunately, selecting them in the inspector highlights the corresponding component on the page and that's how we can tell that the first is the track, the second is the progress and the third is the thumb.

Inner structure in Firefox.

We can access the track (first div) with ::-moz-range-track, the progress (second div) with ::-moz-range-progress and the thumb (last div) with ::-moz-range-thumb.

The structure in Edge is much more complex, which, to a certain extent, allows for a greater degree of control over styling the slider. However, we can only access the elements with -ms- prefixed IDs, which means there are also a lot of elements we cannot access, with baked in styles we'd often need to change, like the overflow: hidden on the elements between the actual input and its track or the transition on the thumb's parent.

Inner structure in Edge.

Having a different structure and being unable to access all the elements inside in order to style everything as we wish means that achieving the same result in all browsers can be very difficult, if not even impossible, even if having to use a different pseudo-element for every browser helps with setting individual styles.

We should always aim to keep the individual styles to a minimum, but sometimes it's just not possible, as setting the same style can produce very different results due to having different structures. For example, setting properties such as opacity or filter or even transform on the track would also affect the thumb in Chrome and Edge (where it's a child/ descendant of the track), but not in Firefox (where it's its sibling).

The most efficient way I've found to set common styles is by using a Sass mixin because the following won't work:

input::-webkit-slider-runnable-track, input::-moz-range-track, input::-ms-track { /* common styles */ }

To make it work, we'd need to write it like this:

input::-webkit-slider-runnable-track { /* common styles */ } input::-moz-range-track { /* common styles */ } input::-ms-track { /* common styles */ }

But that's a lot of repetition and a maintainability nightmare. This is what makes the mixin solution the sanest option: we only have to write the common styles once so, if we decide to modify something in the common styles, then we only need to make that change in one place - in the mixin.

@mixin track() { /* common styles */ } input { &::-webkit-slider-runnable-track { @include track } &::-moz-range-track { @include track } &::-ms-track { @include track } }

Note that I'm using Sass here, but you may use any other preprocessor. Whatever you prefer is good as long as it avoids repetition and makes the code easier to maintain.

Initial styles

Next, we take a look at some of the default styles the slider and its components come with in order to better understand which properties need to be set explicitly to avoid visual inconsistencies between browsers.

Just a warning in advance: things are messy and complicated. It's not just that we have different defaults in different browsers, but also changing a property on one element may change another in an unexpected way (for example, when setting a background also changes the color and adds a border).

WebKit browsers and Edge (because, yes, Edge also applies a lot of WebKit prefixed stuff) also have two levels of defaults for certain properties (for example those related to dimensions, borders, and backgrounds), if we may call them that - before setting -webkit-appearance: none (without which the styles we set won't work in these browsers) and after setting it. The focus is going to be however on the defaults after setting -webkit-appearance: none because, in WebKit browsers, we cannot style the range input without setting this and the whole reason we're going through all of this is to understand how we can make our lives easier when styling sliders.

Note that setting -webkit-appearance: none on the range input and on the thumb (the track already has it set by default for some reason) causes the slider to completely disappear in both Chrome and Edge. Why that happens is something we'll discuss a bit later in this article.

The actual range input element

The first property I've thought about checking, box-sizing, happens to have the same value in all browsers - content-box. We can see this by looking up the box-sizing property in the Computed tab in DevTools.

The box-sizing of the range input, comparative look at all three browsers (from top to bottom: Chrome, Firefox, Edge).

Sadly, that's not an indication of what's to come. This becomes obvious once we have a look at the properties that give us the element's boxes - margin, border, padding, width, height.

By default, the margin is 2px in Chrome and Edge and 0 .7em in Firefox.

Before we move on, let's see how we got the values above. The computed length values we get are always px values.

However, Chrome shows us how browser styles were set (the user agent stylesheet rule sets on a grey background). Sometimes the computed values we get weren't explicitly set, so that's no use, but in this particular case, we can see that the margin was indeed set as a px value.

Tracing browser styles in Chrome, the margin case.

Firefox also lets us trace the source of the browser styles in some cases, as shown in the screenshot below:

Tracing browser styles in Firefox and how this fails for the margin of our range input.

However, that doesn't work in this particular case, so what we can do is look at the computed values in DevTools and then checking whether these computed values change in one of the following situations:

  1. When changing the font-size on the input or on the html, which entails is was set as an em or rem value.
  2. When changing the viewport, which indicates the value was set using % values or viewport units. This can probably be safely skipped in a lot of cases though.
Changing the font-size of the range input in Firefox also changes its margin value.

The same goes for Edge, where we can trace where user styles come from, but not browser styles, so we need to check if the computed px value depends on anything else.

Changing the font-size of the range input in Edge doesn't change its margin value.

In any event, this all means margin is a property we need to set explicitly in the input[type='range'] if we want to achieve a consistent look across browsers.

Since we've mentioned the font-size, let's check that as well. Sure enough, this is also inconsistent.

First off, we have 13.3333px in Chrome and, in spite of the decimals that might suggest it's the result of a computation where we divided a number by a multiple of 3, it seems to have been set as such and doesn't depend on the viewport dimensions or on the parent or root font-size.

The font-size of the range input in Chrome.

Firefox shows us the same computed value, except this seems to come from setting the font shorthand to -moz-field, which I was first very confused about, especially since background-color is set to -moz-Field, which ought to be the same since CSS keywords are case-insensitive. But if they're the same, then how can it be a valid value for both properties? Apparently, this keyword is some sort of alias for making the input look like what any input on the current OS looks like.

The font-size of the range input in Firefox.

Finally, Edge gives us 16px for its computed value and this seems to be either inherited from its parent or set as 1em, as illustrated by the recording below:

The font-size of the range input in Edge.

This is important because we often want to set dimensions of sliders and controls (and their components) in general using em units so that their size relative to that of the text on the page stays the same - they don't look too small when we increase the size of the text or too big when we decrease the size of the text. And if we're going to set dimensions in em units, then having a noticeable font-size difference between browsers here will result in our range input being smaller in some browsers and bigger in others.

For this reason, I always make sure to explicitly set a font-size on the actual slider. Or I might set the font shorthand, even though the other font-related properties don't matter here at this point. Maybe they will in the future, but more on that later, when we discuss tick marks and tick mark labels.

Before we move on to borders, let's first see the color property. In Chrome this is rgb(196,196,196) (set as such), which makes it slightly lighter than silver (rgb(192,192,192)/ #c0c0c0), while in Edge and Firefox, the computed value is rgb(0,0,0) (which is solid black). We have no way of knowing how this value was set in Edge, but in Firefox, it was set via another similar keyword, -moz-fieldtext.

The color of the range input, comparative look at all three browsers (from top to bottom: Chrome, Firefox, Edge).

The border is set to initial in Chrome, which is equivalent to none medium currentcolor (values for border-style, border-width and border-color). How thick a medium border is exactly depends on the browser, though it's at least as thick as a thin one everywhere. In Chrome in particular, the computed value we get here is 0.

The border of the range input in Chrome.

In Firefox, we also have a none medium currentcolor value set for the border, though here medium seems to be equivalent to 0.566667px, a value that doesn't depend on the element or root font-size or on the viewport dimensions.

The border of the range input in Firefox.

We can't see how everything was set in Edge, but the computed values for border-style and border-width are none and 0 respectively. The border-color changes when we change the color property, which means that, just like in the other browsers, it's set to currentcolor.

The border of the range input in Edge.

The padding is 0 in both Chrome and Edge.

The padding of the range input, comparative look at Chrome (top) and Edge (bottom).

However, if we want a pixel-perfect result, then we need to set it explicitly because it's set to 1px in Firefox.

The padding of the range input in Firefox.

Now let's take another detour and check the backgrounds before we try to make sense of the values for the dimensions. Here, we get that the computed value is transparent/ rgba(0, 0, 0, 0) in Edge and Firefox, but rgb(255,255,255) (solid white) in Chrome.

The background-color of the range input, comparative look at all three browsers (from top to bottom: Chrome, Firefox, Edge).

And... finally, let's look at the dimensions. I've saved this for last because here is where things start to get really messy.

Chrome and Edge both give us 129px for the computed value of the width. Unlike with previous properties, we can't see this being set anywhere in Chrome, which would normally lead me to believe it's something that depends either on the parent, stretching horizontally to fit as all block elements do (which is definitely not the case here) or on the children. There's also a -webkit-logical-width property taking the same 129px value in the Computed panel. I was a bit confused by this at first, but it turns out it's the writing-mode relative equivalent - in other words, it's the width for horizontal writing-mode and the height for vertical writing-mode.

Changing the font-size of the range input in Chrome doesn't change its width value.

In any event, it doesn't depend on the font-size of the input itself or of that of the root element nor on the viewport dimensions in either browser.

Changing the font-size of the range input in Edge doesn't change its width value.

Firefox is the odd one out here, returning a computed value of 160px for the default width. This computed value does however depend on the font-size of the range input - it seems to be 12em.

Changing the font-size of the range input in Firefox also changes its width value.

In the case of the height, Chrome and Edge again both agree, giving us a computed value of 21px. Just like for the width, I cannot see this being set anywhere in the user agent stylesheet in Chrome DevTools, which normally happens when the height of an element depends on its content.

Changing the font-size of the range input in Chrome doesn't change its height value.

This value also doesn't depend on the font-size in either browser.

Changing the font-size of the range input in Edge doesn't change its height value.

Firefox is once again different, giving us 17.3333px as the computed value and, again, this depends on the input's font-size - it's 1.3em.

Changing the font-size of the range input in Firefox also changes its height value.

But this isn't worse than the margin case, right? Well, so far, it isn't! But that's just about to change because we're now moving on to the track component.

The range track component

There's one more possibility regarding the actual input dimensions that we haven't yet considered: that they're influenced by those of its components. So let's explicitly set some dimensions on the track and see whether that influences the size of the slider.

Apparently, in this situation, nothing changes for the actual slider in the case of the width, but we can spot more inconsistencies when it comes to the track width, which, by default, stretches to fill the content-box of the parent input in all three browsers.

In Firefox, if we explicitly set a width, any width on the track, then the track takes this width we give it, expanding outside of its parent slider or shrinking inside, but always staying middle aligned with it. Not bad at all, but, sadly, it turns out Firefox is the only browser that behaves in a sane manner here.

Explicitly setting a width on the track changes the width of the track in Firefox, but not that of the parent slider.

In Chrome, the track width we set is completely ignored and it looks like there's no sane way of making it have a value that doesn't depend on that of the parent slider.

Changing the width of the track doesn't do anything in Chrome (computed value remains 129px).

As for insane ways, using transform: scaleX(factor) seems to be the only way to make the track wider or narrower than its parent slider. Do note doing this also causes quite a few side effects. The thumb is scaled horizontally as well and its motion is limited to the scaled down track in Chrome and Edge (as the thumb is a child of the track in these browsers), but not in Firefox, where its size is preserved and its motion is still limited to the input, not the scaled down track (since the track and thumb are siblings here). Any lateral padding, border or margin on the track is also going to be scaled.

Moving on to Edge, the track again takes any width we set.

Edge also allows us to set a track width that's different from that of the parent slider.

This is not the same situation as Firefox however. While setting a width greater than that of the parent slider on the track makes it expand outside, the two are not middle aligned. Instead, the left border limit of the track is left aligned with the left content limit of its range input parent. This alignment inconsistency on its own wouldn't be that much of a problem - a margin-left set only on ::-ms-track could fix it.

However, everything outside of the parent slider's content-box gets cut out in Edge. This is not equivalent to having overflow set to hidden on the actual input, which would cut out everything outside the padding-box, not content-box. Therefore, it cannot be fixed by setting overflow: visible on the slider.

This clipping is caused by the elements between the input and the track having overflow: hidden, but, since we cannot access these, we also cannot fix this problem. Setting everything such that no component (including its box-shadow) goes outside the content-box of the range is an option in some cases, but not always.

For the height, Firefox behaves in a similar manner it did for the width. The track expands or shrinks vertically to the height we set without affecting the parent slider and always staying middle aligned to it vertically.

Explicitly setting a height on the track changes the height of the track in Firefox, but not that of the parent slider.

The default value for this height with no styles set on the actual input or track is .2em.

Changing the font-size on the track changes its computed height in Firefox.

Unlike in the case of the width, Chrome allows the track to take the height we set and, if we're not using a % value here, it also makes the content-box of the parent slider expand or shrink such that the border-box of the track perfectly fits in it. When using a % value, the actual slider and the track are middle aligned vertically.

Explicitly setting a height on the track in % changes the height of the track in Chrome, but not that of the parent slider. Using other units, the actual range input expands or shrinks vertically such that the track perfectly fits inside.

The computed value we get for the height without setting any custom styles is the same as for the slider and doesn't change with the font-size.

Changing the font-size on the track doesn't change its computed height in Chrome.

What about Edge? Well, we can change the height of the track independently of that of the parent slider and they both stay middle aligned vertically, but all of this is only as long as the track height we set is smaller than the initial height of the actual input. Above that, the track's computed height is always equal to that of the parent range.

Explicitly setting a height on the track in Edge doesn't change the height of the parent slider and the two are middle aligned. However, the height of the track is limited by that of the actual input.

The initial track height is 11px and this value doesn't depend on the font-size or on the viewport.

Changing the font-size on the track doesn't change its computed height in Edge.

Moving on to something less mindbending, we have box-sizing. This is border-box in Chrome and content-box in Edge and Firefox so, if we're going to have a non-zero border or padding, then box-sizing is a property we need to explicitly set in order to even things out.

The box-sizing of the track, comparative look at all three browsers (from top to bottom: Chrome, Firefox, Edge).

The default track margin and padding are both 0 in all three browsers - finally, an oasis of consistency!

The box-sizing of the track, comparative look at all three browsers (from top to bottom: Chrome, Firefox, Edge).

The values for the color property can be inherited from the parent slider in all three browsers.

The color of the track, comparative look at Chrome (top) and Firefox (bottom).

Even so, Edge is the odd one here, changing it to white, though setting it to initial changes it to black, which is the value we have for the actual input.

Resetting the color to initial in Edge.

Setting -webkit-appearance: none on the actual input in Edge makes the computed value of the color on the track transparent (if we haven't explicitly set a color value ourselves). Also, once we add a background on the track, the computed track color suddenly changes to black.

Unexpected consequence of adding a background track in Edge.

To a certain extent, the ability to inherit the color property is useful for theming, though inheriting custom properties can do a lot more here. For example, consider we want to use a silver for secondary things and an orange for what we want highlighted. We can define two CSS variables on the body and then use them across the page, even inside our range inputs.

body { --fading: #bbb; --impact: #f90 } h2 { border-bottom: solid .125em var(--impact) } h6 { color: var(--fading) } [type='range']:focus { box-shadow: 0 0 2px var(--impact) } @mixin track() { background: var(--fading) } @mixin thumb() { background: var(--impact) }

Sadly, while this works in Chrome and Firefox, Edge doesn't currently allow custom properties on the range inputto be inherited down to its components.

Expected result (left) vs. result in Edge (right), where no track or thumb show up (live demo).

By default, there is no border on the track in Chrome or Firefox (border-width is 0 and border-style is none).

The border of the track, comparative look at Chrome (top) and Firefox (bottom).

Edge has no border on the track if we have no background set on the actual input and no background set on the track itself. However, once that changes, we get a thin (1px) black track border.

Another unexpected consequence of adding a track or parent slider background in Edge.

The default background-color is shown to be inherited as white, but then somehow we get a computed value of rgba(0,0,0,0) (transparent) in Chrome (both before and after -webkit-appearance: none). This also makes me wonder how come we can see the track before, since there's no background-color or background-image to give us anything visible. Firefox gives us a computed value of rgb(153,153,153) (#999) and Edge transparent (even though we might initially think it's some kind of silver, that is not the background of the ::-ms-track element - more on that a bit later).

The background-color of the track, comparative look at all three browsers (from top to bottom: Chrome, Firefox, Edge). The range thumb component

Ready for the most annoying inconsistency yet? The thumb moves within the limits of the track's content-box in Chrome and within the limits of the actual input's content-box in Firefox and Edge, even when we make the track longer or shorter than the input (Chrome doesn't allow this, forcing the track's border-box to fit the slider's content-box horizontally).

The way Chrome behaves is illustrated below:

Recording of the thumb motion in Chrome from one end of the slider to the other.

The padding is transparent, while the content-box and the border are semitransparent. We've used orange for the actual slider, red for the track and purple for the thumb.

For Firefox, things are a bit different:

Recording of the thumb motion in Firefox from one end of the slider to the other (the three cases from top to bottom: the border-box of the track perfectly fits the content-box of the slider horizontally, it's longer and it's shorter).

In Chrome, the thumb is the child of the track, while in Firefox it's its sibling, so, looking at it this way, it makes sense that Chrome would move the thumb within the limits of the track's content-box and Firefox would move it within the limits of the slider's content-box. However, the thumb is inside the track in Edge too and it still moves within the limits of the slider's content-box.

Recording of the thumb motion in Edge from one end of the slider to the other (the three cases from top to bottom: the border-box of the track perfectly fits the content-box of the slider horizontally, it's longer and it's shorter).

While this looks very strange at first, it's because Edge forces the position of the track to static and we cannot change that, even if we set it to relative with !important.

Trying (and failing) to change the value of the position property on the track in Edge.

This means we may style our slider exactly the same for all browsers, but if its content-box doesn't coincide to that of its track horizontally (so if we have a non-zero lateral padding or border on the track), it won't move within the same limits in all browsers.

Furthermore, if we scale the track horizontally, then Chrome and Firefox behave as they did before, the thumb moving within the limits of the now scaled track's content-box in Chrome and within the limits of the actual input's content-box in Firefox. However, Edge makes the thumb move within an interval whose width equals that of the track's border-box, but starts from the left limit of the track's padding-box, which is probably explained by the fact that the transform property creates a stacking context.

Recording of the thumb motion in Edge when the track is scaled horizontally.

Vertically, the thumb is middle-aligned to the track in Firefox, seemingly middle-aligned in Edge, though I've been getting very confusing different results over multiple tests of the same situation, and the top of its border-box is aligned to the top of the track's content-box in Chrome once we've set -webkit-appearance: none on the actual input and on the thumb so that we can style the slider.

While the Chrome decision seems weird at first, is annoying in most cases and lately has even contributed to breaking things in... Edge (but more about that in a moment), there is some logic behind it. By default, the height of the track in Chrome is determined by that of the thumb and if we look at things this way, the top alignment doesn't seem like complete insanity anymore.

However, we often want a thumb that's bigger than the track's height and is middle aligned to the track. We can correct the Chrome alignment with margin-top in the styles we set on the ::-webkit-slider-thumb pseudo.

Unfortunately, this way we're breaking the vertical alignment in Edge. This is because Edge now applies the styles set via ::-webkit-slider-thumb as well. At least we have the option of resetting margin-top to 0 in the styles we set on ::-ms-thumb. The demo below shows a very simple example of this in action.

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

Just like in the case of the track, the value of the box-sizing property is border-box in Chrome and content-box in Edge and Firefox, so, for consistent results across browsers, we need to set it explicitly if we want to have a non-zero border or padding on the thumb.

The margin and padding are both 0 by default in all three browsers.

After setting -webkit-appearance: none on both the slider and the thumb (setting it on just one of the two doesn't change anything), the dimensions of the thumb are reset from 10x21 (dimensions that don't depend on the font-size) to 129x0 in Chrome. The height of the track and actual slider also get reset to 0, since they depend on that of their content (the thumb inside, whose height has become 0).

The thumb box model in Chrome.

This is also why explicitly setting a height on the thumb makes the track take the same height.

According to Chrome DevTools, there is no border in either case, even though, before setting -webkit-appearance: none, it sure looks like there is one.

How the slider looks in Chrome before setting -webkit-appearance: none.

If that's not a border, it might be an outline or a box-shadow with no blur and a positive spread. But, according to Chrome DevTools, we don't have an outline, nor box-shadow on the thumb.

Computed values for outline and box-shadow in Chrome DevTools.

Setting -webkit-appearance: none in Edge makes the thumb dimensions go from 11x11 (values that don't depend on the font-size) to 0x0. Explicitly setting a height on the thumb makes the track take the initial height (11px).

The thumb box model in Edge.

In Edge, there's initially no border on the thumb. However, after setting a background on either the actual range input or any of its components, we suddenly get a solid 1px white lateral one (left and right, but not top and bottom), which visually turns to black in the :active state (even though Edge DevTools doesn't seem to notice that). Setting -webkit-appearance: none removes the border-width.

The thumb border in Edge.

In Firefox, without setting a property like background on the range input or its components, the dimensions of the thumb are 1.666x3.333 and, in this case, they don't change with the font-size. However, if we set something like background: transparent on the slider (or any background value on its components), then both the width and height of the thumb become 1em.

The thumb box model in Firefox.

In Firefox, if we are to believe what we see in DevTools, we initially have a solid thick grey (rgb(153, 153, 153)) border.

The thumb border in Firefox DevTools.

Visually however, I can't spot this thick grey border anywhere.

How the slider looks initially in Firefox, before setting a background on it or on any of its components.

After setting a background on the actual range input or one of its components, the thumb border actually becomes visually detectable and it seems to be .1em.

The thumb border in Firefox.

In Chrome and in Edge, the border-radius is always 0.

The thumb border-radius in Chrome (top) and Edge (bottom).

In Firefox however, we have a .5em value for this property, both before and after setting a background on the range input or on its components, even though the initial shape of the thumb doesn't look like a rectangle with rounded corners.

The thumb border-radius in Firefox.

The strange initial shape of the thumb in Firefox has made me wonder whether it doesn't have a clip-path set, but that's not the case according to DevTools.

The thumb clip-path in Firefox.

More likely, the thumb shape is due to the -moz-field setting, though, at least on Windows 10, this doesn't make it look like every other slider.

Initial appearance of slider in Firefox vs. appearance of a native Windows 10 slider.

The thumb's background-color is reported as being rgba(0, 0, 0, 0) (transparent) by Chrome DevTools, even though it looks grey before setting -webkit-appearance: none. We also don't seem to have a background-image that could explain the gradient or the lines on the thumb before setting -webkit-appearance: none. Firefox DevTools reports it as being rgb(240, 240, 240), even though it looks blue as long as we don't have a background explicitly set on the actual range input or on any of its components.

The thumb background-color in Chrome (top) and Firefox (bottom).

In Edge, the background-color is rgb(33, 33, 33) before setting -webkit-appearance: none and transparent after.

The thumb background-color in Edge. The range progress (fill) component

We only have dedicated pseudo-elements for this in Firefox (::-moz-range-progress) and in Edge (::-ms-fill-lower). Note that this element is a sibling of the track in Firefox and a descendant in Edge. This means that it's sized relative to the actual input in Firefox, but relative to the track in Edge.

In order to better understand this, consider that the track's border-box perfectly fits horizontally within the slider's content-box and that the track has both a border and a padding.

In Firefox, the left limit of the border-box of the progress component always coincides with the left limit of the slider's content-box. When the current slider value is its minimum value, the right limit of the border-box of our progress also coincides with the left limit of the slider's content-box. When the current slider value is its maximum value, the right limit of the border-box of our progress coincides with the right limit of the slider's content-box.

This means the width of the border-box of our progress goes from 0 to the width of the slider's content-box. In general, when the thumb is at x% of the distance between the two limit value, the width of the border-box for our progress is x% of that of the slider's content-box.

This is shown in the recording below. The padding area is always transparent, while the border area and content-box are semitransparent (orange for the actual input, red for the track, grey for the progress and purple for the thumb).

How the width of the ::-moz-range-progress component changes in Firefox.

In Edge however, the left limit of the fill's border-box always coincides with the left limit of the track's content-box while the right limit of the fill's border-box always coincides with the vertical line that splits the thumb's border-box into two equal halves. This means that when the current slider value is its minimum value, the right limit of the fill's border-box is half the thumb's border-box to the right of the left limit of the track's content-box. And when the current slider value is its maximum value, the right limit of the fill's border-box is half the thumb's border-box to the left of the right limit of the track's content-box.

This means the width of the border-box of our progress goes from half the width of the thumb's border-box minus the track's left border and padding to the width of the track's content-box plus the track's right padding and border minus half the width of the thumb's border-box. In general, when the thumb is at x% of the distance between the two limit value, the width of the border-box for our progress is its minimum width plus x% of the difference between its maximum and its minimum width.

This is all illustrated by the following recording of this live demo you can play with:

How the width of the ::-ms-fill-lower component changes in Edge.

While the description of the Edge approach above might make it seem more complicated, I've come to the conclusion that this is the best way to vary the width of this component as the Firefox approach may cause some issues.

For example, consider the case when we have no border or padding on the track for cross browser consistency and the height of the both the fill's and thumb's border-box equal to that of the track. Furthermore, the thumb is a disc (border-radius: 50%).

In Edge, all is fine:

How our example works in Edge.

But in Firefox, things look awkward (live demo):

How our example works in Firefox.

The good news is that we don't have other annoying and hard to get around inconsistencies in the case of this component.

box-sizing has the same computed value in both browsers - content-box.

The computed value for box-sizing in the case of the progress (fill) component: Firefox (top) and Edge (bottom).

In Firefox, the height of the progress is .2em, while the padding, border and margin are all 0.

The height of the progress in Firefox.

In Edge, the fill's height is equal to that of the track's content-box, with the padding, border and margin all being 0, just like in Firefox.

The height of the fill in Edge.

Initially, the background of this element is rgba(0, 0, 0, 0) (transparent, which is why we don't see it at first) in Firefox and rgb(0, 120, 115) in Edge.

The background-color of the progress (fill) in Firefox (top) and Edge (bottom).

In both cases, the computed value of the color property is rgb(0, 0, 0) (solid black).

The computed value for color in the case of the progress (fill) component: Firefox (top) and Edge (bottom).

WebKit browsers don't provide such a component and, since we don't have a way of accessing and using a track's ::before or ::after pseudos anymore, our only option of emulating this remains layering an extra, non-repeating background on top of the track's existing one for these browsers and making the size of this extra layer along the x axis depend depend on the current value of the range input.

The simplest way of doing this nowadays is by using a current value --val CSS variable, which holds the slider's current value. We update this variable every time the slider's value changes and we make the background-size of this top layer a calc() value depending on --val. This way, we don't have to recompute anything when the value of the range input changes - our calc() value is dynamic, so updating the --val variable is enough (not just for this background-size, but also for other styles that may depend on it as well).

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

Also doing this for Firefox is an option if the way ::-moz-range-progress increases doesn't look good for our particular use case.

Edge also provides a ::-ms-fill-upper which is basically the complementary of the lower one and it's the silver background of this pseudo-element that we initially see to the right of the thumb, not that of the track (the track is transparent).

Tick marks and labels

Edge is the only browser that shows tick marks by default. They're shown on the track, delimiting two, five, ten, twenty sections, the exact number depending initially on the track width. The only style we can change for these tick marks is the color property as this is inherited from the track (so setting color: transparent on the track removes the initial tick marks in Edge).

The structure that generates the initial tick marks on the track in Edge.

The spec says that tick marks and labels can be added by linking a datalist element, for whose option children we may specify a label attribute if we want that particular tick mark to also have a label.

Unfortunately, though not at all surprising anymore at this point, browsers have a mind of their own here too. Firefox doesn't show anything - no tick marks, no labels. Chrome shows the tick marks, but only allows us to control their position along the slider with the option values. It doesn't allow us to style them in any way and it doesn't show any labels.

Tick marks in Chrome.

Also, setting -webkit-appearance: none on the actual slider (which is something that we need to to in order to be able to style it) makes these tick marks disappear.

Edge joins the club and doesn't show any labels either and it doesn't allow much control over the look of the ticks either. While adding the datalist allows us to control which tick marks are shown where on the track, we cannot style them beyond changing the color property on the track component.

Tick marks in Edge.

In Edge, we also have ::-ms-ticks-before and ::-ms-ticks-after pseudo-elements. These are pretty much what they sound like - tick marks before and after the track. However, I'm having a hard time understanding how they really work.

They're hidden by display: none, so changing this property to block makes them visible if we also explicitly set a slider height, even though doing this does not change their own height.

How to make tick marks crested by ::-ms-ticks-after visible in Edge.

Beyond that, we can set properties like margin, padding, height, background, color in order to control their look. However, I have no idea how to control the thickness of individual ticks, how to give individual ticks gradient backgrounds or how to make some of them major and some minor.

So, at the end of the day, our best option if we want a nice cross-browser result remains using repeating-linear-gradient for the ticks and the label element for the values corresponding to these ticks.

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

Tooltip/ current value display

Edge is the only browser that provides a tooltip via ::-ms-tooltip, but this doesn't show up in the DOM, cannot really be styled (we can only choose to hide it by setting display: none on it) and can only display integer values, so it's completely useless for a range input between let's say .1 and .4 - all the values it displays are 0!

::-ms-tooltip when range limits are both subunitary.

So our best bet is to just hide this and use the output element for all browsers, again taking advantage of the possibility of storing the current slider value into a --val variable and then using a calc() value depending on this variable for the position.

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

Orientation

The good news is that every browser allows us to create vertical sliders. The bad news is, as you may have guessed... every browser provides a different way of doing this, none of which is the one presented in the spec (setting a width smaller than the height on the range input). WebKit browsers have opted for -webkit-appearance: slider-vertical, Edge for writing-mode: bt-lr, while Firefox controls this via an orient attribute with a value of 'vertical'.

The really bad news is that, for WebKit browsers, making a slider vertical this way leaves us unable to set any custom styles on it (as setting custom styles requires a value of none for -webkit-appearance).

Our best option is to just style our range input as a horizontal one and then rotate it with a CSS transform.

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

A Sliding Nightmare: Understanding the Range Input is a post from CSS-Tricks

::part and ::theme, an ::explainer

Wed, 12/27/2017 - 14:30

Monica Dinculescu on ::part and ::theme, two pseudo-elements that are very likely to gain traction and receive more attention in the new year. They're designed to help us create and style web components, as Monica explains:

The current new proposal is ::part and ::theme, a set of pseudo-elements that allow you to style inside a shadow tree, from outside of that shadow tree. Unlike :shadow and /deep/, they don’t allow you to style arbitrary elements inside a shadow tree: they only allow you to style elements that an author has tagged as being eligible for styling. They’ve already gone through the CSS working group and were blessed, and were brought up at TPAC at a Web Components session, so we’re confident they’re both the right approach and highly likely to be implemented as a spec by all browsers.

If the whole "shadow tree" phrase makes you panic as much as me then not to worry! Monica has already written an excellent piece that goes into great depth about web components and the Shadow DOM spec, too.

Direct Link to ArticlePermalink

::part and ::theme, an ::explainer is a post from CSS-Tricks

Fragmented HTML5 Video

Tue, 12/26/2017 - 14:21

I have seen various implementations of the Voronoi Diagram. Perhaps you've seen one without knowing what it was. It almost looks like random stained glass:

Wikipedia:

In mathematics, a Voronoi diagram is a partitioning of a plane into regions based on distance to points in a specific subset of the plane.

It's even possible to create a Voronoi diagram by hand, as eLVirus88 has documented.

I wanted to give it a try.

The Idea

My idea is to chop up a video into fragmented parts (called cells) and put them into 3D space on a slightly different z-axis. Then, by moving the mouse, you would rotate the whole experience so you would see the cells in different depths.

The Code

Building on top of Raymond Hill’s and Larix Kortbeek’s JavaScript implementation, the first thing I needed to was split up the cells.

I choose to use the <canvas> element, and put each of the cells on different canvas on a differnet 3D plane through CSS.

The Voronoi library takes care of computing all the sites to cells and creating objects with the vertices and edges for us to work with.

Cells to Canvases

First we create the canvases to match the number of Voronoi cells. These will be rendered to the DOM. The canvases and their respective contexts will be saved to an array.

var canv = document.createElement('canvas'); canv.id = 'mirror-'+i; canv.width = canvasWidth; canv.height = canvasHeight; // Append to DOM document.body.appendChild(canv); document.getElementById('container-mirrors').appendChild(canv); // Push to array canvasArray.push(canv); contextArray.push(canv.getContext('2d')); Masking

All of the canvases are now a copy of the video.

The desired effect is to show one cell per canvas. The Voronoi library provides us with a compute function. When providing the sites with the bounds we get a detailed object where we extract all of the cells edges. These will be used to create a cut out to each section using the globalCompositeOperation.

// Compute diagram = voronoi.compute(sites, bounds); // Find cell for (i=0;i<sites.length;i++) { if (!found) { cell = diagram.cells[i]; if (sites[j].voronoiId === cell.site.voronoiId) { found = 1; } } } // Create mask to only show the current cell ctx.globalCompositeOperation = 'destination-in'; ctx.globalAlpha = 1; ctx.beginPath(); var halfedges = cell.halfedges, nHalfedges = halfedges.length, v = halfedges[0].getStartpoint(); ctx.moveTo(v.x,v.y); for (var iHalfedge=0; iHalfedge<nHalfedges; iHalfedge++) { v = halfedges[iHalfedge].getEndpoint(); ctx.lineTo(v.x,v.y); } ctx.fillStyle = sites[j].c; ctx.fill(); Adding Video

Displaying video to the canvas only takes a couple of lines of code. This will be executed on requestAnimationFrame:

v = document.getElementById('video'); ctx.drawImage(v,0,0,960,540);

It's also possible to use a video input source (like a webcam), but I didn't like the result as much for this demo. If you would like to know how to use the webcam to draw to canvas using the getUserMedia() method you can read about it here.

To optimise video drawing performance skip a few frames in between the requestAnimationFrame. Videos for the web are usually encoded with a frame rate not higher than 30 fps.

See the Pen Fragmented HTML5 Video - Demo 1 by virgilspruit (@Virgilspruit) on CodePen.

Conclusion

Demos like this are my favorite things to do. Seeing what's out there and adding your own layer of interactivity to it. I'm looking forward to seeing what other people will be doing with this nice visual algorithm.

See the Pen Fragmented HTML5 Video - Demo 2 by virgilspruit (@Virgilspruit) on CodePen.

See the Pen Fragmented HTML5 Video - Demo 3 by virgilspruit (@Virgilspruit) on CodePen.

View Demos GitHub Repo

Fragmented HTML5 Video is a post from CSS-Tricks

Further working mode changes at WHATWG

Tue, 12/26/2017 - 14:09

The Web Hypertext Application Technology Working Group (WHATWG) announced that it has adopted a formal governance structure:

The WHATWG has operated successfully since 2004 with no formal governance structure, guided by a strong culture of pragmatism and collaboration. Although this has worked well for driving the web forward, we realized that we could get broader participation by being clear about what rights and responsibilities members of the community have. Concretely, this involves creating an IPR Policy and governance structure.

WHATWG was founded by folks at Apple, Mozilla and Opera. The new structure will be comprised of individuals from Apple, Google, Microsoft and Mozilla. The Big Four, you might say.

I find this interesting because we often think of the Web as a wild west where standards are always evolving and adopted at a different pace. This change largely keeps public contributions to the Living Standards in tact, but establishes a clearer line of communication between working groups and provides a path to appeal and resolve disputes over standards.

Living Standards are informed by input from contributors, driven by workstream participants, articulated by editors, and coordinated by the Steering Group. If necessary, controversies are resolved by the Steering Group with members appointed from the organizations that develop browser engines.

And, with representatives from leading browsers at the table, we may see more agreement with adoption. I'm speculating here, but it seems reasonable.

If you're like me and are fuzzy on the differences between WHATWG and W3C, Bruce Lawson has a pretty simple explanation. It still kinda blows my mind that they're both standards we often refer to but come from two completely different groups.

Direct Link to ArticlePermalink

Further working mode changes at WHATWG is a post from CSS-Tricks

Refactoring Your Way to a Design System

Tue, 12/26/2017 - 14:08

Mina Markham on refactoring a large and complex codebase into an agile design system, slowly over time:

If you’re not lucky enough to be able to start a new design system from scratch, you can start small and work on a single feature or component. With each new project comes a new opportunity to flesh out a new part of the system, and another potential case study to secure buy-in and showcase its value. Make sure to carefully and thoroughly document each new portion of the system as it’s built. After a few projects, you’ll find yourself with a decent start to a design system.

As a side note, Mina’s point here also reminds me of an old blog post called "Things You Should Never Do" by Joel Spolsky where he talks about how all this work and all this code you feel you needs to be refactored is actually solving a problem. Deleting everything and starting from scratch is almost never a good idea:

When you throw away code and start from scratch, you are throwing away all that knowledge. All those collected bug fixes. Years of programming work.

I’m not entirely sure that Joel’s piece about programming fits snuggly with Mina’s point but I think it’s an interesting one to make nonetheless: new code doesn’t necessarily mean that it’s better.

Direct Link to ArticlePermalink

Refactoring Your Way to a Design System is a post from CSS-Tricks

2017 Staff Favorites

Mon, 12/25/2017 - 16:18

It's been a very productive year for the web community, and as all of us here at CSS-Tricks roamed around to conferences, read posts, and built projects, there were some highlights of contributions that really stuck out to us. Each of us picked 5 resources that were either the most helpful, the most unique, or are things you might have missed that we think are worth checking out.

Sarah's Picks The Miracle of Generators

I quite like when someone takes a deep dive on a particular subject and does it well. I had the honor of seeing Bodil Stokke give this talk at Frontend Conference Zurich and it's as charming and entertaining as it is educational.

Designing with Grid

Jen Simmons covers the status of CSS Grid, and how to work with it from design to development. Jen is a master of grid, and the lab section of her site shows the capability of this medium.

An update since this talk has come out: Grid has also shipped in Microsoft Edge.

Vue Apollo

This is more like two things I'm interested in wrapped up in a single thing. Guillaume Chau has done a great job of creating an Apollo/GraphQL integration for Vue, including a few great demos to explore.

The Coding Train

This resource has been out for a while, but Dan now has such a great collection that it's a great time to mention it.

Coding Train are small, half-hour chunks of tutorials- everything from creating a star field to learning how Neural Networks work. Dan is an incredibly engaging and lovable instructor and makes you feel very welcome when exploring new concepts.

Motion in Design Systems

Val Head gets to the heart of the matter when it comes to integrating motion into a Design System or Component Library.

It can be really tough to communicate animation because you necessarily need to collaborate between design and engineering. Val gives you some tools to make this process function well.

Robin's Picks Design Systems

Design Systems by Alla Kholmatova was one of the best design books I read in 2017.

It’s a book all about how to collaborate with a team and reveals that code quality is only one part of designing great systems on the web. The book isn’t so much about design as much as it’s about learning how to communicate across large groups of people and how we can better communicate with everyone in an organization.

Web Typography

Another great book I read this year was Web Typography by Richard Rutter, this time focusing a great deal more on the relationship between CSS and typesetting.

My favorite part of the book though is where Richard describes that web typography is fundamentally different from other forms of typesetting and requires a series of new skills. It makes for exciting reading.

Purple.pm

For most of this year I’ve been focusing on improving my UX and product design skills and I have to say that Purple was the most useful tool for organizing large amounts of data and research. I used it as an archive that stored every document I made, every Balsamiq wireframe and hi-fi Figma mockup I created all in one place. This made it so much easier to communicate with other teams and explain my thinking on a project.

Figma

This year I switched to using the web-based design tool Figma full time. It's been so very useful because my day job work requires collaborating with dozens of engineers, product managers and other designers — so being able to quickly share a mockup with them and get feedback has exponentially improved my design chops. Plus, it reminds me of Fireworks which is probably one of the best apps ever made.

Inkwell

This year Hoefler & Co. released Inkwell, a new family of typefaces that mimics a variety of handwriting styles and I can’t stop thinking about them. One great example of their use is on Chris' blog where all these weird styles shouldn't work at all but somehow they just do.

Chris' Picks A Design System for All Americans by Mina Markham

A masterclass in public speaking if you ask me. Funny, personal, and right on target for the kind of work a lot of us are doing.

Notion

Notion is probably the most significant bit of new software I've switched to this year. It's a notes app at it's core and it's feature-rich in that regard. One of my favorites features is that every note is also a folder, so you can get as nested as you like with your organization. If you give it a shot, I bet you'll be able to see how it quickly can replace lots of other apps.

Most significant to a list like this, is that it's built for the web, but also has native apps on a variety of platforms. I think 2017 was significant in that we started to really feel a blurring between what is web and what is platform native. I suspect it will get harder and hard to tell, and then with all the advantages the web has inherently, it will make less and less sense to build anywhere else.

CSS

CSS had a banner year. CSS Grid, of course, but we also got font-display starting to ship, which is wonderful for performance. We got landmark selectors like :focus-within that prove parent selectors aren't impossible. Vector graphics has moved it way into CSS with a collection of properties, including animation and movement. You might say CSS has gotten more capable and easier. I enjoyed writing posts like this one about a slider that shows how far you can get in CSS these days.

RIP Firebug

I think it's nice the Firebug homepage is an homage, goodbye, and short history to Firebug. Firebug laid the foundation for what good DevTools could be. I'm glad browsers have taken them in-house and turned them into the powerhouses we use now, but that's all thanks to Firebug.

If I had to pick the most significant three things that have made the web the development platform it is today: 1) DevTooling, started by Firebug 2) The agreement from all browsers that web standards benefit everyone and actually being disciplined about applying that thinking 3) Evergreen browsers.

PWAs

I feel like Progressive Web Apps are essentially good marketing for a collection of good ideas that benefit any website. That's exactly the case Jason Grigsby makes:

That makes it an easy pick for 2017. HTTPS! Service workers for offline friendliness! Performance focused! Do these things, and be rewarded with more features that native apps enjoy, like a position on the home screen on mobile devices. Blur them lines, y'all! Even if you don't do everything on the list, there are big advantages for every step along the way.

Geoff's Picks CSS Grid

This one comes as no surprise but it's certainly worthy of multiple mentions. Grid has really rejuvenated my love for CSS. Not only does it take a lot of the complexity of out layouts that used to require creative uses of floats, displays and positioning, but it has allowed me to cut CSS frameworks completely out of my workflow. I mean, it's that frameworks are bad or should not be used, but I personally leaned on them a lot for their grid systems. That's no longer the case and the result is one less dependency in my stack and the liberty to write more vanilla CSS.

Prototyping Tools

Robin already mentioned Figma and that is totally in line with what I'm referring to. There seemed to be an explosion of innovation in prototyping tools. Sketch, Figma, InVision and, yes, Adobe all upped their games this year and web designers were the beneficiaries. These tools have opened up have made it easier to collaborate with other designs, critique work, get client feedback, and ultimately get into the browser faster. I have never spent less time in graphic design software and it's been awesome.

Here's a taste of what I'm referring to:

Contrast in Accessibility

Often when we talk about accessibility, the focus is on things like semantics, document structure, screen readers ARIA roles. It can get super complicated. That's why I really enjoyed Lara's recent post advocating for accessible UI. Aside from being extremely well-written, she presents commonsense approaches to improving accessibility in ways that go beyond code.

One of her suggestions to check color contrasts in the design to ensure good legibility. This one really resonated with me because I recently worked on a project with a visual brand that includes a lot of greens and yellows. Running our designs through the tools Lara recommends revealed that our work failed many accessibility checks. It also taught me that accessibility really does not favor greens and yellows.

Web Typography: Designing Tables to be Read, Not Looked At

If you didn't catch Richard Rutter's post on A List Apart, I'll please go read that right now. Don't worry, I'll wait right here for you.

You back? Great!

This one was a splash of cold water in my face. Richard not only convicted me to honestly assess whether I over-design tables but also turned all my preconceptions about what a table is and what a good one looks like. Good design is problem solving and this post is one that reminds me design for solutions before aesthetics.

place-items

Chris snuck this property into a demo and I had no idea it existed. I had to forgive myself a little when I saw that browser support on it is low, but it is a really nice shortcut that combines the align-items and justify-items properties. You may have guessed why I love this: it comes in super handy with Grid and Flexbox. While support is limited it Chrome 59+ and Firefox 45+, I am stoked to see more browsers hop on board.

What are your picks for 2017?

2017 Staff Favorites is a post from CSS-Tricks

Invision Studio

Sun, 12/24/2017 - 16:03

Studio is the name of the new design tool by the team at InVision that’ll launch in January 2018 and it looks like it has a lot of great features, with shared component libraries being one of the more interesting features that I can’t wait to take a closer look at. Also I’m sure that it’ll integrate really nicely with InVision’s existing tools and apps to make prototyping a whole lot easier.

Direct Link to ArticlePermalink

Invision Studio is a post from CSS-Tricks

Many Ways to Learn

Sat, 12/23/2017 - 14:56

Julie Zhuo responds to the classic "What can I do to continue my growth?":

One of the things I believe the most firmly is that everyone has something to teach you if you’re looking for the lessons. And these people don’t have to be other designers at your company! There are many paths to becoming an awesome product designer

She lists (and explains):

  • Learn from your users
  • Learn from people with different skillsets
  • Learn by doing 

I have a draft blog post called "Tech Books are Supplementary" that I started in 2011 and somehow haven't gotten around to finishing. One of these days! The point I try to make in it, as you can imagine, is that tech books are just a slice of the learning pie.

I'm playing a lot more banjo lately, trying to level up the best I can. You know what it takes? Going to jams. YouTubing people playing the songs I want to learn. Asking for advice. Listening to tons of recordings. Playing along to those recordings. Buying and reading books on the topic. Finding tabs online.

Learning things well takes hitting it from all sides.

Direct Link to ArticlePermalink

Many Ways to Learn is a post from CSS-Tricks

Chrome is Not the Standard

Fri, 12/22/2017 - 19:35

Chris Krycho has written an excellent post about how us fickle web developers might sometimes confuse features that land in one browser as being “the future of the web.” However, Chris argues that there’s more than one browser’s vision of the web that we should care about:

No single company gets to dominate the others in terms of setting the agenda for the web. Not Firefox, with its development and advocacy of WebAssembly, dear to my heart though that is. Not Microsoft and the IE/Edge team, with its proposal of the CSS grid spec in 2011, sad though I am that it languished for as long as it did. Not Apple, with its pitch for concurrent JavaScript. And not—however good its developer relations team is—Chrome, with any of the many ideas it’s constantly trying out, including PWAs.

It’s also worth recognizing how these decisions aren’t, in almost any case, unalloyed pushes for “the future of the web.” They reflect business priorities, just like any other technical prioritization.

I particularly like Chris’ last point about business priorities because I think it’s quite easy to forget that browser manufacturers aren’t making the web a better place out of sheer kindness; they’re companies with investors and incentives that might not always align with other companies’ objectives.

Direct Link to ArticlePermalink

Chrome is Not the Standard is a post from CSS-Tricks

The Rise of the Butt-less Website

Fri, 12/22/2017 - 15:59

It seems like all the cool kids have divided themselves into two cliques: the Headless CMS crowd on one side and the Static Site Generator crowd on the other. While I admit those are pretty cool team names, I found myself unable to pick a side. To paraphrase Groucho Marx, “I don't care to belong to any club that will have me as a member.”

For my own simple blog (which is embarrassingly empty at the moment), a static site generator could be a great fit. Systems like Hugo and Jekyll have both been highly recommended by developers I love and trust and look great at first glance, but I hit stumbling blocks when I wanted to change my theme or set up more complex JavaScript and interactions across pages. There are ways to solve both these issues, but that’s not the kind of weekend I want to have.

Besides that, I love to experiment, make new things, and I’ve got a major crush on Vue at the moment. Having a Headless CMS setup with a front-end that is decoupled from the back-end could be a great combination for me, but after 7+ years of PHP slinging with WordPress, the whole setup feels like overkill for my basic needs.

What I really want is a static site generator that will let me write a blog as a component of a larger single-page app so I have room to try new things and still have full control over styling, without the need for a database or any sort of back-end. This is a long way of telling you that I’ve found my own club to join, with a decidedly un-cool name.

Get ready for it...

The Butt-less Website

Because there’s no back-end, get it? &#x1f636;

It takes a few steps to go butt-less:

  1. Setup a single page app with Vue
  2. Generate each route at build time
  3. Create blog and article components
  4. Integrate Webpack to parse Markdown content
  5. Extend functionality with plugins
  6. Profit!

That last point has to be a part of every proposal, right?

I know it looks like a lot of steps but this is not quite as tough as it seems. Let's break down the steps together.

Setup a single page app with Vue

Let's get Vue up and running. We're going to need Webpack to do that.

I get it, Webpack is pretty intimidating even when you know what’s going on. It’s probably best to let someone else do the really hard work, so we’ll use the Vue Progressive Web App Boilerplate as our foundation and make a few tweaks.

We could use the default setup from the repo, but even while I was writing this article, there were changes being made there. In the interest of not having this all break on us, we will use a repo I created for demonstration purposes. The repo has a branch for each step we'll be covering in this post to help follow along.

View on GitHub

Cloning the repo and check out the step-1 branch:

$ git clone https://github.com/evanfuture/vue-yes-blog.git step-1 $ cd vue-yes-blog $ npm install $ npm run dev

One of my favorite parts of modern development is that it takes a mere thirty seconds to get a progressive web app up and running!

Next, let’s complicate things.

Generate each route at build time

Out of the box, single page apps only have a single entry point. In other words, it lives lives at a single URL. This makes sense in some cases, but we want our app to feel like a normal website.

We’ll need to make use of the history mode in the Vue Router file in order to do that. First, we’ll turn that on by adding mode: 'history' to the Router object’s properties like so:

// src/router/index.js Vue.use(Router); export default new Router({ mode: 'history', routes: [ // ...

Our starter app has two routes. In addition to Hello, we have a second view component called Banana that lives at the route /banana. Without history mode, the URL for that page would be http://localhost:1982/#/banana. History mode cleans that up to http://localhost:1982/banana. Much more elegant!

All this works pretty well in development mode (npm run dev), but let’s take a peek at what it would look like in production. Here's how we compile everything:

$ npm run build

That command will generate your Vue site into the ./dist folder. To see it live, there’s a handy command for starting up a super simple HTTP server on your Mac:

$ cd dist $ python -m SimpleHTTPServer

Sorry Windows folks, I don’t know the equivalent!

Now visit localhost:8000 in your browser. You’ll see your site as it will appear in a production environment. Click on the Banana link, and all is well.

Refresh the page. Oops! This reveals our first problem with single page apps: there is only one HTML file being generated at build time, so there’s no way for the browser to know that /banana should target the main app page and load the route without fancy Apache-style redirects!

Of course, there's an app for that. Or, at least a plugin. The basic usage is noted in the Vue Progressive Web App Boilerplate documentation. Here's how it says we can spin up the plugin:

$ npm install -D prerender-spa-plugin

Let's add our routes to the Webpack production configuration file:

// ./build/webpack.prod.conf.js // ... const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin') const PrerenderSpaPlugin = require('prerender-spa-plugin') const loadMinified = require('./load-minified') // ... const webpackConfig = merge(baseWebpackConfig, { // ... plugins: [ // ... new SWPrecacheWebpackPlugin({ // ... minify: true, stripPrefix: 'dist/' }), // prerender app new PrerenderSpaPlugin( // Path to compiled app path.join(__dirname, '../dist'), // List of endpoints you wish to prerender [ '/', '/banana' ] ) ] })

That’s it. Now, when you run a new build, each route in that array will be rendered as a new entry point to the app. Congratulations, we’ve basically just enabled static site generation!

Create blog and article components

If you’re skipping ahead, we’re now up to the step-2 branch of my demo repo. Go ahead and check it out:

$ git checkout step-2

This step is pretty straightforward. We’ll create two new components, and link them together.

Blog Component

Let's register the the blog component. We'll create a new file called YesBlog.vue in the /src/components directory and drop in the markup for the view:

// ./src/components/YesBlog.vue <template> <div class="blog"> <h1>Blog</h1> <router-link to="/">Home</router-link> <hr/> <article v-for="article in articles" :key="article.slug" class="article"> <router-link class="article__link" :to="`/blog/${ article.slug }`"> <h2 class="article__title">{{ article.title }}</h2> <p class="article__description">{{article.description}}</p> </router-link> </article> </div> </template> <script> export default { name: 'blog', computed: { articles() { return [ { slug: 'first-article', title: 'Article One', description: 'This is article one\'s description', }, { slug: 'second-article', title: 'Article Two', description: 'This is article two\'s description', }, ]; }, }, }; </script>

All we’re really doing here is creating a placeholder array (articles) that will be filled with article objects. This array creates our article list and uses the slug parameter as the post ID. The title and description parameters fill out the post details. For now, it’s all hard-coded while we get the rest of our code in place.

Article Component

The article component is a similar process. We'll create a new file called YesArticle.vue and establish the markup for the view:

// ./src/components/YesArticle.vue <template> <div class="article"> <h1 class="blog__title">{{article.title}}</h1> <router-link to="/blog">Back</router-link> <hr/> <div class="article__body" v-html="article.body"></div> </div> </template> <script> export default { name: 'YesArticle', props: { id: { type: String, required: true, }, }, data() { return { article: { title: this.id, body: '<h2>Testing</h2><p>Ok, let\'s do more now!</p>', }, }; }, }; </script>

We’ll use the props passed along by the router to know what article ID we’re working with. For now, we’ll just use that as the post title, and hardcode the body.

Routing

We can't move ahead until we add our new views to the router. This will ensure that our URLs are valid and allows our navigation to function properly. Here is the entirety of the router file:

// ./src/router/index.js import Router from 'vue-router'; import Hello from '@/components/Hello'; import Banana from '@/components/Banana'; import YesBlog from '@/components/YesBlog'; import YesArticle from '@/components/YesArticle'; Vue.use(Router); export default new Router({ mode: 'history', routes: [ { path: '/', name: 'Hello', component: Hello, }, { path: '/banana', name: 'Banana', component: Banana, }, { path: '/blog', name: 'YesBlog', component: YesBlog, }, { path: '/blog/:id', name: 'YesArticle', props: true, component: YesArticle, }, ], });

Notice that we've appended /:id to the YesArtcle component's path and set its props to true. These are crucial because they establish the dynamic routing we set up in the component's props array in the component file.

Finally, we can add a link to our homepage that points to the blog. This is what we drop into the Hello.vue file to get that going:

<router-link to="/blog">Blog</router-link> Pre-rendering

We've done a lot of work so far but none of it will stick until we pre-render our routes. Pre-rendering is a fancy way of saying that we tell the app what routes exist and to dump the right markup into the right route. We added a Webpack plugin for this earlier, so here's what we can add to our Webpack production configuration file:

// ./build/webpack.prod.conf.js // ... // List of endpoints you wish to prerender [ '/', '/banana', '/blog', '/blog/first-article', '/blog/second-article' ] // ...

I have to admit, this process can be cumbersome and annoying. I mean, who wants to touch multiple files to create a URL?! Thankfully, we can automate this, which we'll cover further down.

Integrate Webpack to parse Markdown content

We’re now up to the step-3 branch. Check it out if you're following along in the code:

$ git checkout step-3 The Posts

We’ll be using Markdown to write our posts, with some FrontMatter to create meta data functionality.

Fire up a new file in the posts directory to create our very first post:

// ./src/posts/first-article.md --- title: Article One from MD description: In which the hero starts fresh created: 2017-10-01T08:01:50+02 updated: status: publish --- Here is the text of the article. It's pretty great, isn't it? // ./src/posts/second-article.md --- title: Article Two from MD description: This is another article created: 2017-10-01T08:01:50+02 updated: status: publish --- ## Let's start with an H2 And then some text And then some code: ```html <div class="container"> <div class="main"> <div class="article insert-wp-tags-here"> <h1>Title</h1> <div class="article-content"> <p class="intro">Intro Text</p> <p></p> </div> <div class="article-meta"></div> </div> </div> </div> ``` Dynamic Routing

One annoying thing at the moment is that we need to hardcode our routes for the pre-rendering plugin. Luckily, it isn’t complicated to make this dynamic with a bit of Node magic. First, we’ll create a helper in our utility file to find the files:

// ./build/utils.js // ... const ExtractTextPlugin = require('extract-text-webpack-plugin') const fs = require('fs') exports.filesToRoutes = function (directory, extension, routePrefix = '') { function findFilesInDir(startPath, filter){ let results = [] if (!fs.existsSync(startPath)) { console.log("no dir ", startPath) return } const files = fs.readdirSync(startPath) for (let i = 0; i < files.length; i++) { const filename = path.join(startPath, files[i]) const stat = fs.lstatSync(filename) if (stat.isDirectory()) { results = results.concat(findFilesInDir(filename, filter)) //recurse } else if (filename.indexOf(filter) >= 0) { results.push(filename) } } return results } return findFilesInDir(path.join(__dirname, directory), extension) .map((filename) => { return filename .replace(path.join(__dirname, directory), routePrefix) .replace(extension, '') }) } exports.assetsPath = function (_path) { // ...

This can really just be copied and pasted, but what we’ve done here is create a utility method called filesToRoutes() which will take in a directory, extension, and an optional routePrefix, and return an array of routes based on a recursive file search within that directory.

All we have to do to make our blog post routes dynamic is merge this new array into our PrerenderSpaPlugin routes. The power of ES6 makes this really simple:

// ./build/webpack.prod.conf.js // ... new PrerenderSpaPlugin( // Path to compiled app path.join(__dirname, '../dist'), // List of endpoints you wish to prerender [ '/', '/banana', '/blog', ...utils.filesToRoutes('../src/posts', '.md', '/blog') ] )

Since we've already imported utils at the top of the file for other purposes, we can just use the spread operator ... to merge the new dynamic routes array into this one, and we’re done. Now our pre-rendering is completely dynamic, only dependent on us adding a new file!

Webpack Loaders

We’re now up to the step-4 branch:

$ git checkout step-4

In order to actually turn our Markdown files into parse-able content, we’ll need some Webpack loaders in place. Again, someone else has done all the work for us, so we only have to install and add them to our config.

$ npm install -D json-loader markdown-it-front-matter-loader markdown-it highlight.js yaml-front-matter

We will only be calling the json-loader and markdown-it-front-matter-loader from our Webpack config, but the latter has peer dependencies of markdown-it and highlight.js, so we’ll install those at the same time. Also, nothing warns us about this, but yaml-front-matter is also required, so the command above adds that as well.

To use these fancy new loaders, we’re going to add a block to our Webpack base config:

// ./build/webpack.base.conf.js // ... module.exports = { // ... module: { rules: [ // ... { test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('fonts/[name].[hash:7].[ext]') } }, { test: /\.md$/, loaders: ['json-loader', 'markdown-it-front-matter-loader'], }, ] } }

Now, any time Webpack encounters a require statement with a .md extension, it will use the front-matter-loader (which will correctly parse the metadata block from our articles as well as the code blocks), and take the output JSON and run it through the json-loader. This way, we know we’re ending up with an object for each article that looks like this:

// first-article.md [Object] { body: "<p>Here is the text of the article. It's pretty great, isn't it?</p>\n" created: "2017-10-01T06:01:50.000Z" description: "In which the hero starts fresh" raw: "\n\nHere is the text of the article. It's pretty great, isn't it?\n" slug: "first-article" status: "publish" title: "Article One from MD" updated: null }

This is exactly what we need and it’s pretty easy to extend with other metadata if you need to. But so far, this doesn’t do anything! We need to require these in one of our components so that Webpack can find and load it.

We could just write:

require('../posts/first-article.md')

...but then we’d have to do that for every article we create, and that won’t be any fun as our blog grows. We need a way to dynamically require all our Markdown files.

Dynamic Requiring

Luckily, Webpack does this! It wasn’t easy to find documentation for this but here it is. There is a method called require.context() that we can use to do just what we need. We’ll add it to the script section of our YesBlog component:

// ./src/components/YesBlog.vue // ... <script> const posts = {}; const req = require.context('../posts/', false, /\.md$/); req.keys().forEach((key) => { posts[key] = req(key); }); export default { name: 'blog', computed: { articles() { const articleArray = []; Object.keys(posts).forEach((key) => { const article = posts[key]; article.slug = key.replace('./', '').replace('.md', ''); articleArray.push(article); }); return articleArray; }, }, }; </script> // ...

What’s happening here? We’re creating a posts object that we’ll first populate with articles, then use later within the component. Since we’re pre-rendering all our content, this object will be instantly available.

The require.context() method accepts three arguments.

  • the directory where it will search
  • whether or not to include subdirectories
  • a regex filter to return files

In our case, we only want Markdown files in the posts directory, so:

require.context('../posts/', false, /\.md$/);

This will give us a kind of strange new function/object that we need to parse in order to use. That's where req.keys() will give us an array of the relative paths to each file. If we call req(key), this will return the article object we want, so we can assign that value to a matching key in our posts object.

Finally, in the computed articles() method, we’ll auto-generate our slug by adding a slug key to each post, with a value of the file name without a path or extensions. If we wanted to, this could be altered to allow us to set the slug in the Markdown itself, and only fall back to auto-generation. At the same time, we push the article objects into an array, so we have something easy to iterate over in our component.

Extra Credit

There are two things you’ll probably want to do right away if you use this method. First is to sort by date and second is to filter by article status (i.e. draft and published). Since we already have an array, this can be done in one line, added just before return articleArray:

articleArray.filter(post => post.status === 'publish').sort((a, b) => a.created < b.created); Final Step

One last thing to do now, and that’s instruct our YesArticle component to use the new data we’re receiving along with the route change:

// ./src/components/YesArticle.vue // ... data() { return { article: require(`../posts/${this.id}.md`), // eslint-disable-line global-require, import/no-dynamic-require }; },

Since we know that our component will be pre-rendered, we can disable the ESLint rules that disallow dynamic and global requires, and require the path to the post that matches the id parameter. This triggers our Webpack Markdown loaders, and we’re all done!

OMG!

Go ahead and test this out:

$ npm run build && cd dist && python -m SimpleHTTPServer

Visit localhost:8000, navigate around and refresh the pages to load the whole app from the new entry point. It works!

I want to emphasize just how cool this is. We’ve turned a folder of Markdown files into an array of objects that we can use as we wish, anywhere on our website. The sky is the limit!

If you want to just see how it all works, you can check out the final branch:

$ git checkout step-complete Extend functionality with plugins

My favorite part about this technique is that everything is extensible and replaceable.

Did someone create a better Markdown processor? Great, swap out the loader! Need control over your site’s SEO? There’s a plugin for that. Need to add a commenting system? Add that plugin, too.

I like to keep an eye on these two repositories for ideas and inspiration:

Profit!

You thought this step was a joke?

The very last thing we’ll want to do now is profit from the simplicity we’ve created and nab some free hosting. Since your site is now being generated on your git repository, all you really need is to do is push your changes to Github, Bitbucket, Gitlab or whatever code repository you use. I chose Gitlab because private repos are free and I didn’t want to have my drafts public, even in repo-form.

After that's that set up, you need to find a host. What you really want is a host that offers continuous integration and deployment so that merging to your master branch triggers the npm run build command and regenerates your site.

I used Gitlab’s own CI tools for the first few months after I set this up. I found the setup to be easy but troubleshooting issues to be difficult. I recently switched to Netlify, which has an outstanding free plan and some great CLI tools built right in.

In both cases, you’re able to point your own domain at their servers and even setup an SSL certificate for HTTPS support—that last point being important if you ever want to experiment with things like the getUserMedia API, or create a shop to make sales.

With all this set up, you’re now a member of the Butt-less Website club. Congratulations and welcome, friends! Hopefully you find this to be a simple alternative to complex content management systems for your own personal website and that it allows you to experiment with ease. Please let me know in the comments if you get stuck along the way...or if you succeed beyond your wildest dreams. &#x1f609;

The Rise of the Butt-less Website is a post from CSS-Tricks

Pages