Developer News

Learn Design for Developers and SVG Animation with Sarah Drasner ?????

Css Tricks - Thu, 09/05/2019 - 4:16am

(This is a sponsored post.)

Hey, Marc here from Frontend Masters — excited to support CSS-Tricks ❤️!

Have you checked out Sarah Drasner's courses yet? She has two awesome courses on Design for Developers and SVG! Plus another introducing Vue.js!

Design for Developers

In the Design for Developers course, you’ll learn to become self-sufficient throughout the entire lifecycle of the project — from concept to design to implementation!

You’ll learn to...

  • Code dynamic layouts and understand the principles of composition.
  • Select the typography that compliments your design by following simple rules.
  • Choose colors palettes, and understand the theories to understand why they work together.
  • Know how and when to reach for design tools such as Photoshop or Sketch.
  • Use the correct image formats for performance.
  • Prototype to quickly communicate your ideas and get your layout up-and-running.
SVG Essentials and Animation

In the SVG Essentials and Animation course, you’ll learn to build and optimize SVG – the scalable graphics format for the web that can achieve impressively small file sizes for fast-loading websites.

You’ll learn to...

  • 〰 Create platonic and custom shapes with path commands.
  • ⚡️ Optimize SVG to achieve smaller file sizes for performance.
  • 💻 Assemble new SVGs with code and graphics programs.
  • 🖱 Control complex animations and timelines with user input.
  • 💥 Leverage GreenSock’s JavaScript libraries for immersive animation effects.
Introduction to Vue.js

In the Introduction to Vue.js course, you'll get started quickly with the Vue.js JavaScript framework!

You'll learn to...

  • Build custom, reusable components and animate them.
  • Use props, slots, and scoped styles to create flexible components.
  • Grok advanced features like filters and mixins for transforming data.
  • Get a single page application up and running fast with the Vue-CLI.
  • Work with Vuex to manage the state of larger-scale applications.

This course is for developers with an intermediate knowledge of JavaScript who want to learn how to build and maintain complex applications quickly and efficiently.

You'll love Sarah's awesome courses!

Direct Link to ArticlePermalink

The post Learn Design for Developers and SVG Animation with Sarah Drasner ✨💖 appeared first on CSS-Tricks.

Multiline truncated text with “show more” button

Css Tricks - Wed, 09/04/2019 - 11:53am

Now that we've got cross-browser support for the line-clamp property, I expect we'll see a lot more of that around the web. And as we start to see it more in use, it’s worth the reminder that: Truncation is not a content strategy.

We should at least offer a way to read that that truncated content, right? Paul Bakaus uses the checkbox hack and some other trickery to add a functional button that does exactly that.

See the Pen
truncated text w/ more button (CSS only, with optional JS helper)
by Paul Bakaus (@pbakaus)
on CodePen.

Direct Link to ArticlePermalink

The post Multiline truncated text with “show more” button appeared first on CSS-Tricks.

Model-Based Testing in React with State Machines

Css Tricks - Wed, 09/04/2019 - 4:24am

Testing applications is crucially important to ensuring that the code is error-free and the logic requirements are met. However, writing tests manually is tedious and prone to human bias and error. Furthermore, maintenance can be a nightmare, especially when features are added or business logic is changed. We’ll learn how model-based testing can eliminate the need to manually write integration and end-to-end tests, by automatically generating full tests that keep up-to-date with an abstract model for any app.

From unit tests to integration tests, end-to-end tests, and more, there are many different testing methods that are important in the development of non-trivial software applications. They all share a common goal, but at different levels: ensure that when anyone uses the application, it behaves exactly as expected without any unintended states, bugs, or worse, crashes.

Testing Trophy showing importance of different types of tests

Kent C. Dodds describes the practical importance of writing these tests in his article, Write tests. Not too many. Mostly integration. Some tests, like static and unit tests, are easy to author, but don't completely ensure that every unit will work together. Other tests, like integration and end-to-end (E2E) tests, take more time to author, but give you much more confidence that the application will work as the user expects, since they replicate scenarios similar to how a user would use the application in real life.

So why are there never many integration nor E2E in applications nowadays, yet hundreds (if not thousands) of unit tests? The reasons range from not enough resources, not enough time, or not enough understanding of the importance of writing these tests. Furthermore, even if numerous integration/E2E tests are written, if one part of the application changes, most of those long and complicated tests need to be rewritten, and new tests need to be written. Under deadlines, this quickly becomes infeasible.

From Automated to Autogenerated

The status-quo of application testing is:

  1. Manual testing, where no automated tests exist, and app features and user flows are tested manually
  2. Writing automated tests, which are scripted tests that can be executed automatically by a program, instead of being manually tested by a human
  3. Test automation, which is the strategy for executing these automated tests in the development cycle.

Needless to say, test automation saves a lot of time in executing the tests, but the tests still need to be manually written. It would sure be nice to tell some sort of tool: "Here is a description of how the application is supposed to behave. Now generate all the tests, even the edge cases."

Thankfully, this idea already exists (and has been researched for decades), and it's called model-based testing. Here's how it works:

  1. An abstract "model" that describes the behavior of your application (in the form of a directed graph) is created
  2. Test paths are generated from the directed graph
  3. Each "step" in the test path is mapped to a test that can be executed on the application.

Each integration and E2E test is essentially a series of steps that alternate between:

  1. Verify that the application looks correct (a state)
  2. Simulate some action (to produce an event)
  3. Verify that the application looks right after the action (another state)

If you’re familiar with the given-when-then style of behavioral testing, this will look familiar:

  1. Given some initial state (precondition)
  2. When some action occurs (behavior)
  3. Then some new state is expected (postcondition).

A model can describe all the possible states and events, and automatically generate the "paths" needed to get from one state to another, just like Google Maps can generate the possible routes between one location and another. Just like a map route, each path is a collection of steps needed to get from point A to point B.

Integration Testing Without a Model

To better explain this, consider a simple "feedback" application. We can describe it like so:

  • A panel appears asking the user, "How was your experience?"
  • The user can click "Good" or "Bad"
  • When the user clicks "Good," a screen saying "Thanks for your feedback" appears.
  • When the user clicks "Bad," a form appears, asking for further information.
  • The user can optionally fill out the form and submit the feedback.
  • When the form is submitted, the thanks screen appears.
  • The user can click "Close" or press the Escape key to close the feedback app on any screen.

See the Pen
Untitled by David Khourshid(
@davidkpiano)
on CodePen.

Manually Testing the App

The @testing-library/react library makes it straightforward to render React apps in a testing environment with its render() function. This returns useful methods, such as:

  • getByText, which identifies DOM elements by the text contained inside of them
  • baseElement, which represents the root document.documentElement and will be used to trigger a keyDown event
  • queryByText, which will not throw an error if a DOM element containing the specified text is missing (so we can assert that nothing is rendered)
import Feedback from './App'; import { render, fireEvent, cleanup } from 'react-testing-library'; // ... // Render the feedback app const { getByText, getByTitle, getByPlaceholderText, baseElement, queryByText } = render(<Feedback />); // ...

More information can be found in the @testing-library/react documentation. Let's write a couple integration tests for this with Jest (or Mocha) and @testing-library/react:

import { render, fireEvent, cleanup } from '@testing-library/react'; describe('feedback app', () => { afterEach(cleanup); it('should show the thanks screen when "Good" is clicked', () => { const { getByText } = render(<Feedback />); // The question screen should be visible at first assert.ok(getByText('How was your experience?')); // Click the "Good" button fireEvent.click(getByText('Good')); // Now the thanks screen should be visible assert.ok(getByText('Thanks for your feedback.')); }); it('should show the form screen when "Bad" is clicked', () => { const { getByText } = render(<Feedback />); // The question screen should be visible at first assert.ok(getByText('How was your experience?')); // Click the "Bad" button fireEvent.click(getByText('Bad')); // Now the form screen should be visible assert.ok(getByText('Care to tell us why?')); }); });

Not too bad, but you'll notice that there's some repetition going on. At first, this isn't a big deal (tests shouldn't necessarily be DRY), but these tests can become less maintainable when:

  • Application behavior changes, such as adding a new steps or deleting steps
  • User interface elements change, in a way that might not even be a simple component change (such as trading a button for a keyboard shortcut or gesture)
  • Edge cases start occurring and need to be accounted for.

Furthermore, E2E tests will test the exact same behavior (albeit in a more realistic testing environment, such as a live browser with Puppeteer or Selenium), yet they cannot reuse the same tests since the code for executing the tests is incompatible with those environments.

The State Machine as an Abstract Model

Remember the informal description of our feedback app above? We can translate that into a model that represents the different states, events, and transitions between states the app can be in; in other words, a finite state machine. A finite state machine is a representation of:

  • The finite states in the app (e.g., question, form, thanks, closed)
  • An initial state (e.g., question)
  • The events that can occur in the app (e.g., CLICK_GOOD, CLICK_BAD for clicking the good/bad buttons, CLOSE for clicking the close button, and SUBMIT for submitting the form)
  • Transitions, or how one state transitions to another state due to an event (e.g., when in the question state and the CLICK_GOOD action is performed, the user is now in the thanks state)
  • Final states (e.g., closed), if applicable.

The feedback app's behavior can be represented with these states, events, and transitions in a finite state machine, and looks like this:

A visual representation can be generated from a JSON-like description of the state machine, using XState:

import { Machine } from 'xstate'; const feedbackMachine = Machine({ id: 'feedback', initial: 'question', states: { question: { on: { CLICK_GOOD: 'thanks', CLICK_BAD: 'form', CLOSE: 'closed' } }, form: { on: { SUBMIT: 'thanks', CLOSE: 'closed' } }, thanks: { on: { CLOSE: 'closed' } }, closed: { type: 'final' } } });

If you're interested in diving deeper into XState, you can read the XState docs, or read a great article about using XState with React by Jon Bellah. Note that this finite state machine is used only for testing, and not in our actual application — this is an important principle of model-based testing, because it represents how the user expects the app to behave, and not its actual implementation details. The app doesn’t necessarily need to be created with finite state machines in mind (although it’s a very helpful practice).

Creating a Test Model

The app's behavior is now described as a directed graph, where the nodes are states and the edges (or arrows) are events that denote the transitions between states. We can use that state machine (the abstract representation of the behavior) to create a test model. The @xstate/graph library contains a createModel function to do that:

import { Machine } from 'xstate'; import { createModel } from '@xstate/test'; const feedbackMachine = Machine({/* ... */}); const feedbackModel = createModel(feedbackMachine);

This test model is an abstract model which represents the desired behavior of the system under test (SUT) — in this example, our app. With this testing model, test plans can be created which we can use to test that the SUT can reach each state in the model. A test plan describes the test paths that can be taken to reach a target state.

Verifying States

Right now, this model is a bit useless. It can generate test paths (as we’ll see in the next section) but to serve its purpose as a model for testing, we need to add a test for each of the states. The @xstate/test package will read these test functions from meta.test:

const feedbackMachine = Machine({ id: 'feedback', initial: 'question', states: { question: { on: { CLICK_GOOD: 'thanks', CLICK_BAD: 'form', CLOSE: 'closed' }, meta: { // getByTestId, etc. will be passed into path.test(...) later. test: ({ getByTestId }) => { assert.ok(getByTestId('question-screen')); } } }, // ... etc. } });

Notice that these are the same assertions from the manually written tests we’ve created previously with @testing-library/react. The purpose of these tests is to verify the precondition that the SUT is in the given state before executing an event.

Executing Events

To make our test model complete, we need to make each of the events, such as CLICK_GOOD or CLOSE, “real” and executable. That is, we have to map these events to actual actions that will be executed in the SUT. The execution functions for each of these events are specified in createModel(…).withEvents(…):

import { Machine } from 'xstate'; import { createModel } from '@xstate/test'; const feedbackMachine = Machine({/* ... */}); const feedbackModel = createModel(feedbackMachine) .withEvents({ // getByTestId, etc. will be passed into path.test(...) later. CLICK_GOOD: ({ getByText }) => { fireEvent.click(getByText('Good')); }, CLICK_BAD: ({ getByText }) => { fireEvent.click(getByText('Bad')); }, CLOSE: ({ getByTestId }) => { fireEvent.click(getByTestId('close-button')); }, SUBMIT: { exec: async ({ getByTestId }, event) => { fireEvent.change(getByTestId('response-input'), { target: { value: event.value } }); fireEvent.click(getByTestId('submit-button')); }, cases: [{ value: 'something' }, { value: '' }] } });

Notice that you can either specify each event as an execution function, or (in the case of SUBMIT) as an object with the execution function specified in exec and sample event cases specified in cases.

From Model To Test Paths

Take a look at the visualization again and follow the arrows, starting from the initial question state. You'll notice that there are many possible paths you can take to reach any other state. For example:

  • From the question state, the CLICK_GOOD event transitions to...
  • the form state, and then the SUBMIT event transitions to...
  • the thanks state, and then the CLOSE event transitions to...
  • the closed state.

Since the app's behavior is a directed graph, we can generate all the possible simple paths or shortest paths from the initial state. A simple path is a path where no node is repeated. That is, we're assuming the user isn't going to visit a state more than once (although that might be a valid thing to test for in the future). A shortest path is the shortest of these simple paths.

Rather than explaining algorithms for traversing graphs to find shortest paths (Vaidehi Joshi has great articles on graph traversal if you're interested in that), the test model we created with @xstate/test has a .getSimplePathPlans(…) method that generates test plans.

Each test plan represents a target state and simple paths from the initial state to that target state. Each test path represents a series of steps to get to that target state, with each step including a state (precondition) and an event (action) that is executed after verifying that the app is in the state.

For example, a single test plan can represent reaching the thanks state, and that test plan can have one or more paths for reaching that state, such as question -- CLICK_BAD ? form -- SUBMIT ? thanks, or question -- CLICK_GOOD ? thanks:

testPlans.forEach(plan => { describe(plan.description, () => { // ... }); });

We can then loop over these plans to describe each state. The plan.description is provided by @xstate/test, such as reaches state: "question":

// Get test plans to all states via simple paths const testPlans = testModel.getSimplePathPlans();

And each path in plan.paths can be tested, also with a provided path.description like via CLICK_GOOD ? CLOSE:

testPlans.forEach(plan => { describe(plan.description, () => { // Do any cleanup work after testing each path afterEach(cleanup); plan.paths.forEach(path => { it(path.description, async () => { // Test setup const rendered = render(<Feedback />); // Test execution await path.test(rendered); }); }); }); });

Testing a path with path.test(…) involves:

  1. Verifying that the app is in some state of a path’s step
  2. Executing the action associated with the event of a path’s step
  3. Repeating 1. and 2. until there are no more steps
  4. Finally, verifying that the app is in the target plan.state.

Finally, we want to ensure that each of the states in our test model were tested. When the tests are run, the test model keeps track of the tested states, and provides a testModel.testCoverage() function which will fail if not all states were covered:

it('coverage', () => { testModel.testCoverage(); });

Overall, our test suite looks like this:

import React from 'react'; import Feedback from './App'; import { Machine } from 'xstate'; import { render, fireEvent, cleanup } from '@testing-library/react'; import { assert } from 'chai'; import { createModel } from '@xstate/test'; describe('feedback app', () => { const feedbackMachine = Machine({/* ... */}); const testModel = createModel(feedbackMachine) .withEvents({/* ... */}); const testPlans = testModel.getSimplePathPlans(); testPlans.forEach(plan => { describe(plan.description, () => { afterEach(cleanup); plan.paths.forEach(path => { it(path.description, () => { const rendered = render(<Feedback />); return path.test(rendered); }); }); }); }); it('coverage', () => { testModel.testCoverage(); }); });

This might seem like a bit of setup, but manually scripted integration tests need to have all of this setup anyway, in a much less abstracted way. One of the major advantages of model-based testing is that you only need to set this up once, whether you have 10 tests or 1,000 tests generated.

Running the Tests

In create-react-app, the tests are ran using Jest via the command npm test (or yarn test). When the tests are ran, assuming they all pass, the output will look something like this:

PASS src/App.test.js feedback app ? coverage reaches state: "question" ? via (44ms) reaches state: "thanks" ? via CLICK_GOOD (17ms) ? via CLICK_BAD ? SUBMIT ({"value":"something"}) (13ms) reaches state: "closed" ? via CLICK_GOOD ? CLOSE (6ms) ? via CLICK_BAD ? SUBMIT ({"value":"something"}) ? CLOSE (11ms) ? via CLICK_BAD ? CLOSE (10ms) ? via CLOSE (4ms) reaches state: "form" ? via CLICK_BAD (5ms) Test Suites: 1 passed, 1 total Tests: 9 passed, 9 total Snapshots: 0 total Time: 2.834s

That's nine tests automatically generated with our finite state machine model of the app! Every single one of those tests asserts that the app is in the correct state and that the proper actions are executed (and validated) to transition to the next state at each step, and finally asserts that the app is in the correct target state.

These tests can quickly grow as your app gets more complex; for instance, if you add a back button to each screen or add some validation logic to the form page (please don't; be thankful the user is even going through the feedback form in the first place) or add a loading state submitting the form, the number of possible paths will increase.

Advantages of Model-Based Testing

Model-based testing greatly simplifies the creation of integration and E2E tests by autogenerating them based on a model (like a finite state machine), as demonstrated above. Since manually writing full tests is eliminated from the test creation process, adding or removing new features no longer becomes a test maintenance burden. The abstract model only needs to be updated, without touching any other part of the testing code.

For example, if you want to add the feature that the form shows up whether the user clicks the "Good" or "Bad" button, it's a one-line change in the finite state machine:

// ... question: { on: { // CLICK_GOOD: 'thanks', CLICK_GOOD: 'form', CLICK_BAD: 'form', CLOSE: 'closed', ESC: 'closed' }, meta: {/* ... */} }, // ...

All tests that are affected by the new behavior will be updated. Test maintenance is reduced to maintaining the model, which saves time and prevents errors that can be made in manually updating tests. This has been shown to improve efficiency in both developing and testing production applications, especially as used at Microsoft on recent customer projects — when new features were added or changes made, the autogenerated tests gave immediate feedback on which parts of the app logic were affected, without needing to manually regression test various flows.

Additionally, since the model is abstract and not tied to implementation details, the exact same model, as well as most of the testing code, can be used to author E2E tests. The only things that would change are the tests for verifying the state and the execution of the actions. For example, if you were using Puppeteer, you can update the state machine:

// ... question: { on: { CLICK_GOOD: 'thanks', CLICK_BAD: 'form', CLOSE: 'closed' }, meta: { test: async (page) => { await page.waitFor('[data-testid="question-screen"]'); } } }, // ... const testModel = createModel(/* ... */) .withEvents({ CLICK_GOOD: async (page) => { const goodButton = await page.$('[data-testid="good-button"]'); await goodButton.click(); }, // ... });

And then these tests can be run against a live Chromium browser instance:

The tests are autogenerated the same, and this cannot be overstated. Although it just seems like a fancy way to create DRY test code, it goes exponentially further than that — autogenerated tests can exhaustively represent paths that explore all the possible actions a user can do at all possible states in the app, which can readily expose edge-cases that you might not have even imagined.

The code for both the integration tests with @testing-library/react and the E2E tests with Puppeteer can be found in the the XState test demo repository.

Challenges to Model-Based Testing

Since model-based testing shifts the work from manually writing tests to manually writing models, there is a learning curve. Creating the model necessitates the understanding of finite state machines, and possibly even statecharts. Learning these are greatly beneficial for more reasons than just testing, since finite state machines are one of the core principles of computer science, and statecharts make state machines more flexible and scalable for complex app and software development. The World of Statecharts by Erik Mogensen is a great resource for understanding and learning how statecharts work.

Another issue is that the algorithm for traversing the finite state machine can generate exponentially many test paths. This can be considered a good problem to have, since every one of those paths represents a valid way that a user can potentially interact with an app. However, this can also be computationally expensive and result in semi-redundant tests that your team would rather skip to save testing time. There are also ways to limit these test paths, e.g., using shortest paths instead of simple paths, or by refactoring the model. Excessive tests can be a sign of an overly complex model (or even an overly complex app &#x1f609;).

Write Fewer Tests!

Modeling app behavior is not the easiest thing to do, but there are many benefits of representing your app as a declarative and abstract model, such as a finite state machine or a statechart. Even though the concept of model-based testing is over two decades old, it is still an evolving field. But with the above techniques, you can get started today and take advantage of generating integration and E2E tests instead of manually writing every single one of them.

More resources:

I gave a talk at React Rally 2019 demonstrating model-based testing in React apps:

Slides: Slides: Write Fewer Tests! From Automation to Autogeneration

Happy testing!

The post Model-Based Testing in React with State Machines appeared first on CSS-Tricks.

Firefox blocks third-party tracking cookies and cryptominers

Css Tricks - Wed, 09/04/2019 - 4:18am

This is super interesting stuff from Mozilla: the most recent update of Firefox will now block cryptominers and third-party tracking scripts by default. In the press release they write:

For today’s release, Enhanced Tracking Protection will automatically be turned on by default for all users worldwide as part of the ‘Standard’ setting in the Firefox browser and will block known “third-party tracking cookies” according to the Disconnect list. We first enabled this default feature for new users in June 2019. As part of this journey we rigorously tested, refined, and ultimately landed on a new approach to anti-tracking that is core to delivering on our promise of privacy and security as central aspects of your Firefox experience.

Compare this to A Vox interview with Mat Marquis discussing Google's efforts to make Chrome more private:

“Google is an advertising company, not a group of concerned altruists; there aren’t any charts at stakeholder meetings showing what amount they ‘saved the web’ this past quarter. They’re notorious for overstepping and outright abusing users’ personal data in pursuit of, well, making money as an advertising company,” Mat Marquis, a web developer, told Recode. “Their business model — the thing that keeps all these genuinely brilliant, genuinely well-meaning designers and developers employed — depends on convincing a company that they can make their users look at your ads.”

This is yet another reason why Firefox is my browser of choice and why, as concerned web designers and developers, I suggest others consider joining me in making the switch.

Direct Link to ArticlePermalink

The post Firefox blocks third-party tracking cookies and cryptominers appeared first on CSS-Tricks.

Fast Software

Css Tricks - Tue, 09/03/2019 - 4:22am

There have been some wonderfully interconnected things about fast software lately.

We talk a lot of performance on the web. We can make things a little faster here and there. We see rises in success metrics with rises in performance. I find those type of charts very satisfying. But perhaps even more interesting is to think about the individual people that speed affects. It can be the difference between I love this software and Screw this, I'm out.

Craig Mod, in "Fast Software, the Best Software", totally bailed on Google Maps:

Google Maps has gotten so slow, that I did the unthinkable: I reinstalled Apple Maps on my iPhone. Apple Maps in contrast, today, is downright zippy and responsive. The data still isn’t as good as Google Maps, but this a good example of where slowness pushed me to reinstall an app I had all but written off. I’ll give Apple Maps more of a chance going forward.

And puts a point on it:

But why is slow bad? Fast software is not always good software, but slow software is rarely able to rise to greatness. Fast software gives the user a chance to “meld” with its toolset. That is, not break flow.

Sometimes it's even life and death! Hillel Wayne, in "Performance Matters," says emergency workers in an ambulance don't use the built-in digital "Patient Care Report" (PCR) system, instead opting for paper and pencil, simply because the PCR is a little slow:

The ambulance I shadowed had an ePCR. Nobody used it. I talked to the EMTs about this, and they said nobody they knew used it either. Lack of training? «No, we all got trained.» Crippling bugs? No, it worked fine. Paper was good enough? No, the ePCR was much better than paper PCRs in almost every way. It just had one problem: it was too slow.

It wasn’t even that slow. Something like a quarter-second lag when you opened a dropdown or clicked a button. But it made things so unpleasant that nobody wanted to touch it. Paper was slow and annoying and easy to screw up, but at least it wasn’t that.

"Input delay" is a key concept here. Just the kind of thing that can occur on the web if your JavaScript is doin' stuff and, as they say, "occupying the main thread."

Monica Dinculescu created a Typing delay experiment that simulates this input delay. The "we're done here" setting of 200ms is absolutely well-named. I'd never use software that felt like that. Jay Peters over on The Verge agreed, and anything higher is exponentially worse.

Extra interesting: random delay is worse than consistent large delays, which is probably a more likely scenario on our own sites.

The post Fast Software appeared first on CSS-Tricks.

Recreating Netlify’s Neat-o Sliding Button Effect

Css Tricks - Tue, 09/03/2019 - 4:22am

Have you seen Netlify's press page? It's one of those places where you can snag a download of the company's logo. I was looking for it this morning because I needed the logo to use as a featured image for a post here on CSS-Tricks.

Well, I noticed they have these pretty looking buttons to download the logo. They're small and sharp. They grab attention but aren't in the way.

They're also interactive! Look at the way they expand and reveal the word "Download" on hover.

Nice, right?! I actually noticed that they looked a little off in Safari.

That made me curious about how they're made. So, I recreated them here as a demo while cleaning up some of the spacing stuff:

See the Pen
Netlify Sliding Buttons
by Geoff Graham (@geoffgraham)
on CodePen.

How'd they do it? The recipe really comes down to four ingredients:

  • Using the left property to slide the "Download" label in and out of view
  • Using padding on the button's hover state to create additional room for showing the "Download" label on hover
  • Declaring a 1:1 scale() on the button's hover state so all the content stays contained when things move around.
  • Specifiying a transition on the button's padding, the background-position of the button icon and the transform property to make for a smooth animation between the button's default and hover states.

Here's what that looks like without all the presentation styles:

See the Pen
Style-less Netlify Sliding Buttons
by Geoff Graham (@geoffgraham)
on CodePen.

If you're having a tough time visualizing what's happening, here's an illustration showing how the "Download" label is hidden outside of the button (thanks to overflow: hidden) and where it's pushed into view on hover.

So, by putting negative left values on the icon and the "Download" label, we're pushing them out of view and then resetting those to positive values when the entire button is hovered.

/* Natural State */ .button { background: #f6bc00 url(data:image/svg+xml;base64,...) no-repeat -12px center; overflow: hidden; } .button span:nth-child(1) { position: absolute; left: -70px; } /* Hovered State */ .button:hover { padding-left: 95px; background-position: 5px center; } .button span:nth-child(1) { position: absolute; left: -70px; }

Notice that leaving things in this state would let the button icon slide into view and create enough room for the "Download" label, but the label would actually float off the button on hover.

See the Pen
Style-less Netlify Sliding Buttons
by Geoff Graham (@geoffgraham)
on CodePen.

That's where adding a 1:1 scale on the button helps keep things in tact.

* Hovered State */ .button:hover { padding-left: 95px; background-position: 5px center; transform: scale(1, 1); }

Those padding values are magic numbers. They'll be different for you based on the font, font-size, and other factors, so your mileage may vary.

The last core ingredient is the transition property, which makes everything slide smoothly into place rather than letting them snap. It's provides a much nicer experience.

/* Natural State */ .button { background: #f6bc00 url(data:image/svg+xml;base64,...) no-repeat -12px center; overflow: hidden; transition: padding .2s ease, background-position .2s ease, transform .5s ease; }

Toss in some little flourishes, like rounded corners and such, and you've got a pretty slick button.

See the Pen
Netlify Sliding Buttons
by Geoff Graham (@geoffgraham)
on CodePen.

The post Recreating Netlify’s Neat-o Sliding Button Effect appeared first on CSS-Tricks.

Need to scroll to the top of the page?

Css Tricks - Mon, 09/02/2019 - 4:07am

Perhaps the easiest way to offer that to the user is a link that targets an ID on the <html> element. So like...

<html id="top"> <body> <!-- the entire document --> <a href="#top">Jump to top of page</a> </body> </html>

But we've got a few options here.

If you want it to smooth scroll up to the top, you can do that in CSS if you like:

html { scroll-behavior: smooth; }

Note that placing a property on the HTML element like that is all-encompassing behavior you don't have much control over.

In this case, we aren't linking to a focusable element either, which means that focus won't change. That's probably important, so this would be better:

<html> <body> <a id="top"></a> <!-- the entire document --> <a href="#top">Jump to top of page</a> </body> </html>

It's better because the focus will move to that anchor tag, which is good for people using the keyboard or assistive technology.

These require clicks though. You might need to trigger scrolling within JavaScript, in which case:

window.scrollTo(0, 0);

...is a sure bet to scroll the window (or any other element) back to the top. The scrolling behavior of that is determined by CSS as well, but if you aren't doing that, you can force the smoothness in JavaScript:

window.scroll({ top: 0, left: 0, behavior: 'smooth' });

For a more complete story about smooth scrolling, we have a page for that.

The post Need to scroll to the top of the page? appeared first on CSS-Tricks.

Should a website work without JavaScript?

Css Tricks - Mon, 09/02/2019 - 4:07am

The JS Party podcast just had a fun episode where they debated this classic question by splitting into two groups of two. Each group was assigned a "side" of this debate, and then let loose to debate it. I don't think anybody can listen to a show like this and not be totally flooded with thoughts! Here are mine.

  • This is one of those holy war arguments that has raged on for years. Perhaps that’s because people are seeking an answer that applies to the entire web, and the web is too big to pin this broad of an answer to.
  • The question itself worth a look. Why are we talking about hamstringing our sites in this particular way? Should our websites work without HTML? Should our websites work without databases? Perhaps we focus on JavaScript the most because JavaScript has become the largest bottleneck of web performance (even more so than the network!) and we experience failed JavaScript more so than any other type of web failure (except, perhaps, entire sites not loading) (or icons fonts, jeez).
  • I enjoyed all the stumbling around the terminology of "web apps" and "web sites" (web things!). This is such a weird one. It's so easy to picture the difference in your head: it's like facebook versus a blog! But when you start trying to define it exactly, it gets really murky really quickly and the distinction loses any value, if it had any to start with. Here's more on that.
  • Accessibility is certainly involved in all conversation about the web, but it probably can't be broadly applied here. There is a notion that assistive tech doesn’t run JavaScript — so a site that requires JavaScript to use it is a 100% fail for those users. Best I know, that’s entirely not the case anymore. We can debate to death the role of JavaScript in accessibility problems, but just because a particular site requires JavaScript to run doesn't by itself render the site inaccessible.
  • It’s easy enough to flip off JavaScript, browse around the web, find broken things, and chinflip them for this apparent failure. The failure being that this site, or a feature on the site, could have been architected to work without JavaScript. Rule of least power. This is tricky. It’s easy not to care about a person who has intentionally disabled a part of their web browser and still wants everything to work. I straight up don’t care about that. But the resiliency part is more interesting. If you do build a part of a site to work without JavaScript, it will work both before and after the JavaScript executes, which is pretty great.
  • The concept of building functional content and features without JavaScript and enhancing the experience with JavaScript is called progressive enhancement. I'm both a fan and careful not to insist that everything on earth is always to be built that way (see top bullet point). There are situations where progressive enhancement both increases and reduces technical debt. The only wide brush I'd use here is to say that it's worth doing until the debt is too high.
  • There is an in-between moment with progressive enhancement. If a feature is functional without JavaScript, that means it's likely you are deferring the loading of that JavaScript for the performance benefit. But it does eventually need to be downloaded and executed. What happens during that time? There is a performance and UX cost there. Best case, it's negligible. Worst case, you break the feature during this in-between time.
  • I find it more interesting to debate this kind of thing on a site-by-site and feature-by-feature basis. Application holotypes might be an interesting way to scope it. They often turned to Slack as an example which is a perfect choice. How would you build a 20-author movie review site? How you would architect a social and media-heavy site like Dribbble? How do you build dropdown navigation? What about a one-page brochure site where the client wants parallax? How about an airline app that needs a native mobile app as well? And of course, it makes you think about sites you work on yourself. Does CodePen run on the right set of technologies? Does CSS-Tricks?
  • If a site is "client-side rendered" (CSR), that means JavaScript is doing the data fetching and creating the DOM and all that. If we're talking about websites "working" or not with our without JavaScript, a site that is client-side rendered will 100% fail without JavaScript. It is sort of the opposite of "server-side rendered" (SSR) in which the document comes down as HTML right from the server. SSR is almost certainly faster for a first-loading experience. CSR, typically, is faster to move around the site after loading (think "single page app," or SPA).
  • It's not just SSR vs. CSR — there is a whole spectrum. It's more and more common to see sites try to take advantage of the best of both worlds. For example, Next/Nuxt/Gatsby, or Ember's fastboot.
  • Service workers are JavaScript. Web workers are JavaScript. Some of the fancy resilience and performance features of the web are powered by the same technology that causes the grief we debate about.

The post Should a website work without JavaScript? appeared first on CSS-Tricks.

Styling Links with Real Underlines

Css Tricks - Fri, 08/30/2019 - 4:31am

Before we come to how to style underlines, we should answer the question: should we underline?

In graphic design, underlines are generally seen as unsophisticated. There are nicer ways to draw emphasis, to establish hierarchy, and to demarcate titles.

That’s clear in this advice from Butterick’s "Practical Typography":

If you feel the urge to underline, use bold or italic instead. In special situations, like headings, you can also consider using all caps, small caps, or changing the point size. Not convinced? I invite you to find a book, newspaper, or magazine that underlines text. That look is mostly associated with supermarket tabloids.

But the web is different. Hyperlinks are the defining feature of the internet; and from the internet’s inception, they have been underlined. It’s a universally understood convention. The meaning is crystal clear — an underline means a link.

However, plenty of popular websites have ditched underlines: The New York Times, New York Magazine, The Washington Post, Bloomberg, Amazon, Apple, GitHub, Twitter, Wikipedia. When they removed underlines from their search results page in 2014, Google lead designer Jon Wiley argued that it created a cleaner look. Notably though, the majority of these sites have kept slight variances on the traditional lurid blue color (#0000EE) that’s been the browser default since the beginning of the web. While this provides a visual cue for the majority of users, it may not be enough to pass WCAG accessibility compliance.

Color is not used as the only visual means of conveying information, indicating an action, prompting a response, or distinguishing a visual element.
WCAG 2.1

WCAG do not strictly mandate using underlines for links, but it does recommend them. Color blind users need to be able to discern a link. You could differentiate them in other ways, such as with a bold font-weight. Or you could keep this long-established visual affordance. But if we’re going to use underlines, we want them to look nice. Marcin Wichary, a designer at Medium, described the perfect underline as:

[...] visible, but unobtrusive — allowing people to realize what’s clickable, but without drawing too much attention to itself. It should be positioned at just the right distance from the text, sitting comfortably behind it for when descenders want to occupy the same space.

Achieving this has traditionally required CSS tricks.

The hacks we’ve had

This is one trick all developers will be familiar with: border-bottom. By emulating an underline using border-bottom, we gain control over color and thickness. These pseudo-underlines have one problem: an overly large distance from the text. They are underneath the descenders of the letters. You could potentially solve this issue by using line-height, but that comes with its own issues. A similar technique utilises box-shadow. Marcin Wichary pioneered the most sophisticated technique, using background-image to simulate an underline. They were useful hacks but are thankfully no longer needed.

Styling real underlines

Finally we can demarcate links without sacrificing style thanks to two new CSS properties.

  • text-underline-offset controls the position of the underline.
  • text-decoration-thickness controls the thickness of underlines, as well as overlines, and line-throughs.

According to the WebKit blog:

You can also specify from-font to both of these properties which will pull the relevant metric from the used font file itself.

UX agency Clearleft make bold use of (pseudo) underlines, calling clear attention to links with colorful styling. Here’s one example of a faux underline:

a { text-decoration: none; border-bottom: #EA215A 0.125em solid; }

Notice that this fake underline is clearly below the descender of the letter "y":

Here’s the same paragraph, using DevTools to apply the same styling to a real underline using the new CSS properties:

a { text-decoration-color: #EA215A; text-decoration-thickness: .125em; text-underline-offset: 1.5px; }

You’ll notice I’m using the em unit in my example code. The spec strongly encourages using it rather than pixels so that the thickness scales with the font.

These properties have already shipped in Safari and are coming in Firefox 70.

With the move to Chromium for Microsoft’s Edge browser, we will finally have cross browser support for the text-decoration-style property, which offers the options: solid (the default), double, dotted, dashed, and wavy. When combined, these new properties open up a whole range of possibilities.

Perhaps the biggest upgrade for underlines on the web, however, has come without developers needing to do anything. In the bad old days, descenders were unceremoniously sliced through by underlines, which was far from elegant. Developers used to hack around this shortcoming by applying a text-shadow that matched the background color. text-decoration-skip-ink brought a better way to make space for descenders.

The default value of auto (left) and a value of none (right)

Handily, it’s set as the new default value for underlines; meaning the look of underlines has improved while most web developers remain unaware that this property exists. Should you want an underline to cross over glyphs, you can set this property to none.

The post Styling Links with Real Underlines appeared first on CSS-Tricks.

Working with Attributes on DOM Elements

Css Tricks - Fri, 08/30/2019 - 4:30am

The DOM is just a little weird about some things, and the way you deal with attributes is no exception. There are a number of ways to deal with the attributes on elements. By attributes, I mean things like the id in <div id="cool"></div>. Sometimes you need to set them. Sometimes you need to get them. Sometimes there are fancy helper APIs. Sometimes there isn't.

For this article, I'll assume el is a DOM element in your JavaScript. Let's say you've done something like const el = document.querySelector("#cool"); and matched <div id="cool"> or whatever.

Some attributes are also attributes of the DOM object itself, so iff you need to set an id or title, you can do:

el.id; // "cool" el.title = "my title"; el.title; // "my title";

Others that work like that are lang, align, and all the big events, like onclick.

Then there are attributes that work similarly to that but are nested deeper. The style attribute is like that. If you log el.style you'll see a ton of CSS style declarations. You can get and set them easily:

el.style.color = "red"; module.style.backgroundColor = "black";

You can get computed colors this way too. If you do module.style.color hoping to get the color of an element out of the gate, you probably won't get it. For that, you'd have to do:

let style = window.getComputedStyle(el); style.color; // whatever in CSS won out

But not all attributes are like first-class attributes like that.

el['aria-hidden'] = true; // nope

That "works" in that it sets that as a property, but it doesn't set it in the DOM the proper way. Instead, you'll have to use the generic setter and getter functions that work for all attributes, like:

el.setAttribute("aria-hidden", true); el.getAttribute("aria-hidden");

Some attributes have fancy helpers. The most fancy is classList for class attributes. On an element like:

<div class="module big"></div>

You'd have:

el.classList.value; // "module big" el.classList.length; // 2 el.classList.add("cool"); // adds the class "cool", so "module big cool" el.classList.remove("big"); // removes "big", so "module cool" el.classList.toggle("big"); // adds "big" back, because it was missing (goes back and forth) el.classList.contains("module"); // true

There's even more, and classList itself behaves like an array so you can forEach it and such. That's a pretty strong reason to use classes, as the DOM API around them is so handy.

Another attribute type that has a somewhat fancy help is data-*. Say you've got:

<div data-active="true" data-placement="top right" data-extra-words="hi">test</div>

You've got dataset:

el.dataset; /* { active: "true", "placement", "top right" */ el.dataset.active; // "true" el.dataset.extraWords; // "hi", note the conversion to camelCase el.dataset.active = "false"; // setters work like this

The post Working with Attributes on DOM Elements appeared first on CSS-Tricks.

The Best (GraphQL) API is One You Write

Css Tricks - Thu, 08/29/2019 - 1:32pm

Listen, I am no GraphQL expert but I do enjoy working with it. The way it exposes data to me as a front-end developer is pretty cool. It's like a menu of available data and I can ask for whatever I want. That's a massive improvement over REST and highly empowering for me as a front-end developer who desires to craft components with whatever data I think is best for a UI without having to make slew of calls for data or ask a back-end developer to help make me a new bespoke API for my new needs.

But... who builds that menu of data? Somebody does.

If that somebody is a person or team at your own company because you've built out your own GraphQL API for your own needs, that's great. Now you've got control over what goes in there (and when and how).

But sometimes GraphQL APIs are just handed to you. Perhaps that is how your CMS delivers its data. Still cool and useful, but that control is at the mercy of the CMS. You still have a menu of options, but the menu just is what it is. No substitutes, to continue the metaphor. If the menu doesn't have what you need, you can't go back into the kitchen and add extra sauerkraut to that reuben or have the steak fries come with fried mushrooms.

This came up in a discussion with Simen Skogsrud and Knut Melvær on an episode of ShopTalk. Their product, Sanity, is like cloud storage for JSON data, and a CMS if you need it. A modern product like this, you'd think a GraphQL API would be a no-brainer, and indeed, they have a beta for it.

But instead of GraphQL being the main first-class citizen way of querying for and mutating data, they have their own special language: GROQ. At first glance, I'm like: eeeeeesh, there's a way to shoot yourself in the foot. Invent some special language that people have to learn that's unique to your product instead of the emerging industry standard.

But Simen and Knut made a good point about some of the limitations of GraphQL in the context of a third-party handing you an API: you get what you get. Say a GraphQL API offers a way to retrieve authors. A generic API for that is probably designed something like this:

{ allAuthors { author { name username avatar } } }

But what I actually want is just how many authors we have on the site. Perhaps I wish I could do this:

{ allAuthors { count } }

But that's not in the API I was given. Well, too bad. I guess I'll have to request all the authors and count them myself. I might not control the API.

This means that something like a CMS that offers a GraphQL endpoint needs to make a choice. They are either very strict and you just get-what-you-get. Or, they offer not only a GraphQL API but a way to control and augment what goes into that API.

In Santiy's case, rather than offer the later, they offer GROQ, which is a query language that is powerful enough you can get whatever you want out of the (JSON) data. And rather than making it this proprietary Sanity-only thing, they've open sourced it.

With GROQ, I don't need any permission or alterations to the API to ask how many authors there are. I'd do something like...

{ "totalAuthors": count(*[* in authors]) }

(I actually have no idea if the above code is accurate, and of course, it depends on the JSON it is querying, but it's just conceptual anyway.)

By giving someone a query language that is capable of selecting any possible data in the data store, it has a big benefit:

  • You can query for literally anything
  • You don't need a middle layer of configuration

But it comes at a cost:

  • Complexity of syntax
  • No middle layer means less opportunity for connecting multiple APIs, exposing only certain data based on permissions, etc.

The post The Best (GraphQL) API is One You Write appeared first on CSS-Tricks.

Maskable Icons: Android Adaptive Icons for Your PWA

Css Tricks - Thu, 08/29/2019 - 4:12am

You’ve created a Progressive Web App (PWA), designed an icon to represent it, and now you’re installing it to your Android home screen.

But, if you have a recent Android phone, your icons will show up like this:

What happened? Well, Android Oreo introduced adaptive icons, a new icon format that enforces the same shape for all icons on the home screen. Icons that don’t follow the new format are given a white background.

However, there is a new web feature called maskable icons that is coming soon to Firefox Preview and other web browsers. This new icon format will let your PWAs have their own adaptive icons on Android.

I work at Mozilla and have implemented support for maskable icons in Firefox Preview. I’ll show you how to add them to your own PWAs for Android.

What are maskable and adaptive icons?

Until a few years ago, Android app icons were freeform and could be any shape. This meant that web apps could also reuse the same transparent icon when pinned to the home screen.

However, manufacturers, like Samsung, wanted to make all icons on a device the same shape to keep things consistent. Some manufacturers even wanted different shapes. To deal with the variety of requirements from manufacturers and devices, Android introduced “adaptive icons.” You supply an image with extra space around the edges, and Android will crop it to the correct shape.

But web apps are designed to work on any platform, so they don’t have APIs to create these special Android icons. Instead, icons would get squished into white boxes like this:

Lo and behold, last September a brand new API descended upon us and was added to the W3C spec. Maskable icons allow web developers to specify a full-bleed icon that will be cropped. It’s platform agnostic, so Windows could use them for tiles or iOS could use them for icons.

How to create maskable icons

Since the maskable icon format is designed work with any platform, the size and ratios are different from the size and ratios of Android’s adaptive icons. This means you can’t reuse the same asset.

Maskable icons can be any size, and you can continue to use the same sizes that you’d use for normal transparent icons. But when designing the icon, ensure that important information is within a “safe zone” circle with a radius equal to 40% of the image’s size.

The safe zone

All pixels within this zone are guaranteed to be visible. Pixels outside the zone may be cropped off depending on the icon shape and the platform.

Warning: If you already have an Android app, avoid copying and pasting the icon from your Android app to your web app. The ratios are different, so your icons would look too small.

Adding the icon to your Web App Manifest

Once the icons are created, you can add an entry to your Web App Manifest similar to other icon assets. The Web App Manifest provides information about your web app in a JSON file, and includes an "icons" array.

{ ... "icons": [ ... { "src": "path/to/maskable_icon.png", "sizes": "196x196", "type": "image/png", "purpose": "maskable" ] ... }

Maskable icons use a special new key, "purpose", to indicate that they are meant to be used with icon masks. Icons with transparent backgrounds have a default "purpose" of "any", and icons can be used for multiple purposes by separating each option with a space.

"purpose": "maskable any" Preview your icons

Do you want to see what your own maskable icons will look like? I’ve created a tool, Maskable.app, to help you evaluate how the icon appears in different shapes.

The app lets you preview your icon in various shapes that can be found on Android devices. I hope this tool helps you create unique icons for your Progressive Web Apps.

View App

View Source

Once you’re satisfied with the results, you can start testing your app with Mozilla’s Reference Browser. This special browser is a testing ground for features before they reach Firefox Preview, and you can use it to check out how your PWA looks. Chrome is working on maskable icon support too.

Tools like PWACompat also have support for maskable icons. You can automatically generate icons for iOS and other devices based on your new maskable icons!

Time to build your own icons

If you want to more control over how your PWA icons are displayed on Android, maskable icons is the way to go. With maskable icons you can customize how your icon is displayed from edge-to-edge. Hopefully this article can get you started on creating your first maskable icon.

Icon Credits:

The post Maskable Icons: Android Adaptive Icons for Your PWA appeared first on CSS-Tricks.

A Glassy (and Classy) Text Effect

Css Tricks - Thu, 08/29/2019 - 4:12am

The landing page for Apple Arcade has a cool effect where some "white" text has a sort of translucent effect. You can see some of the color of the background behind it through the text. It's not like knockout text where you see the exact background. In this case, live video is playing underneath. It's like if you were to blur the video and then show that blurry video through the letters.

Well, that's exactly what's happening.

Here's a video so you can see it in action (even after they change that page or you are in a browser that doesn't support the effect):

And hey, if you don't like the effect, that's cool. The rest of this is a technological exploration of how it was done — not a declaration of when and how you should use it.

There are two main properties here that have to work together perfectly to pull this off:

  1. backdrop-filter
  2. clip-path

The backdrop-filter property is easy as heck to use. Set it, and it can filter whatever background is seen through that element.

See the Pen
Basic example of backdrop-filter
by Chris Coyier (@chriscoyier)
on CodePen.

Next we'll place text in that container, but we'll actually hide it. It just needs to be there for accessibility. But we'll end up sort of replacing the text by making a clip path out of the text. Yes indeed! We'll use the SVG <text> inside a <clipPath> element and then use that to clip the entire element that has backdrop-filter on it.

See the Pen
Text with Blurred Background
by Chris Coyier (@chriscoyier)
on CodePen.

For some reason (that I think is a bug), Chrome renders that like this:

It's failing to clip the element properly, even though it supposedly supports both of those properties. I tried using an @supports block, but that's not helpful here. It looks like Apple's site has a .no-backdrop-blur class on the <html> element (Modernizr-style) that is set on Chrome to avoid using the effect at all. I just let my demo break. Maybe someday it'll get that fixed.

It looks right in Safari:

And Firefox doesn't support backdrop-filter at the moment, so the @supports block does its thing and gives you white text instead.

The post A Glassy (and Classy) Text Effect appeared first on CSS-Tricks.

Nested Gradients with background-clip

Css Tricks - Wed, 08/28/2019 - 11:31am

I can't say I use background-clip all that often. I'd wager it's hardly ever used in day-to-day CSS work. But I was reminded of it in a post by Stefan Judis, which coincidentally was itself a learning-response post to a post over here by Ana Tudor.

Here's a quick explanation.

You've probably seen this thing a million times:

The box model visualizer in DevTools.

That's showing you the size and position of an element, as well as how that size is made up: content size, padding, margin, and border.

Those things aren't just theoretical to help with understanding and debugging. Elements actually have a content-box, padding-box, and border-box. Perhaps we encounter that most often when we literally set the box-sizing property. (It's tremendously useful to universally set it to border-box).

Those values are the same values as background-clip uses! Meaning that you can set a background to only cover those specific areas. And because multiple backgrounds is a thing, that means we can have multiple backgrounds with different clipping on each.

Like this:

See the Pen
Multiple background-clip
by Chris Coyier (@chriscoyier)
on CodePen.

But that's boring and there are many ways to pull off that effect, like using borders, outline, and box-shadow or any combination of them.

What is more interesting is the fact that those backgrounds could be gradients, and that's a lot harder to pull off any other way!

See the Pen
Nested Gradients
by Chris Coyier (@chriscoyier)
on CodePen.

The post Nested Gradients with background-clip appeared first on CSS-Tricks.

Creating a Maintainable Icon System with Sass

Css Tricks - Wed, 08/28/2019 - 4:05am

One of my favorite ways of adding icons to a site is by including them as data URL background images to pseudo-elements (e.g. ::after) in my CSS. This technique offers several advantages:

  • They don't require any additional HTTP requests other than the CSS file.
  • Using the background-size property, you can set your pseudo-element to any size you need without worrying that they will overflow the boundaries (or get chopped off).
  • They are ignored by screen readers (at least in my tests using VoiceOver on the Mac) so which is good for decorative-only icons.

But there are some drawbacks to this technique as well:

  • When used as a background-image data URL, you lose the ability to change the SVG's colors using the "fill" or "stroke" CSS properties (same as if you used the filename reference, e.g. url( 'some-icon-file.svg' )). We can use filter() as an alternative, but that might not always be a feasible solution.
  • SVG markup can look big and ugly when used as data URLs, making them difficult to maintain when you need to use the icons in multiple locations and/or have to change them.

We're going to address both of these drawbacks in this article.

The situation

Let's build a site that uses a robust iconography system, and let's say that it has several different button icons which all indicate different actions:

  • A "download" icon for downloadable content
  • An "external link" icon for buttons that take us to another website
  • A "right caret" icon for taking us to the next step in a process

Right off the bat, that gives us three icons. And while that may not seem like much, already I'm getting nervous about how maintainable this is going to be when we scale it out to more icons like social media networks and the like. For the sake of this article, we're going to stop at these three, but you can imagine how in a sophisticated icon system this could get very unwieldy, very quickly.

It's time to go to the code. First, we'll set up a basic button, and then by using a BEM naming convention, we'll assign the proper icon to its corresponding button. (At this point, it's fair to warn you that we'll be writing everything out in Sass, or more specifically, SCSS. And for the sake of argument, assume I'm running Autoprefixer to deal with things like the appearance property.)

.button { appearance: none; background: #d95a2b; border: 0; border-radius: 100em; color: #fff; cursor: pointer; display: inline-block; font-size: 18px; font-weight: 700; line-height: 1; padding: 1em calc( 1.5em + 32px ) 0.9em 1.5em; position: relative; text-align: center; text-transform: uppercase; transition: background-color 200ms ease-in-out; &:hover, &:focus, &:active { background: #8c3c2a; } }

This gives us a simple, attractive, orange button that turns to a darker orange on the hover, focused, and active states. It even gives us a little room for the icons we want to add, so let's add them in now using pseudo-elements:

.button { /* everything from before, plus... */ &::after { background: center / 24px 24px no-repeat; // Shorthand for: background-position, background-size, background-repeat border-radius: 100em; bottom: 0; content: ''; position: absolute; right: 0; top: 0; width: 48px; } &--download { &::after { background-image: url( 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="30.544" height="25.294" viewBox="0 0 30.544 25.294"><g transform="translate(-991.366 -1287.5)"><path d="M1454.5,1298.922l6.881,6.881-6.881,6.881" transform="translate(2312.404 -157.556) rotate(90)" fill="none" stroke="%23fff" stroke-width="3"/><path d="M8853.866,5633.57v9.724h27.544v-9.724" transform="translate(-7861 -4332)" fill="none" stroke="%23fff" stroke-linejoin="round" stroke-width="3"/><line y2="14" transform="translate(1006.5 1287.5)" fill="none" stroke="%23fff" stroke-width="3"/></g></svg>' ); } } &--external { &::after { background-image: url( 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="31.408" height="33.919" viewBox="0 0 31.408 33.919"><g transform="translate(-1008.919 -965.628)"><g transform="translate(1046.174 2398.574) rotate(-135)"><path d="M0,0,7.879,7.879,0,15.759" transform="translate(1025.259 990.17) rotate(90)" fill="none" stroke="%23fff" stroke-width="3"/><line y2="16.032" transform="translate(1017.516 980.5)" fill="none" stroke="%23fff" stroke-width="3"/></g><path d="M10683.643,5322.808v10.24h-20.386v-21.215h7.446" transform="translate(-9652.838 -4335)" fill="none" stroke="%23fff" stroke-width="3"/></g></svg>' ); } } &--caret-right { &::after { background-image: url( 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.129 34.016"><path d="M1454.5,1298.922l15.947,15.947-15.947,15.947" transform="translate(-1453.439 -1297.861)" fill="none" stroke="%23fff" stroke-width="3"/></svg>' ); } } }

Let's pause here. While we're keeping our SCSS as tidy as possible by declaring the properties common to all buttons, and then only specifying the background SVGs on a per-class basis, it's already starting to look a bit unwieldy. That's that second downside to SVGs I mentioned before: having to use big, ugly markup in our CSS code.

Also, note how we're defining our fill and stroke colors inside the SVGs. At some point, browsers decided that the octothorpe ("#") that we all know and love in our hex colors was a security risk, and declared that they would no longer support data URLs that contained them. This leaves us with three options:

  1. Convert our data URLs from markup (like we have here) to base-64 encoded strings, but that makes them even less maintainable than before by completely obfuscating them; or
  2. Use rgba() or hsla() notation, not always intuitive as many developers have been using hex for years; or
  3. Convert our octothorpes to their URL-encoded equivalents, "%23".

We're going to go with option number three, and work around that browser limitation. (I will mention here, however, that this technique will work with rgb(), hsla(), or any other valid color format, even CSS named colors. But please don't use CSS named colors in production code.)

Moving to maps

At this point, we only have three buttons fully declared. But I don't like them just dumped in the code like this. If we needed to use those same icons elsewhere, we'd have to copy and paste the SVG markup, or else we could assign them to variables (either Sass or CSS custom properties), and reuse them that way. But I'm going to go for what's behind door number three, and switch to using one of Sass' greatest features: maps.

If you're not familiar with Sass maps, they are, in essence, the Sass version of an associative array. Instead of a numerically-indexed array of items, we can assign a name (a key, if you will) so that we can retrieve them by something logical and easily remembered. So let's build a Sass map of our three icons:

$icons: ( 'download': '<svg xmlns="http://www.w3.org/2000/svg" width="30.544" height="25.294" viewBox="0 0 30.544 25.294"><g transform="translate(-991.366 -1287.5)"><path d="M1454.5,1298.922l6.881,6.881-6.881,6.881" transform="translate(2312.404 -157.556) rotate(90)" fill="none" stroke="%23fff" stroke-width="3"/><path d="M8853.866,5633.57v9.724h27.544v-9.724" transform="translate(-7861 -4332)" fill="none" stroke="%23fff" stroke-linejoin="round" stroke-width="3"/><line y2="14" transform="translate(1006.5 1287.5)" fill="none" stroke="%23fff" stroke-width="3"/></g></svg>', 'external': '<svg xmlns="http://www.w3.org/2000/svg" width="31.408" height="33.919" viewBox="0 0 31.408 33.919"><g transform="translate(-1008.919 -965.628)"><g transform="translate(1046.174 2398.574) rotate(-135)"><path d="M0,0,7.879,7.879,0,15.759" transform="translate(1025.259 990.17) rotate(90)" fill="none" stroke="%23fff" stroke-width="3"/><line y2="16.032" transform="translate(1017.516 980.5)" fill="none" stroke="%23fff" stroke-width="3"/></g><path d="M10683.643,5322.808v10.24h-20.386v-21.215h7.446" transform="translate(-9652.838 -4335)" fill="none" stroke="%23fff" stroke-width="3"/></g></svg>', 'caret-right': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.129 34.016"><path d="M1454.5,1298.922l15.947,15.947-15.947,15.947" transform="translate(-1453.439 -1297.861)" fill="none" stroke="%23fff" stroke-width="3"/></svg>', );

There are two things to note here: We didn't include the data:image/svg+xml;utf-8, string in any of those icons, only the SVG markup itself. That string is going to be the same every single time we need to use these icons, so why repeat ourselves and run the risk of making a mistake? Let's instead define it as its own string and prepend it to the icon markup when needed:

$data-svg-prefix: 'data:image/svg+xml;utf-8,';

The other thing to note is that we aren't actually making our SVG any prettier; there's no way to do that. What we are doing is pulling all that ugliness out of the code we're working on a day-to-day basis so we don't have to look at all that visual clutter as much. Heck, we could even put it in its own partial that we only have to touch when we need to add more icons. Out of sight, out of mind!

So now, let's use our map. Going back to our button code, we can now replace those icon literals with pulling them from the icon map instead:

&--download { &::after { background-image: url( $data-svg-prefix + map-get( $icons, 'download' ) ); } } &--external { &::after { background-image: url( $data-svg-prefix + map-get( $icons, 'external' ) ); } } &--next { &::after { background-image: url( $data-svg-prefix + map-get( $icons, 'caret-right' ) ); } }

Already, that's looking much better. We've started abstracting out our icons in a way that keeps our code readable and maintainable. And if that were the only challenge, we'd be done. But in the real-world project that inspired this article, we had another wrinkle: different colors.

Our buttons are a solid color which turn to a darker version of that color on their hover state. But what if we want "ghost" buttons instead, that turn into solid colors on hover? In this case, white icons would be invisible for buttons that appear on white backgrounds (and probably look wrong on non-white backgrounds). What we're going to need are two variations of each icon: the white one for the hover state, and one that matches button's border and text color for the non-hover state.

Let's update our button's base CSS to turn it in from a solid button to a ghost button that turns solid on hover. And we'll need to adjust the pseudo-elements for our icons, too, so we can swap them out on hover as well.

.button { appearance: none; background: none; border: 3px solid #d95a2b; border-radius: 100em; color: #d95a2b; cursor: pointer; display: inline-block; font-size: 18px; font-weight: bold; line-height: 1; padding: 1em calc( 1.5em + 32px ) 0.9em 1.5em; position: relative; text-align: center; text-transform: uppercase; transition: 200ms ease-in-out; transition-property: background-color, color; &:hover, &:focus, &:active { background: #d95a2b; color: #fff; } }

Now we need to create our different-colored icons. One possible solution is to add the color variations directly to our map... somehow. We can either add new different-colored icons as additional items in our one-dimensional map, or make our map two-dimensional.

One-Dimensional Map:

$icons: ( 'download-white': '<svg xmlns="http://www.w3.org/2000/svg" width="30.544" height="25.294" viewBox="0 0 30.544 25.294"><g transform="translate(-991.366 -1287.5)"><path d="M1454.5,1298.922l6.881,6.881-6.881,6.881" transform="translate(2312.404 -157.556) rotate(90)" fill="none" stroke="%23fff" stroke-width="3"/><path d="M8853.866,5633.57v9.724h27.544v-9.724" transform="translate(-7861 -4332)" fill="none" stroke="%23fff" stroke-linejoin="round" stroke-width="3"/><line y2="14" transform="translate(1006.5 1287.5)" fill="none" stroke="%23fff" stroke-width="3"/></g></svg>', 'download-orange': '<svg xmlns="http://www.w3.org/2000/svg" width="30.544" height="25.294" viewBox="0 0 30.544 25.294"><g transform="translate(-991.366 -1287.5)"><path d="M1454.5,1298.922l6.881,6.881-6.881,6.881" transform="translate(2312.404 -157.556) rotate(90)" fill="none" stroke="%23d95a2b" stroke-width="3"/><path d="M8853.866,5633.57v9.724h27.544v-9.724" transform="translate(-7861 -4332)" fill="none" stroke="%23d95a2b" stroke-linejoin="round" stroke-width="3"/><line y2="14" transform="translate(1006.5 1287.5)" fill="none" stroke="%23d95a2b" stroke-width="3"/></g></svg>', );

Two-Dimensional Map:

$icons: ( 'download': ( 'white': '<svg xmlns="http://www.w3.org/2000/svg" width="30.544" height="25.294" viewBox="0 0 30.544 25.294"><g transform="translate(-991.366 -1287.5)"><path d="M1454.5,1298.922l6.881,6.881-6.881,6.881" transform="translate(2312.404 -157.556) rotate(90)" fill="none" stroke="%23fff" stroke-width="3"/><path d="M8853.866,5633.57v9.724h27.544v-9.724" transform="translate(-7861 -4332)" fill="none" stroke="%23fff" stroke-linejoin="round" stroke-width="3"/><line y2="14" transform="translate(1006.5 1287.5)" fill="none" stroke="%23fff" stroke-width="3"/></g></svg>', 'orange': '<svg xmlns="http://www.w3.org/2000/svg" width="30.544" height="25.294" viewBox="0 0 30.544 25.294"><g transform="translate(-991.366 -1287.5)"><path d="M1454.5,1298.922l6.881,6.881-6.881,6.881" transform="translate(2312.404 -157.556) rotate(90)" fill="none" stroke="%23d95a2b" stroke-width="3"/><path d="M8853.866,5633.57v9.724h27.544v-9.724" transform="translate(-7861 -4332)" fill="none" stroke="%23d95a2b" stroke-linejoin="round" stroke-width="3"/><line y2="14" transform="translate(1006.5 1287.5)" fill="none" stroke="%23d95a2b" stroke-width="3"/></g></svg>', ), );

Either way, this is problematic. By just adding one additional color, we're going to double our maintenance efforts. Need to change the existing download icon with a different one? We need to manually create each color variation to add it to the map. Need a third color? Now you've just tripled your maintenance costs. I'm not even going to get into the code to retrieve values from a multi-dimensional Sass map because that's not going to serve our ultimate goal here. Instead, we're just going to move on.

Enter string replacement

Aside from maps, the utility of Sass in this article comes from how we can use it to make CSS behave more like a programming language. Sass has built-in functions (like map-get(), which we've already seen), and it allows us to write our own.

Sass also has a bunch of string functions built-in, but inexplicably, a string replacement function isn't one of them. That's too bad, as its usefulness is obvious. But all is not lost.

Hugo Giradel gave us a Sass version of str-replace() here on CSS-Tricks in 2014. We can use that here to create one version of our icons in our Sass map, using a placeholder for our color values. Let's add that function to our own code:

@function str-replace( $string, $search, $replace: '' ) { $index: str-index( $string, $search ); @if $index { @return str-slice( $string, 1, $index - 1 ) + $replace + str-replace( str-slice( $string, $index + str-length( $search ) ), $search, $replace); } @return $string; }

Next, we'll update our original Sass icon map (the one with only the white versions of our icons) to replace the white with a placeholder (%%COLOR%%) that we can swap out with whatever color we call for, on demand.

$icons: ( 'download': '<svg xmlns="http://www.w3.org/2000/svg" width="30.544" height="25.294" viewBox="0 0 30.544 25.294"><g transform="translate(-991.366 -1287.5)"><path d="M1454.5,1298.922l6.881,6.881-6.881,6.881" transform="translate(2312.404 -157.556) rotate(90)" fill="none" stroke="%%COLOR%%" stroke-width="3"/><path d="M8853.866,5633.57v9.724h27.544v-9.724" transform="translate(-7861 -4332)" fill="none" stroke="%%COLOR%%" stroke-linejoin="round" stroke-width="3"/><line y2="14" transform="translate(1006.5 1287.5)" fill="none" stroke="%%COLOR%%" stroke-width="3"/></g></svg>', 'external': '<svg xmlns="http://www.w3.org/2000/svg" width="31.408" height="33.919" viewBox="0 0 31.408 33.919"><g transform="translate(-1008.919 -965.628)"><g transform="translate(1046.174 2398.574) rotate(-135)"><path d="M0,0,7.879,7.879,0,15.759" transform="translate(1025.259 990.17) rotate(90)" fill="none" stroke="%%COLOR%%" stroke-width="3"/><line y2="16.032" transform="translate(1017.516 980.5)" fill="none" stroke="%%COLOR%%" stroke-width="3"/></g><path d="M10683.643,5322.808v10.24h-20.386v-21.215h7.446" transform="translate(-9652.838 -4335)" fill="none" stroke="%%COLOR%%" stroke-width="3"/></g></svg>', 'caret-right': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.129 34.016"><path d="M1454.5,1298.922l15.947,15.947-15.947,15.947" transform="translate(-1453.439 -1297.861)" fill="none" stroke="%%COLOR%%" stroke-width="3"/></svg>', );

But if we were going to try and fetch these icons using just our new str-replace() function and Sass' built-in map-get() function, we'd end with something big and ugly. I'd rather tie these two together with one more function that makes calling the icon we want in the color we want as simple as one function with two parameters (and because I'm particularly lazy, we'll even make the color default to white, so we can omit that parameter if that's the color icon we want).

Because we're getting an icon, it's a "getter" function, and so we'll call it get-icon():

@function get-icon( $icon, $color: #fff ) { $icon: map-get( $icons, $icon ); $placeholder: '%%COLOR%%'; $data-uri: str-replace( url( $data-svg-prefix + $icon ), $placeholder, $color ); @return str-replace( $data-uri, '#', '%23' ); }

Remember where we said that browsers won't render data URLs that have octothorpes in them? Yeah, we're str-replace()ing that too so we don't have to remember to pass along "%23" in our color hex codes.

Side note: I have a Sass function for abstracting colors too, but since that's outside the scope of this article, I'll refer you to my get-color() gist to peruse at your leisure.

The result

Now that we have our get-icon() function, let's put it to use. Going back to our button code, we can replace our map-get() function with our new icon getter:

&--download { &::before { background-image: get-icon( 'download', #d95a2b ); } &::after { background-image: get-icon( 'download', #fff ); // The ", #fff" isn't strictly necessary, because white is already our default } } &--external { &::before { background-image: get-icon( 'external', #d95a2b ); } &::after { background-image: get-icon( 'external' ); } } &--next { &::before { background-image: get-icon( 'arrow-right', #d95a2b ); } &::after { background-image: get-icon( 'arrow-right' ); } }

So much easier, isn't it? Now we can call any icon we've defined, with any color we need. All with simple, clean, logical code.

  • We only ever have to declare an SVG in one place.
  • We have a function that gets that icon in whatever color we give it.
  • Everything is abstracted out to a logical function that does exactly what it looks like it will do: get X icon in Y color.
Making it fool-proof

The one thing we're lacking is error-checking. I'm a huge believer in failing silently... or at the very least, failing in a way that is invisible to the user yet clearly tells the developer what is wrong and how to fix it. (For that reason, I should be using unit tests way more than I do, but that's a topic for another day.)

One way we have already reduced our function's propensity for errors is by setting a default color (in this case, white). So if the developer using get-icon() forgets to add a color, no worries; the icon will be white, and if that's not what the developer wanted, it's obvious and easily fixed.

But wait, what if that second parameter isn't a color? As if, the developer entered a color incorrectly, so that it was no longer being recognized as a color by the Sass processor?

Fortunately we can check for what type of value the $color variable is:

@function get-icon( $icon, $color: #fff ) { @if 'color' != type-of( $color ) { @warn 'The requested color - "' + $color + '" - was not recognized as a Sass color value.'; @return null; } $icon: map-get( $icons, $icon ); $placeholder: '%%COLOR%%'; $data-uri: str-replace( url( $data-svg-prefix + $icon ), $placeholder, $color ); @return str-replace( $data-uri, '#', '%23' ); }

Now if we tried to enter a nonsensical color value:

&--download { &::before { background-image: get-icon( 'download', ce-nest-pas-un-couleur ); } }

...we get output explaining our error:

Line 25 CSS: The requested color - "ce-nest-pas-un-couleur" - was not recognized as a Sass color value.

...and the processing stops.

But what if the developer doesn't declare the icon? Or, more likely, declares an icon that doesn't exist in the Sass map? Serving a default icon doesn't really make sense in this scenario, which is why the icon is a mandatory parameter in the first place. But just to make sure we are calling an icon, and it is valid, we're going to add another check:

@function get-icon( $icon, $color: #fff ) { @if 'color' != type-of( $color ) { @warn 'The requested color - "' + $color + '" - was not recognized as a Sass color value.'; @return null; } @if map-has-key( $icons, $icon ) { $icon: map-get( $icons, $icon ); $placeholder: '%%COLOR%%'; $data-uri: str-replace( url( $data-svg-prefix + $icon ), $placeholder, $color ); @return str-replace( $data-uri, '#', '%23' ); } @warn 'The requested icon - "' + $icon + '" - is not defined in the $icons map.'; @return null; }

We've wrapped the meat of the function inside an @if statement that checks if the map has the key provided. If so (which is the situation we're hoping for), the processed data URL is returned. The function stops right then and there — at the @return — which is why we don't need an @else statement.

But if our icon isn't found, then null is returned, along with a @warning in the console output identifying the problem request, plus the partial filename and line number. Now we know exactly what's wrong, and when and what needs fixing.

So if we were to accidentally enter:

&--download { &::before { background-image: get-icon( 'ce-nest-pas-une-icône', #d95a2b ); } }

...we would see the output in our console, where our Sass process was watching and running:

Line 32 CSS: The requested icon - "ce-nest-pas-une-icône" - is not defined in the $icons map.

As for the button itself, the area where the icon would be will be blank. Not as good as having our desired icon there, but soooo much better than a broken image graphic or some such.

Conclusion

After all of that, let's take a look at our final, processed CSS:

.button { -webkit-appearance: none; -moz-appearance: none; appearance: none; background: none; border: 3px solid #d95a2b; border-radius: 100em; color: #d95a2b; cursor: pointer; display: inline-block; font-size: 18px; font-weight: 700; line-height: 1; padding: 1em calc( 1.5em + 32px ) 0.9em 1.5em; position: relative; text-align: center; text-transform: uppercase; transition: 200ms ease-in-out; transition-property: background-color, color; } .button:hover, .button:active, .button:focus { background: #d95a2b; color: #fff; } .button::before, .button::after { background: center / 24px 24px no-repeat; border-radius: 100em; bottom: 0; content: ''; position: absolute; right: 0; top: 0; width: 48px; } .button::after { opacity: 0; transition: opacity 200ms ease-in-out; } .button:hover::after, .button:focus::after, .button:active::after { opacity: 1; } .button--download::before { background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="30.544" height="25.294" viewBox="0 0 30.544 25.294"><g transform="translate(-991.366 -1287.5)"><path d="M1454.5,1298.922l6.881,6.881-6.881,6.881" transform="translate(2312.404 -157.556) rotate(90)" fill="none" stroke="%23d95a2b" stroke-width="3"/><path d="M8853.866,5633.57v9.724h27.544v-9.724" transform="translate(-7861 -4332)" fill="none" stroke="%23d95a2b" stroke-linejoin="round" stroke-width="3"/><line y2="14" transform="translate(1006.5 1287.5)" fill="none" stroke="%23d95a2b" stroke-width="3"/></g></svg>'); } .button--download::after { background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="30.544" height="25.294" viewBox="0 0 30.544 25.294"><g transform="translate(-991.366 -1287.5)"><path d="M1454.5,1298.922l6.881,6.881-6.881,6.881" transform="translate(2312.404 -157.556) rotate(90)" fill="none" stroke="%23fff" stroke-width="3"/><path d="M8853.866,5633.57v9.724h27.544v-9.724" transform="translate(-7861 -4332)" fill="none" stroke="%23fff" stroke-linejoin="round" stroke-width="3"/><line y2="14" transform="translate(1006.5 1287.5)" fill="none" stroke="%23fff" stroke-width="3"/></g></svg>'); } .button--external::before { background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="31.408" height="33.919" viewBox="0 0 31.408 33.919"><g transform="translate(-1008.919 -965.628)"><g transform="translate(1046.174 2398.574) rotate(-135)"><path d="M0,0,7.879,7.879,0,15.759" transform="translate(1025.259 990.17) rotate(90)" fill="none" stroke="%23d95a2b" stroke-width="3"/><line y2="16.032" transform="translate(1017.516 980.5)" fill="none" stroke="%23d95a2b" stroke-width="3"/></g><path d="M10683.643,5322.808v10.24h-20.386v-21.215h7.446" transform="translate(-9652.838 -4335)" fill="none" stroke="%23d95a2b" stroke-width="3"/></g></svg>'); } .button--external::after { background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="31.408" height="33.919" viewBox="0 0 31.408 33.919"><g transform="translate(-1008.919 -965.628)"><g transform="translate(1046.174 2398.574) rotate(-135)"><path d="M0,0,7.879,7.879,0,15.759" transform="translate(1025.259 990.17) rotate(90)" fill="none" stroke="%23fff" stroke-width="3"/><line y2="16.032" transform="translate(1017.516 980.5)" fill="none" stroke="%23fff" stroke-width="3"/></g><path d="M10683.643,5322.808v10.24h-20.386v-21.215h7.446" transform="translate(-9652.838 -4335)" fill="none" stroke="%23fff" stroke-width="3"/></g></svg>'); } .button--next::before { background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.129 34.016"><path d="M1454.5,1298.922l15.947,15.947-15.947,15.947" transform="translate(-1453.439 -1297.861)" fill="none" stroke="%23d95a2b" stroke-width="3"/></svg>'); } .button--next::after { background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.129 34.016"><path d="M1454.5,1298.922l15.947,15.947-15.947,15.947" transform="translate(-1453.439 -1297.861)" fill="none" stroke="%23fff" stroke-width="3"/></svg>'); }

Yikes, still ugly, but it's ugliness that becomes the browser's problem, not ours.

I've put all this together in CodePen for you to fork and experiment. The long goal for this mini-project is to create a PostCSS plugin to do all of this. This would increase the availability of this technique to everyone regardless of whether they were using a CSS preprocessor or not, or which preprocessor they're using.

"If I have seen further it is by standing on the shoulders of Giants."
– Isaac Newton, 1675

Of course we can't talk about Sass and string replacement and (especially) SVGs without gratefully acknowledging the contributions of the others who've inspired this technique.

The post Creating a Maintainable Icon System with Sass appeared first on CSS-Tricks.

Can you rotate the cursor in CSS?

Css Tricks - Wed, 08/28/2019 - 4:05am

Kinda! There is no simple or standard way to do it, but it's possible. You can change the cursor to different built-in native versions with CSS with the cursor property, but that doesn't help much here. You can also use that property to set a static image as the cursor. But again that doesn't help much because you can't rotate it once it's there.

The trick is to totally hide the cursor with cursor: none; and replace it with your own element.

Here's an example of that:

See the Pen
Move fake mouse with JavaScript
by Chris Coyier (@chriscoyier)
on CodePen.

That's not rotating yet. But now that the cursor is just some element on the page, CSS's transform: rotate(); is fully capable of that job. Some math is required.

I'll leave that to Aaron Iker's really fun demo:

See the Pen
Mouse cursor pointing to cta
by Aaron Iker (@aaroniker)
on CodePen.

Is this an accessibility problem? Something about it makes me think it might be. It's a little extra motion where you aren't expecting it and perhaps a little disorienting when an element you might rely on for a form of stability starts moving on you. It's really only something you'd do for limited-use novelty and while respecting the prefers-reduced-motion. You could also keep the original cursor and do something under it, as Jackson Callaway has done here.

The post Can you rotate the cursor in CSS? appeared first on CSS-Tricks.

Going Buildless

Css Tricks - Tue, 08/27/2019 - 4:44am

I'm in a long distance relationship. That means I’m on a plane to England every few weeks, and every time I'm on that plane, I think about how nice it would be to read some Reddit posts. What I could do is find a Reddit app that lets me cache posts for offline (I’m sure there is one out there), or I could take the opportunity to write something myself and have fun using the latest and greatest technologies and web standards out there!

On top of that, there has been a lot of discussion around what I like to call going buildless, which I think is really fascinating development in which production projects are created without using a build process (like a bundler).

This post is also a homage to a couple of awesome people in the web community who are making some great things possible. I'll be linking to all that stuff as we move along. Do note that this won't be a step-by-step tutorial, but if you want to check out the code, you can find the finished project on GitHub.

Our end result should look something like this:

Let's dive in and install a few dependencies npm i @babel/core babel-loader @babel/preset-env @babel/preset-react webpack webpack-cli react react-dom redux react-redux html-webpack-plugin are-you-tired-yet html-loader webpack-dev-server

I'm kidding.

We're not gonna use any of that.

We're going to try and avoid as much tooling and dependencies as we can to keep the entry barrier low. What we will be using is:

  • LitElement - LitElement is our component model. It's easy to use, lightweight, close to the metal, and leverages web components.
  • @vaadin/router - This is a really small (< 7kb) router that has an awesome developer experience that I cannot recommend enough.
  • @pika/web - This will help us get our modules together for easy development.
  • es-dev-server - This is a simple dev server for modern web development workflows, made by us at open-wc. Although any HTTP server will doc, feel free to bring your own.

That's it! We'll also be using a few browser standards, namely: es modules, web components, import-maps, kv-storage and service-worker.

Let's go ahead and install our dependencies:

npm i -S lit-element @vaadin/router npm i -D @pika/web es-dev-server

We'll also add a postinstall hook to our package.json that's going to run Pika for us:

"scripts": { "start": "es-dev-server", "postinstall": "pika-web" } &#x1f42d; Pika

Pika is a project by Fred K. Schott that aims to bring that nostalgic 2014 simplicity to 2019 web development. Fred is up to all sorts of awesome stuff. For one, he made pika.dev, which lets you easily search for modern JavaScript packages on npm. He also recently gave his talk Reimagining the Registry at DinosaurJS 2019, which I highly recommend you watch.

Pika takes things even one step further. If we run pika-web, it'll install our dependencies as single JavaScript files to a new web_modules/ directory. If your dependency exports an ES "module" entrypoint in its package.json manifest, Pika supports it. If you have any transitive dependencies, Pika will create separate chunks for any shared code among your dependencies.

What this means, is that in our case our output will look something like:

?? web_modules/ ?? lit-element.js ?? @vaadin ?? router.js

Sweet! That's it. We have our dependencies ready to go as single JavaScript module files, and this is going to make things really convenient for us later on in this post, so stay tuned!

&#x1f4e5; Import maps

Alright! Now that we've got our dependencies sorted out, let's get to work. We'll make an index.html that'll look something like this:

<html> <!-- head, etc. --> <body> <reddit-pwa-app></reddit-pwa-app> <script src="./src/reddit-pwa-app.js" type="module"></script> </body> </html>

And reddit-pwa-app.js:

import { LitElement, html } from 'lit-element'; class RedditPwaApp extends LitElement { // ... render() { return html` <h1>Hello world!</h1> `; } } customElements.define('reddit-pwa-app', RedditPwaApp);

We're off to a great start. Let's try and see how this looks in the browser so far, so lets start our server, open the browser and... What's this? An error?

Oh boy.

And we've barely even started. Alright, let's take a look. The problem here is that our module specifiers are bare. They are bare module specifiers. What this means is that there are no paths specified, no file extensions, they're just... pretty bare. Our browser has no idea on what to do with this, so it'll throw an error.

import { LitElement, html } from 'lit-element'; // <-- bare module specifier import { Router } from '@vaadin/router'; // <-- bare module specifier import { foo } from './bar.js'; // <-- not bare! import { html } from 'https://unpkg.com/lit-html'; // <-- not bare!

Naturally, we could use some tools for this, like webpack, or rollup, or a dev server that rewrites the bare module specifiers to something meaningful to browsers, so we can load our imports. But that means we have to bring in a bunch of tooling, dive into configuration, and we're trying to stay minimal here. We just want to write code! In order to solve this, we're going to take a look at import maps.

Import maps is a new proposal that lets you control the behavior of JavaScript imports. Using an import map, we can control what URLs get fetched by JavaScript import statements and import() expressions, and allows this mapping to be reused in non-import contexts. This is great for several reasons:

  • It allows our bare module specifiers to work.
  • It provides a fallback resolution so that import $ from "jquery"; can try to go to a CDN first, but fall back to a local version if the CDN server is down.
  • It enables polyfilling of (or other control over) built-in modules. (More on that later, hang on tight!)
  • Solves the nested dependency problem. (Go read that blog!)

Sounds pretty sweet, no? Import maps are currently available in Chrome 75+ behind a flag, and with that knowledge in mind, let's go to our index.html, and add an import map to our <head>:

<head> <script type="importmap"> { "imports": { "@vaadin/router": "/web_modules/@vaadin/router.js", "lit-element": "/web_modules/lit-element.js" } } </script> </head>

If we go back to our browser, and refresh our page, we'll have no more errors, and we should see our <h1>Hello world!</h1> on our screen.

Import maps is an incredibly interesting new standard, and definitely something you should be keeping your eyes on. If you're interested in experimenting with them, and generate your own import map based on a yarn.lock file, you can try our open-wc import-maps-generate package and play around. Im really excited to see what people will develop in combination with import maps.

&#x1f4e1; Service Worker

Alright, we're going to skip ahead in time a little bit. We've got our dependencies working, we have our router set up, and we've done some API calls to get the data from Reddit and display it on our screen. Going over all of the code is a bit out of scope for this post, but remember that you can find all the code in the GitHub repo if you want to read the implementation details.

Since we're making this app so we can read reddit threads on the airplane it would be great if our application worked offline, and if we could somehow save some posts to read.

Service workers are a kind of JavaScript Worker that runs in the background. You can visualize it as sitting in between the web page, and the network. Whenever your web page makes a request, it goes through the service worker first. This means that we can intercept the request, and do stuff with it! For example, we can let the request go through to the network to get a response, and cache it when it returns so we can use that cached data later when we might be offline. We can also use a service worker to precache our assets. What this means is that we can precache any critical assets our application may need in order to work offline. If we have no network connection, we can simply fall back to the assets we cached, and still have a working (albeit offline) application.

If you're interested in learning more about Progressive Web Apps and service worker, I highly recommend you read The Offline Cookbook, by Jake Archibald, as well as this video tutorial series by Jad Joubran.

Let's go ahead and implement a service worker. In our index.html, we'll add the following snippet:

<script> if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('./sw.js').then(() => { console.log('ServiceWorker registered!'); }, (err) => { console.log('ServiceWorker registration failed: ', err); }); }); } </script>

We'll also add a sw.js file to the root of our project. So we're about to precache the assets of our app, and this is where Pika just made life really easy for us. If you'll take a look at the install handler in the service worker file:

self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHENAME).then((cache) => { return cache.addAll([ '/', './web_modules/lit-element.js', './web_modules/@vaadin/router.js', './src/reddit-pwa-app.js', './src/reddit-pwa-comment.js', './src/reddit-pwa-search.js', './src/reddit-pwa-subreddit.js', './src/reddit-pwa-thread.js', './src/utils.js', ]); }) ); });

You'll find that we're totally in control of our assets, and we have a nice, clean list of files we need in order to work offline.

&#x1f4f4; Going offline

Right. Now that we've cached our assets to work offline, it would be excellent if we could actually save some posts that we can read while offline. There are many ways that lead to Rome, but since we're living on the edge a little bit, we're going to go with: Kv-storage!

&#x1f4e6; Built-in Modules

There are a few things to talk about here. Kv-storage is a built-in module. Built-in modules are very similar to regular JavaScript modules, except they ship with the browser. It's good to note that while built-in modules ship with the browser, they are not exposed on the global scope, and are namespaced with std: (Yes, really.). This has a few advantages: they won't add any overhead to starting up a new JavaScript runtime context (e.g. a new tab, worker, or service worker), and they won't consume any memory or CPU unless they're actually imported, as well as avoid naming collisions with existing code.

Another interesting, if not somewhat controversial, proposal as a built-in module is the std-toast element, and the std-switch element.

&#x1f5c3; Kv-storage

Alright, with that out of the way, lets talk about kv-storage. Kv-storage (or "key value storage") is layered on top of IndexedDB and fairly similar to localStorage, except for only a few major differences.

The motivation for kv-storage is that localStorage is synchronous, which can lead to bad performance and syncing issues. It's also limited exclusively to String key/value pairs. The alternative, IndexedDB, is... hard to use. The reason it's so hard to use is that it predates promises, and this leads to a, well, pretty bad developer experience. Not fun. Kv-storage, however, is a lot of fun, asynchronous, and easy to use! Consider the following example:

import { storage, /* StorageArea */ } from "std:kv-storage"; (async () => { await storage.set("mycat", "Tom"); console.log(await storage.get("mycat")); // Tom })();

Notice how we're importing from std:kv-storage? This import specifier is bare as well, but in this case it's okay because it actually ships with the browser.

Pretty neat. We can perfectly use this for adding a 'save for offline' button, and simply store the JSON data for a Reddit thread, and get it when we need it.

// reddit-pwa-thread.js:52: const savedPosts = new StorageArea("saved-posts"); // ... async saveForOffline() { await savedPosts.set(this.location.params.id, this.thread); // id of the post + thread as json this.isPostSaved = true; }

So now if we click the “save for offline" button, and we go to the DevTools “Application" tab, we can see a kv-storage:saved-posts that holds the JSON data for this post:

And if we go back to our search page, we'll have a list of saved posts with the post we just saved:

&#x1f52e; Polyfilling

Excellent. However, we're about to run into another problem here. Living on the edge is fun, but also dangerous. The problem that we're hitting here is that, at the time of writing, kv-storage is only implemented in Chrome behind a flag. That's not great. Fortunately, there's a polyfill available, and at the same time we get to show off yet another really useful feature of import-maps; polyfilling!

First things first, lets install the kv-storage-polyfill:

npm i -S kv-storage-polyfill

Note that our postinstall hook will run Pika for us again.

Let’s also add the following to our import map in our index.html:

<script type="importmap"> { "imports": { "@vaadin/router": "/web_modules/@vaadin/router.js", "lit-element": "/web_modules/lit-element.js", "/web_modules/kv-storage-polyfill.js": [ "std:kv-storage", "/web_modules/kv-storage-polyfill.js" ] } } </script>

What happens here is that whenever /web_modules/kv-storage-polyfill.js is requested or imported, the browser will first try to see if std:kv-storage is available; however, if that fails, it'll load /web_modules/kv-storage-polyfill.js instead.

So in code, if we import:

import { StorageArea } from '/web_modules/kv-storage-polyfill.js';

This is what will happen:

"/web_modules/kv-storage-polyfill.js": [ // when I'm requested "std:kv-storage", // try me first! "/web_modules/kv-storage-polyfill.js" // or fallback to me ] &#x1f389; Conclusion

And we should now have a simple, functioning PWA with minimal dependencies. There are a few nitpicks to this project that we could complain about, and they'd all likely be fair. For example, we probably could've gone without using Pika, but it does make life really easy for us. You could have made the same argument about adding a webpack configuration, but you'd have missed the point. The point here is to make a fun application, while using some of the latest features, drop some buzzwords, and have a low barrier for entry. As Fred Schott would say: "In 2019, you should use a bundler because you want to, not because you need to."

If you're interested in nitpicking, however, you can read this great discussion about using webpack vs. Pika vs. buildless, and you'll get some great insights from Sean Larkinn of the webpack core team himself, as well as Fred K. Schott, creator of Pika.

I hope you enjoyed this blog post, and I hope you learned something, or discovered some new interesting people to follow. There are lots of exciting developments happening in this space right now, and I hope I got you as excited about them as I am. If you have any questions, comments, feedback, or nitpicks, feel free to reach out to me on twitter at @passle_ or @openwc and don't forget to check out open-wc.org &#x1f609;.

Honorable Mentions

I'd like to give a few shout-outs to some very interesting people that are doing some great stuff, and you may want to keep an eye on.

The post Going Buildless appeared first on CSS-Tricks.

More Flexible Online Stores WooCommerce and Gutenberg Blocks

Css Tricks - Tue, 08/27/2019 - 4:43am

Blocks have become an indispensable component for managing content in WordPress since the Gutenberg editor was officially released earlier this year. Not only does WordPress include some nifty blocks right out of the box, but we're starting to see plugin developers take advantage of them and provide some interesting ones as well.

One of those plugins is WooCommerce. That makes a lot of sense seeing since WooCommerce is part of the Automattic family of products.

WooCommerce Blocks

Blocks are not exactly new to WooCommerce. Well, blocks themselves are a relatively new concept, but WooCommerce was early on the bandwagon to leverage them as a plugin.

For example, the Newest Products block makes it trivial to showcase the most recently added products to a WooCommerce store catalog. In the past, that would have required manually entering and linking up each product. Now, all it takes is dropping a block into place and then on to the next task.

There are a whole slew of blocks like this in WooCommerce. So many, in fact, that making an online store itself has also become somewhat trivial. Sure, sure, e-commerce isn't easy. But at least the part about building a fully functional store that offers a great user experience is less of a worry.

Here are all of the block currently offered by WooCommerce:

  • Newest Products: Show off the most recently added products.
  • Best-Selling Products: Display top-sellers.
  • Product Categories List: Embeds an unordered list of all available product categories that link to category pages.
  • Products by Category: Create a custom list of products by searching product categories.
  • Featured Product: You guessed it! Same as a featured category, but linked to a specific product page.
  • Hand-Picked Products: Create a custom matrix of products from the catalog.
  • On Sale Products: Highlights only products that are currently marked as on sale from the full retail price.
  • Products by Attribute: This lets you put items together by product characteristics. For example, you could make a page just showing large clothing sizes.
  • Top Rated Products: This one puts all of your products with the best reviews.

Phew, that's a lot considering this all comes with a standard WooCommerce installation! Seriously, this opens up a huge range of possibilities for building store pages and driving sales.

New WooCommerce Blocks

If 11 blocks were not enough for you to do some pretty awesome things already, WooCommerce is actively developing more and has recently shipped two new ones.

The first one is which is an offshoot of Featured Product. Let's say you have introduced a collection of products filed under the same category. This block is a perfect way to spotlight that new tag by dropping in a hero component. It takes the description and featured image of the category and places them into an attractive layout that would otherwise require time and development.

The second new block is Products by Tag which is another useful way to group products together on a page or post, just like the Products by Category block. Choose one or multiple tags and those products will display together in a grid.

WooCommerce has really opened the floodgates, giving both developers and store owners new and interesting ways to build an online shopping experience. These are the types of features you might expect to see in some proprietary enterprise-level e-commerce software, but they are freely available in WooCommerce, which is freely available to install into WordPress, which is freely available to anyone who wants a website.

Give WooCommerce a try and see how it can up upgrade your online store.

The post More Flexible Online Stores WooCommerce and Gutenberg Blocks appeared first on CSS-Tricks.

Reusable Popovers to Add a Little Pop

Css Tricks - Mon, 08/26/2019 - 5:12am

A popover is a transient view that shows up on top of a content on the screen when a user clicks on a control button or within a defined area. For example, clicking on an info icon on a specific list item to get the item details. Typically, a popover includes an arrow pointing to the location from which it emerged.

Popovers are great for situations when we want to show a temporary context to get user’s attention when interacting with a specific element on the screen. They provide additional context and instruction for users without having to clutter up a screen. Users can simply close them by clicking the same way they were opened or outside the popover.

We’re going to look at a library called popper.js that allows us to create reusable popover components in the Vue framework. Popovers are the perfect type of component for a component-based system like Vue because they can be contained, encapsulated components that are maintained on their own, but used anywhere throughout an app.

Let’s dig in and get started.

But first: What’s the difference between a popover and tooltip?

Was the name "popover" throwing you for a loop? The truth is that popovers are a lot like tooltips, which are another common UI pattern for displaying additional context in a contained element. There are differences between them, though, so let’s briefly spell them out so we have a solid handle on what we’re building.

Tooltips Popovers Tooltips are meant to be exactly that, a hint or tip on what a tool or other interaction does. They are meant to clarify or help you use the content that they hover over, not add additional content. Popovers, on the other hand, can be much more verbose, they can include a header and many lines of text in the body. Tooltips are typically only visible on hover, for that reason if you need to be able to read the content while interacting with other parts of the page then a tooltip will not work. Popovers are typically dismissible, whether by click on other parts of the page or second clicking the popover target (depending on implementation), for that reason you can set up a popover to allow you to interact with other elements on the page while still being able to read it's content.

Popovers are most appropriate on larger screens and we’re most likely to encounter them in use cases such as:

Looking at those use cases, we can glean some requirements that make a good popover:

  1. Reusability: A popover should allow to pass a custom content to the popover.
  2. Dismissibility: A popover should be dismissible by clicking outside of the popover and escape button.
  3. Positioning: A popover should reposition itself when the screen edge is reached.
  4. Interaction: A popover should allow to interact with the content in the popover.

I created an example to refer to as we go through the process of creating a component.

View Demo

OK, now that we’ve got a baseline understanding of popovers and what we’re building, let’s get into the step-by-step details for creating them using popper.js.

Step 1: Create the BasePopover component

Let’s start by creating a component that will be responsible for initializing and positioning the popover. We’ll call this component BasePopover.vue and, in the component template, we’ll render two elements:

  • Popover content: This is the element that will be responsible for rendering the content within the popover. For now we use a slot that will allow us to pass the content from the parent component responsible for rendering our popover (Requirement #1: Reusability).
  • Popover overlay: This is the element responsible for covering the content under the popover and preventing user from interacting with the elements outside the popover. It also allows us to close the popover when clicked (Requirement #2: Dismissibility).
// BasePopover.vue <template> <div> <div ref="basePopoverContent" class="base-popover" > <slot /> </div> <div ref="basePopoverOverlay" class="base-popover__overlay" /> </div> </template>

In the script section of the component:

  • we import popper.js (the library that takes care of the popover positioning), then
  • we receive the popoverOptions props, and finally
  • we set initial popperInstance to null (because initially we do not have any popover).

Let’s describe what the popoverOptions object contains:

  • popoverReference: This is an object in relation to which the popover will be positioned (usually element that triggers the popover).
  • placement: This is a popper.js placement option that specifies the where the popover is displayed in relation to the popover reference element (the thing it is attached to)
  • offset: This is a popper.js offset modifier that allows us to adjust popover position by passing x- and y-coordinates.
import Popper from "popper.js" export default { name: "BasePopover", props: { popoverOptions: { type: Object, required: true } }, data() { return { popperInstance: null } } }

Why do we need that? The popper.js library allows us to position the element in relation to another element with ease. It also does the magic when the popover gets to the edge of the screen an reposition it to be always in user’s viewport (Requirement #3: Positioning)

Step 2: Initialize popper.js

Now that we have a BasePopover component skeleton, we will add few methods that will be responsible for positioning and showing the popover.

In the initPopper method, we will start by creating a modifiers object that will be used to create a Popper instance. We set the options received from the parent component (placement and offset) to the corresponding fields in the modifiers object. All those fields are optional, which is why we first need to check for their existence.

Then, we initialize a new Popper instance by passing:

  • the popoverReference node (the element to which the popover is pointing: popoverReference ref)
  • the popper content node (the element containing the popover content: basePopoverContent ref)
  • the options object

We also set the preventOverflow option to prevent the popover from being positioned outside of the viewport. After initialization we set the popper instance to our popperInstance data property to have access to methods and properties provided by popper.js in the future.

methods: { ... initPopper() { const modifiers = {} const { popoverReference, offset, placement } = this.popoverOptions if (offset) { modifiers.offset = { offset } } if (placement) { modifiers.placement = placement } this.popperInstance = new Popper( popoverReference, this.$refs.basePopoverContent, { placement, modifiers: { ...modifiers, preventOverflow: { boundariesElement: "viewport" } } } ) } ... }

Now that we have our initPopper method ready, we need a place to invoke it. The best place for that is in the mounted lifecycle hook.

mounted() { this.initPopper() this.updateOverlayPosition() }

As you can see, we are calling one more method in the mounted hook: the updateOverlayPosition method. This method is a safeguard used to reposition our overlay in case we have any other elements on the page that have absolute positioning (e.g. NavBar, SideBar). The method is making sure the overlay is always covering the full screen and prevent user from interacting with any element except the popover and overlay itself.

methods: { ... updateOverlayPosition() { const overlayElement = this.$refs.basePopoverOverlay; const overlayPosition = overlayElement.getBoundingClientRect(); overlayElement.style.transform = <code>translate(-${overlayPosition.x}px, -${ overlayPosition.y }px)`; } ... } Step 3: Destroy Popper

We have our popper initialized but now we need a way to remove and dispose it when it gets closed. There’s no need to have it in the DOM at that point.

We want to close the popover when we click anywhere outside of it. We can do that by adding a click listener to the overlay because we made sure that the overlay is always covering the whole screen under our popover

<template> ... <div ref="basePopoverOverlay" class="base-popover__overlay" @click.stop="destroyPopover" /> ... </template>

Let’s create a method responsible for destroying the popover. In that method we first check if the popperInstance actually exist and if it does we call popper destroy method that makes sure the popper instance is destroyed. After that we clean our popperInstance data property by setting it to null and emit a closePopover event that will be handled in the component responsible for rendering the popover.

methods: { ... destroyPopover() { if (this.popperInstance) { this.popperInstance.destroy(); this.popperInstance = null; this.$emit("closePopover"); } } ... } Step 4: Render BasePopover component

OK, we have our popover ready to be rendered. We do that in our parent component, which will be responsible for managing the visibility of the popover and passing the content to it.

In the template, we need to have an element responsible for triggering our popover (popoverReference) and the BasePopover component. The BasePopover component receives a popoverOptions property that will tell the component how we want to display it and isPopoverVisible property bound to v-if directive that will be responsible for showing and hiding the popover.

<template> <div> <img ref="popoverReference" width="25%" src="./assets/logo.png" > <BasePopover v-if="isPopoverVisible" :popover-options="popoverOptions" > <div class="custom-content"> <img width="25%" src="./assets/logo.png"> Vue is Awesome! </div> </BasePopover> </div> </template>

In the script section of the component, we import our BasePopover component, set the isPopoverVisible flag initially to false and popoverOptions object that will be used to configure popover on init.

data() { return { isPopoverVisible: false, popoverOptions: { popoverReference: null, placement: "top", offset: "0,0" } }; }

We set popoverReference property to null initially because the element that will be the popover trigger does not exist when our parent component is created. We get that fixed in the mounted lifecycle hook when the component (and the popover reference) gets rendered.

mounted() { this.popoverOptions.popoverReference = this.$refs.popoverReference; }

Now let’s create two methods, openPopover and closePopover that will be responsible for showing and hiding our popover by setting proper value on the isPopoverVisible property.

methods: { closePopover() { this.isPopoverVisible = false; }, openPopover() { this.isPopoverVisible = true; } }

The last thing we need to do in this step is to attach those methods to appropriate elements in our template. We attach the openPopover method to click event on our trigger element and closePopover method to closePopover event emitted from the BasePopover component when the popover gets destroyed by clicking on the popover overlay.

<template> <div> <img ... @click="openPopover" > <BasePopover ... @closePopover="closePopover" > ... </BasePopover> </div> </template>

Having this in place, we have our popover showing up when we click on the trigger element and disappearing when we click outside of the popover.

Step 5: Create BasePopoverContent component

It does not look like a popover though. Sure, it renders content passed to the BasePopover component, but it does so without the usual popover wrapper and an arrow pointing to the trigger element. We could have included the wrapper component in the BasePopover component, but this would made it less reusable and couple the popover to a specific template implementation. Our solution allows us to render any template in the popover. We also want to make sure that the component is responsible only for positioning and showing the content.

To make it look like a popover, let’s create a BasePopoverContent component. We need to render two elements in the template:

  • an arrow element having a popper.js x-arrow selector needed for the popper.js to properly position the arrow
  • content wrapper that expose a slot element in which our content will be rendered
<template> <div class="base-popover-content"> <div class="base-popover-content__arrow" x-arrow/> <div class="base-popover-content__body"> <slot/> </div> </div> </template>

Now let’s use our wrapper component in the parent component where we use BasePopover

<template> <div> <img ref="popoverReference" width="25%" src="./assets/logo.png" @click="openPopover" > <BasePopover v-if="isPopoverVisible" :popover-options="popoverOptions" @closePopover="closePopover" > <BasePopoverContent> <div class="custom-content"> <img width="25%" src="./assets/logo.png"> Vue is Awesome! </div> </BasePopoverContent> </BasePopover> </div> </template>

And, there we go!

You can see the popover animating in and out in the example above. We’ve left animation out of this article for the sake of brevity, but you can check out other popper.js examples for inspiration.

You can see the animation code and working example here.

Let’s look at our requirements and see if we didn’t missed anything:

Pass? Requirement Explanation Pass Reusability We used a slot in the BasePopover component that decouples the popover implementation from the content template. This allows us to pass any content to the component. Fail Dismissibility We made it possible to close the popover when clicking outside of it. We still need to make sure we can dismiss the popover by pressing the ESC on the keyboard. Pass Positioning That’s where popper.js solved an issue for us. It not only gave us positioning superpowers, but also takes care of repositioning the popover when it reaches the edge of the viewport. Fail Interaction We have a popover popping in and out, but we do not have any interactions with the popover content yet. As for now, it looks more like a tooltip than popover and could actually be used as a tooltip when it comes to showing and hiding the element. Tooltips are usually shown on hover, so that’s the only change we’d have to make.

Darn, we failed interaction requirement. Adding the interaction is a matter of creating a component (or components) that will be placed in the BasePopoverContent slot. In the example, I created a very simple component with a header and text showing a few Vue style guide rules. By clicking on the buttons, we can interact with the popover content and change the rules, when you get to the last rule the button changes its purpose and serve as a close button for the popover. It’s a lot like the new user welcome screens we see in apps.

We also need to fully meet the dismissibility requirement. We want to hit ESC on the keyboard to close the popover in addition to clicking anywhere outside it. For kicks, we’ll also add an event that proceeds to the next Vue style guide rule when pressing Enter.

We can handle that in the component responsible for rendering the popover content using Vue event key modifiers. To make the events work we need to make sure that the popover is focused when mounted. To do that we add a tabindex attribute to the popover content and a ref that will allow us to access the element in the mounted hook and call focus method on it.

// VueTourPopoverContent.vue <template> <div class="vue-tour-popover-content" ref="vueTourPopoverContent" tabindex="-1" @keydown.enter="proceedToNextStep" @keydown.esc="closePopover" > ... </template ... <script> export default { ... mounted() { this.$refs.vueTourPopoverContent.focus(); } ... } </script> Wrapping up

And there we have it: a fully functional popover component we can use anywhere in our app. Here are a few things we learned along the way:

  • Use a popover to expose a small amount of information or functionality. Remember that the content will disappear when user is finished with it.
  • Consider using popovers instead of temporary views like sidebars. Popovers leave more space for content and are only temporary.
  • Enable a closure behavior that makes sense based on the popover’s function. A popover should be visible only when needed. If it allows user to make a choice, close the popover as soon as the user makes a decision.
  • Position popovers onscreen with care. A popover’s arrow should always point directly to the element that triggered it and should never cover the trigger element.
  • Display one popover on screen at a time. More than one steals attention.
  • Take care of the popover size. Prevent making it too big but bear in mind that proper use of padding can make things look nice and clean.

If you don't want to dig too deep into the code and you just need the component as it is, you can try out the npm package version of the component.

Hopefully you will find this component useful in your project!

The post Reusable Popovers to Add a Little Pop appeared first on CSS-Tricks.

Jeremy Keith – Building the Web

Css Tricks - Fri, 08/23/2019 - 5:36am

I really enjoyed this interview with Jeremy Keith on the state of the web, how things have changed in recent years and why he’s a mix of optimistic and nervous for the future.

One thing that caught my attention during the interview more than anything was where Jeremy started discussing how folks think that websites are pretty crummy in general. This reminded me that I cannot count the number of times when someone has said to me “ah, I can’t view this website on my phone.”

We have websites that aren’t responsive! We have websites that litter the UI with advertisements and modals! And we have websites that are slow as all heck just when we need them the most!

Of course folks are going to start complaining about the web and working around them if they find that this is the case. I’ll even catch myself sending an email to myself when I know that the mobile experience is going to be crummy. Or I’ll Instapaper something because the design of the website is particularly difficult to read. Remember, Reader Mode is the button to beat.

My quick thought on this is that we shouldn’t become sour and pessimistic. We should roll up our sleeves and get to work because clearly there’s much left to do.

Direct Link to ArticlePermalink

The post Jeremy Keith – Building the Web appeared first on CSS-Tricks.

Syndicate content
©2003 - Present Akamai Design & Development.