Tag: Don’t

Why Don’t Developers Take Accessibility Seriously?

You know that joke, “Two front-end developers walk into a bar and find they have nothing in common”? It’s funny, yet frustrating, because it’s true.

This article will present three different perspectives on accessibility in web design and development. Three perspectives that could help us bridge the great divide between users and designers/developers. It might help us find the common ground to building a better web and a better future.

The corner of a white and blue building in focus, with white on the left and blue on the right representing the divide between developers when it comes to accessibility practices.
Photo by Alexander Naglestad on Unsplash

Act 1

“I just don’t know how developers don’t think about accessibility.”

Someone once said that to me. Let’s stop and think about it for a minute. Maybe there’s a perspective to be had.

Think about how many things you have to know as a developer to successfully build a website. In any given day, for any given job position in web development, there are the other details of web development that come up. Meaning, it’s more than “just” knowing HTML, CSS, ARIA, and JavaScript. Developers will also learn other things over the course of their careers, based on what they need to do.

This could be package management, workspaces, code generators, collaboration tools, asset loading, asset management, CDN optimizations, bundle optimizations, unit tests, integration tests, visual regression tests, browser integration tests, code reviews, linting, formatting, communication through examples, changelogs, documentation, semantic versioning, security, app deployment, package releases, rollbacks, incremental improvements, incremental testing, continuous deployments, merge management, user experience, user interaction design, typography scales, aspect ratios for responsive design, data management, and… well, the list could go on, but you get the idea.

As a developer, I consider myself to be pretty gosh darn smart for knowing how to do most these things! Stop and consider this: if you think about how many people are in the world, and compare that to how many people in the world can build websites, it’s proportionally a very small percentage. That’s kind of… cool. Incredible, even. On top of that, think about the last time you shipped code and how good that felt. “I figured out a hard thing and made it work! Ahhhhh! I feel amazing!”

That kind of emotional high is pretty great, isn’t it? It makes me smile just to think about it.

Now, imagine that an accessibility subject-matter expert comes along and essentially tells you that not only are you not particularly smart, but you have been doing things wrong for a long time.

Ouch. Suddenly you don’t feel very good. Wrong? Me?? What??? Your adrenaline can even kick in and you start to feel defensive. Time to stick up for yourself… right? Time to dig those heels.

The cognitive dissonance can even be really overwhelming. It feels bad to find out that not only are you not good at the thing you thought you were really good at doing, but you’ve also been saying, “Screw you, who cares about you anyway,” to a whole bunch of people who can’t use the websites you’ve helped build because you (accidentally or otherwise) ignored that they even existed, that you ignored users who needed something more than the cleverness you were delivering for all these years. Ow.

All things considered, it is quite understandable to me that a developer would want to put their fingers in their ears and pretend that none of this has happened at all, that they are still very clever and awesome. That the one “expert” telling you that you did it wrong is just one person. And one person is easy to ignore.

end scene.

Act 2

“I feel like I don’t matter at all.”

This is a common refrain I hear from people who need assistive technology to use websites, but often find them unusable for any number of reasons. Maybe they can’t read the text because the website’s design has ignored color contrast. Maybe there are nested interactive elements, so they can’t even log in to do things like pay a utility bill or buy essential items on their own. Maybe their favorite singer has finally set up an online shop but the user with assistive technology cannot even navigate the site because, while it might look interactive from a sighted-user’s perspective, all the buttons are divs and are not interactive with a keyboard… at all.

This frustration can boil over and spill out; the brunt of this frustration is often borne by the folks who are trying to deliver more inclusive products. The result is a negative feedback cycle; some tech folks opt out of listening because “it’s rude” (and completely missing the irony of that statement). Other tech folks struggle with the emotional weight that so often accompanies working in accessibility-focused design and development.

The thing is, these users have been ignored for so long that it can feel like they are screaming into a void. Isn’t anyone listening? Doesn’t anyone care? It seems like the only way to even be acknowledged is to demand the treatment that the law affords them! Even then, they often feel ignored and forgotten. Are lawsuits the only recourse?

It increasingly seems that being loud and militant is the only way to be heard, and even then it might be a long time before anything happens.

end scene.

Act 3

“I know it doesn’t pass color contrast, but I feel like it’s just so restrictive on my creativity as a designer. I don’t like the way this looks, at all.”

I’ve heard this a lot across the span of my career. To some, inclusive design is not the necessary guardrail to ensure that our websites can be used by all, but rather a dampener on their creative freedom.

If you are a designer who thinks this way, please consider this: you’re not designing for yourself. This is not like physical art; while your visual design can be artistic, it’s still on the web. It’s still for the web. Web designers have a higher challenge—their artistic vision needs to be usable by everyone. Challenge yourself to move the conversation into a different space: you just haven’t found the right design yet. It’s a false choice to think that a design can either be beautiful or accessible; don’t fall into that trap.

end scene.

Let’s re-frame the conversation

These are just three of the perspectives we could consider when it comes to digital accessibility.

We could talk about the project manager that “just wants to ship features” and says that “we can come back to accessibility later.” We could talk about the developer who jokes that “they wouldn’t use the internet if they were blind anyway,” or the one that says they will only pay attention to accessibility “once browsers make them do it.”

We could, but we don’t really need to. We know how these these conversations go, because many of us have lived these experiences. The project never gets retrofitted. The company pays once to develop the product, then pays for an accessibility audit, then pays for the re-write after the audit shows that a retrofit is going to be more costly than building something new. We know the developer who insists they should only be forced to do something if the browser otherwise disallows it, and that they are unlikely to be convinced that the inclusive architecture of their code is not only beneficial, but necessary.

So what should we be talking about, then?

We need to acknowledge that designers and developers need to be learning about accessibility much sooner in their careers. I think of it with this analogy: Imagine you’ve learned a foreign language, but you only learned that language’s slang. Your words are technically correct, but there are a lot of native speakers of that language who will never be able to understand you. JavaScript-first web developers are often technically correct from a JavaScript perspective, but they also frequently create solutions that leave out a whole lotta people in the end.

How do we correct for this? I’m going to be resolute here, as we all must be. We need to make sure that any documentation we produce includes accessible code samples. Designs must contain accessible annotations. Our conference talks must include accessibility. The cool fun toys we make to make our lives easier? They must be accessible, and there must be no excuse for anything less This becomes our new minimum-viable product for anything related to the web.

But what about the code that already exists? What about the thousands of articles already written, talks already given, libraries already produced? How do we get past that? Even as I write this article for CSS-Tricks, I think about all of the articles I’ve read and the disappointment I’ve felt when I knew the end result was inaccessible. Or the really fun code-generating tools that don’t produce accessible code. Or the popular CSS frameworks that fail to consider tab order or color contrast. Do I want all of those people to feel bad, or be punished somehow?

Nope. Not even remotely. Nothing good comes from that kind of thinking. The good comes from the places we already know—compassion and curiosity.

We approach this with compassion and curiosity, because these are sustainable ways to improve. We will never improve if we wallow in the guilt of past actions, berating ourselves or others for ignoring accessibility for all these years. Frankly, we wouldn’t get anything done if we had to somehow pay for past ignorant actions; because yes, we did ignore it. In many ways, we still do ignore it.

Real examples: the Google Developer training teaches a lot of things, but it doesn’t teach anything more than the super basic parts of accessibility. JavaScript frameworks get so caught up in the cleverness and complexity of JavaScript that they completely forget that HTML already exists. Even then, accessibility can still take a back seat. Ember existed for about eight years before adding an accessibility-focused community group (even if they have made a lot of progress since then). React had to have a completely different router solution created. Vue hasn’t even begun to publicly address accessibility in the core framework (although there are community efforts). Accessibility engineers have been begging for inert to be implemented in browsers natively, but it often is underfunded and de-prioritized.

But we are technologists and artists, so our curiosity wins when we read interesting articles about how the accessibility object model and how our code can be translated by operating systems and fed into assistive technology. That’s pretty cool. After all, writing machine code so it can talk to another machine is probably more of what we imagined we’d be doing, right?

The thing is, we can only start to be compassionate toward other people once we are able to be compassionate toward ourselves. Sure, we messed up—but we don’t have to stay ignorant. Think about that time you debugged your code for hours and hours and it ended up being a typo or a missing semicolon. Do you still beat yourself up over that? No, you developed compassion through logical thinking. Think about the junior developer that started to be discouraged, and how you motivated them to keep trying and that we all have good days and bad. That’s compassion.

Here’s the cool part: not only do we have the technology, we are literally the ones that can fix it. We can get up and try to do better tomorrow. We can make some time to read about accessibility, and keep reading about it every day until we know it just as well as we do other things. It will be hard at first, just like the first time we tried… writing tests. Writing CSS. Working with that one API that is forever burned in our memory. But with repetition and practice, we got better. It got easier.

Logically, we know we can learn hard things; we have already learned hard things, time and time again. This is the life and the career we signed up for. This is what gets us out of bed every morning. We love challenges and we love figuring them out. We are totally here for this.

What can we do? Here are some action steps.

Perhaps I have lost some readers by this point. But, if you’ve gotten this far, maybe you’re asking, “Melanie, you’ve convinced me, but what can I do right now?” I will give you two lists to empower you to take action by giving you a place to start.

Compassionately improve yourself:

  1. Start following some folks with disabilities who are on social media with the goal of learning from their experiences. Listen to what they have to say. Don’t argue with them. Don’t tone police them. Listen to what they are trying to tell you. Maybe it won’t always come out in the way you’d prefer, but listen anyway.
  2. Retro-fit your knowledge. Try to start writing your next component with HTML first, then add functionality with JavaScript. Learn what you get for free from HTML and the browser. Take some courses that are focused on accessibility for engineers. Invest in your own improvement for the sake of improving your craft.
  3. Turn on a screen reader. Learn how it works. Figure out the settings—how do you turn on a text-only version? How do you change the voice? How do you make it stop talking, or make it talk faster? How do you browse by headings? How do you get a list of links? What are the keyboard shortcuts?

Bonus Challenge: Try your hand at building some accessibility-related tooling. Check out A11y Automation Tracker, an open source project that intends to track what automation could exist, but just hasn’t been created yet.

Incrementally improve your code

There are critical blockers that stop people from using your website. Don’t stop and feel bad about them; propel yourself into action and make your code even better than it was before.

Here are some of the worst ones:

  1. Nested interactive elements. Like putting a button inside of a link. Or another button inside of a button.
  2. Missing labels on input fields (or non-associated labels)
  3. Keyboard traps stop your users in their tracks. Learn what they are and how to avoid them.
  4. Are the images on your site important for users? Do they have the alt attribute with a meaningful value?
  5. Are there empty links on your site? Did you use a link when you should have used a button?

Suggestion: Read through the checklist on The A11y Project. It’s by no means exhaustive, but it will get you started.

And you know what? A good place to start is exactly where you are. A good time to start? Today.

Featured header photo by Scott Rodgerson on Unsplash

Why Don’t Developers Take Accessibility Seriously? originally published on CSS-Tricks. You should get the newsletter and become a supporter.


, , , ,

Don’t Fight the Cascade, Control It!

If you’re disciplined and make use of the inheritance that the CSS cascade provides, you’ll end up writing less CSS. But because our styles often comes from all kinds of sources — and can be a pain to structure and maintain—the cascade can be a source of frustration, and the reason we end up with more CSS than necessary.

Some years ago, Harry Roberts came up with ITCSS and it’s a clever way of structuring CSS.

Mixed with BEM, ITCSS has become a popular way that people write and organize CSS.

However, even with ITCSS and BEM, there are still times where we still struggle with the cascade. For example, I’m sure you’ve had to @import external CSS components at a specific location to prevent breaking things, or reach for the dreaded !important at some point in time.

Recently, some new tools were added to our CSS toolbox, and they allow us to finally control the cascade. Let’s look at them.

O cascade, :where art thou?

Using the :where pseudo-selector allows us to remove specificity to “just after the user-agent default styles,” no matter where or when the CSS is loaded into the document. That means the specificity of the whole thing is literally zero — totally wiped out. This is handy for generic components, which we’ll look into in a moment.

First, imagine some generic <table> styles, using :where:

:where(table) {   background-color: tan; }

Now, if you add some other table styles before the :where selector, like this:

table {   background-color: hotpink; }  :where(table) {   background-color: tan; }

…the table background becomes hotpink, even though the table selector is specified before the :where selector in the cascade. That’s the beauty of :where, and why it’s already being used for CSS resets.

:where has a sibling, which has almost the exact opposite effect: the :is selector.

The specificity of the :is() pseudo-class is replaced by the specificity of its most specific argument. Thus, a selector written with :is() does not necessarily have equivalent specificity to the equivalent selector written without :is(). Selectors Level 4 specification

Expanding on our previous example:

:is(table) {   --tbl-bgc: orange; } table {   --tbl-bgc: tan; } :where(table) {   --tbl-bgc: hotpink;   background-color: var(--tbl-bgc); }

The <table class="c-tbl"> background color will be tan because the specificity of :is is less specific than table.

However, if we were to change it to this:

:is(table, .c-tbl) {   --tbl-bgc: orange; }

…the background color will be orange, since :is has the weight of it’s heaviest selector, which is .c-tbl.

Example: A configurable table component

Now, let’s see how we can use :where in our components. We’ll be building a table component, starting with the HTML:

Let’s wrap .c-tbl in a :where-selector and, just for fun, add rounded corners to the table. That means we need border-collapse: separate, as we can’t use border-radius on table cells when the table is using border-collapse: collapse:

:where(.c-tbl) {   border-collapse: separate;   border-spacing: 0;   table-layout: auto;   width: 99.9%; }

The cells use different styling for the <thead> and <tbody>-cells:

:where(.c-tbl thead th) {   background-color: hsl(200, 60%, 40%);   border-style: solid;   border-block-start-width: 0;   border-inline-end-width: 1px;   border-block-end-width: 0;   border-inline-start-width: 0;   color: hsl(200, 60%, 99%);   padding-block: 1.25ch;   padding-inline: 2ch;   text-transform: uppercase; } :where(.c-tbl tbody td) {   background-color: #FFF;   border-color: hsl(200, 60%, 80%);   border-style: solid;   border-block-start-width: 0;   border-inline-end-width: 1px;   border-block-end-width: 1px;   border-inline-start-width: 0;   padding-block: 1.25ch;   padding-inline: 2ch; }

And, because of our rounded corners and the missing border-collapse: collapse, we need to add some extra styles, specifically for the table borders and a hover state on the cells:

:where(.c-tbl tr td:first-of-type) {   border-inline-start-width: 1px; } :where(.c-tbl tr th:last-of-type) {   border-inline-color: hsl(200, 60%, 40%); } :where(.c-tbl tr th:first-of-type) {   border-inline-start-color: hsl(200, 60%, 40%); } :where(.c-tbl thead th:first-of-type) {   border-start-start-radius: 0.5rem; } :where(.c-tbl thead th:last-of-type) {   border-start-end-radius: 0.5rem; } :where(.c-tbl tbody tr:last-of-type td:first-of-type) {   border-end-start-radius: 0.5rem; } :where(.c-tbl tr:last-of-type td:last-of-type) {   border-end-end-radius: 0.5rem; } /* hover */ @media (hover: hover) {   :where(.c-tbl) tr:hover td {     background-color: hsl(200, 60%, 95%);   } }

Now we can create variations of our table component by injecting other styles before or after our generic styles (courtesy of the specificity-stripping powers of :where), either by overwriting the .c-tbl element or by adding a BEM-style modifier-class (e.g. c-tbl--purple):

<table class="c-tbl c-tbl--purple">
.c-tbl--purple th {   background-color: hsl(330, 50%, 40%) } .c-tbl--purple td {   border-color: hsl(330, 40%, 80%); } .c-tbl--purple tr th:last-of-type {   border-inline-color: hsl(330, 50%, 40%); } .c-tbl--purple tr th:first-of-type {   border-inline-start-color: hsl(330, 50%, 40%); }

Cool! But notice how we keep repeating colors? And what if we want to change the border-radius or the border-width? That would end up with a lot of repeated CSS.

Let’s move all of these to CSS custom properties and, while we’re at it, we can move all configurable properties to the top of the component’s “scope“ — which is the table element itself — so we can easily play around with them later.

CSS Custom Properties

I’m going to switch things up in the HTML and use a data-component attribute on the table element that can be targeted for styling.

<table data-component="table" id="table">

That data-component will hold the generic styles that we can use on any instance of the component, i.e. the styles the table needs no matter what color variation we apply. The styles for a specific table component instance will be contained in a regular class, using custom properties from the generic component.

[data-component="table"] {   /* Styles needed for all table variations */ } .c-tbl--purple {   /* Styles for the purple variation */ }

If we place all the generic styles in a data-attribute, we can use whatever naming convention we want. This way, we don’t have to worry if your boss insists on naming the table’s classes something like .BIGCORP__TABLE, .table-component or something else.

In the generic component, each CSS property points to a custom property. Properties, that have to work on child-elements, like border-color, are specified at the root of the generic component:

:where([data-component="table"]) {   /* These will will be used multiple times, and in other selectors */   --tbl-hue: 200;   --tbl-sat: 50%;   --tbl-bdc: hsl(var(--tbl-hue), var(--tbl-sat), 80%); }  /* Here, it's used on a child-node: */ :where([data-component="table"] td) {   border-color: var(--tbl-bdc); }

For other properties, decide whether it should have a static value, or be configurable with its own custom property. If you’re using custom properties, remember to define a default value that the table can fall back to in the event that a variation class is missing.

:where([data-component="table"]) {   /* These are optional, with fallbacks */   background-color: var(--tbl-bgc, transparent);   border-collapse: var(--tbl-bdcl, separate); }

If you’re wondering how I’m naming the custom properties, I’m using a component-prefix (e.g. --tbl) followed by an Emmett-abbreviation (e.g. -bgc). In this case, --tbl is the component-prefix, -bgc is the background color, and -bdcl is the border collapse. So, for example, --tbl-bgc is the table component’s background color. I only use this naming convention when working with component properties, as opposed to global properties which I tend to keep more general.

Now, if we open up DevTools, we can play around with the custom properties. For example, We can change --tbl-hue to a different hue value in the HSL color, set --tbl-bdrs: 0 to remove border-radius, and so on.

A :where CSS rule set showing the custom properties of the table showing how the cascade’s specificity scan be used in context.

When working with your own components, this is the point in time you’ll discover which parameters (i.e. the custom property values) the component needs to make things look just right.

We can also use custom properties to control column alignment and width:

:where[data-component="table"] tr > *:nth-of-type(1)) {   text-align: var(--ca1, initial);   width: var(--cw1, initial);   /* repeat for column 2 and 3, or use a SCSS-loop ... */ }

In DevTools, select the table and add these to the element.styles selector:

element.style {   --ca2: center; /* Align second column center */   --ca3: right; /* Align third column right */ }

Now, let’s create our specific component styles, using a regular class, .c-tbl (which stands for “component-table” in BEM parlance). Let’s toss that class in the table markup.

<table class="c-tbl" data-component="table" id="table">

Now, let’s change the --tbl-hue value in the CSS just to see how this works before we start messing around with all of the property values:

.c-tbl {   --tbl-hue: 330; }

Notice, that we only need to update properties rather than writing entirely new CSS! Changing one little property updates the table’s color — no new classes or overriding properties lower in the cascade.

Notice how the border colors change as well. That’s because all the colors in the table inherit from the --tbl-hue variable

We can write a more complex selector, but still update a single property, to get something like zebra-striping:

.c-tbl tr:nth-child(even) td {   --tbl-td-bgc: hsl(var(--tbl-hue), var(--tbl-sat), 95%); }

And remember: It doesn’t matter where you load the class. Because our generic styles are using :where, the specificity is wiped out, and any custom styles for a specific variation will be applied no matter where they are used. That’s the beauty of using :where to take control of the cascade!

And best of all, we can create all kinds of table components from the generic styles with a few lines of CSS.

Purple table with zebra-striped columns
Light table with a “noinlineborder” parameter… which we’ll cover next

Adding parameters with another data-attribute

So far, so good! The generic table component is very simple. But what if it requires something more akin to real parameters? Perhaps for things like:

  • zebra-striped rows and columns
  • a sticky header and sticky column
  • hover-state options, such as hover row, hover cell, hover column

We could simply add BEM-style modifier classes, but we can actually accomplish it more efficiently by adding another data-attribute to the mix. Perhaps a data-param that holds the parameters like this:

<table data-component="table" data-param="zebrarow stickyrow">

Then, in our CSS, we can use an attribute-selector to match a whole word in a list of parameters. For example, zebra-striped rows:

[data-component="table"][data-param~="zebrarow"] tr:nth-child(even) td {   --tbl-td-bgc: var(--tbl-zebra-bgc); }

Or zebra-striping columns:

[data-component="table"][data-param~="zebracol"] td:nth-of-type(odd) {   --tbl-td-bgc: var(--tbl-zebra-bgc); }

Let’s go nuts and make both the table header and the first column sticky:

 [data-component="table"][data-param~="stickycol"] thead tr th:first-child,[data-component="table"][data-param~="stickycol"] tbody tr td:first-child {   --tbl-td-bgc: var(--tbl-zebra-bgc);   inset-inline-start: 0;   position: sticky; } [data-component="table"][data-param~="stickyrow"] thead th {   inset-block-start: -1px;   position: sticky; }

Here’s a demo that allows you to change one parameter at a time:

The default light theme in the demo is this:

.c-tbl--light {   --tbl-bdrs: 0;   --tbl-sat: 15%;   --tbl-th-bgc: #eee;   --tbl-th-bdc: #eee;   --tbl-th-c: #555;   --tbl-th-tt: normal; }

…where data-param is set to noinlineborder which corresponds to these styles:

[data-param~="noinlineborder"] thead tr > th {   border-block-start-width: 0;   border-inline-end-width: 0;   border-block-end-width: var(--tbl-bdw);   border-inline-start-width: 0; }

I know my data-attribute way of styling and configuring generic components is very opinionated. That’s just how I roll, so please feel free to stick with whatever method you’re most comfortable working with, whether it’s a BEM modifier class or something else.

The bottom line is this: embrace :where and :is and the cascade-controlling powers they provide. And, if possible, construct the CSS in such a way that you wind up writing as little new CSS as possible when creating new component variations!

Cascade Layers

The last cascade-busting tool I want to look at is “Cascade Layers.” At the time of this writing, it’s an experimental feature defined in the CSS Cascading and Inheritance Level 5 specification that you can access in Safari or Chrome by enabling the #enable-cascade-layers flag.

Bramus Van Damme sums up the concept nicely:

The true power of Cascade Layers comes from its unique position in the Cascade: before Selector Specificity and Order Of Appearance. Because of that we don’t need to worry about the Selector Specificity of the CSS that is used in other Layers, nor about the order in which we load CSS into these Layers — something that will come in very handy for larger teams or when loading in third-party CSS.

Perhaps even nicer is his illustration showing where Cascade Layers fall in the cascade:

Credit: Bramus Van Damme

At the beginning of this article, I mentioned ITCSS — a way of taming the cascade by specifying the load-order of generic styles, components etc. Cascade Layers allow us to inject a stylesheet at a given location. So a simplified version of this structure in Cascade Layers looks like this:

@layer generic, components;

With this single line, we’ve decided the order of our layers. First come the generic styles, followed by the component-specific ones.

Let’s pretend that we’re loading our generic styles somewhere much later than our component styles:

@layer components {   body {     background-color: lightseagreen;   } }  /* MUCH, much later... */  @layer generic {    body {     background-color: tomato;   } }

The background-color will be lightseagreen because our component styles layer is set after the generic styles layer. So, the styles in the components layer “win” even if they are written before the generic layer styles.

Again, just another tool for controlling how the CSS cascade applies styles, allowing us more flexibility to organize things logically rather than wrestling with specificity.

Now you’re in control!

The whole point here is that the CSS cascade is becoming a lot easier to wrangle, thanks to new features. We saw how the :where and :is pseudo-selectors allows us to control specificity, either by stripping out the specificity of an entire ruleset or taking on the specificity of the most specific argument, respectively. Then we used CSS Custom Properties to override styles without writing a new class to override another. From there, we took a slight detour down data-attribute lane to help us add more flexibility to create component variations merely by adding arguments to the HTML. And, finally, we poked at Cascade Layers which should prove handy for specifying the loading order or styles using @layer.

If you leave with only one takeaway from this article, I hope it’s that the CSS cascade is no longer the enemy it’s often made to be. We are gaining the tools to stop fighting it and start leaning into even more.

Header photo by Stephen Leonardi on Unsplash

Don’t Fight the Cascade, Control It! originally published on CSS-Tricks. You should get the newsletter and become a supporter.


, , ,

Show, Don’t Tell

How much time do you spend designing the content presentation for your websites? When you write a new blog post or create a new page, are you thinking about just the words, or how your readers will engage with those words? With these few tips in mind, you can make your site’s content easier to digest and more visually interesting for your visitors.

Use good basics

Think about your content’s structure. How can you break down a wall of information into easily digestible chunks? There are some basic formatting rules you can follow to take lots of information and make it easier to read and understand.

Break it up

Use headings and subheadings to group together related content. 

Readers on the web scan for information, rather than reading everything line-by-line. Chunking your content into smaller sections, called out by larger headings, helps them find the information they’re searching for.

When I’m trying to find something quickly, there’s nothing more intimidating than jumping onto a site with a giant wall of unbroken content. At best, I’ll cmd+f and search for specific keywords, but usually I’ll just say “lol no” and look for the information elsewhere.

Just see for yourself — which of these examples seems more approachable?

❌ Bad formatting

✅ Good formatting

Where possible, break down paragraphs into lists. Lists make scanning easier!

❌ Harder to scan

✅ Easier to scan

If I’m reading through content that has multiple steps, for example, I need lists in order to follow along. Whenever I encounter any sort of instructions that don’t break down individual tasks into lists, my brain just can’t pull out the requires steps. I lose my place, or miss something, and get confused. This is true whether you’re listing ingredients, steps in a tutorial, or the different services your company offers. Please, help my brain and make lists.

Call it out

Take advantage of bold and italic formatting to call out important text. 

For example, bold the most important part of a sentence to make sure that readers scanning through your content catch their eyes on what’s most important. If you’re writing an article, what information do you want your readers to remember long after they close your tab? 

Use italics when referencing or citing information, whether that’s a title, quotation, or your own inner dialogue. 

Using good formatting isn’t just helpful for people physically reading your site — it’s also useful for the robots we use to speak your content out loud, whether that’s screen reading software or your virtual assistant. As more of our digital content becomes accessed in physical space, the need for proper semantics becomes even more important.

Aaron Gustafson’s Conversational Semantics goes into this subject more, if you’re curious.

Create emphasis

You can go beyond content formatting like bold and italics to show emphasis, and use tools like layout, color, and imagery to draw attention to your most important information.

Humans process visual information 60,000 times faster than text. Is this figure true? Who knows! I couldn’t find the original data, just tons of websites citing other websites citing some study, but honestly, I’m willing to believe this giving my own experiences browsing content on the web. One of the reasons I find avatars are so helpful is that when scrolling through conversations, whether on Twitter or a chat app like Slack, I identify people first by their avatar, then by their name. Whenever someone updates their avatar I get kind of annoyed at first before my brain catches up and gets used to the new image.

Sorry, who is this?

I know that avatar!

Use imagery to both complement and emphasize your content, whether that’s adding an icon, illustration, or photo your text. Aim for imagery that helps people understand what your content is saying.

For example, take IKEA instructions. Assembling furniture is hard, and IKEA’s visually-driven directions make building your own furniture a little bit easier. 

IKEA MALM instructions

(Just don’t go overboard — a confusing icon or busy image could distract, not help.)

Beyond images, you can also use layout or color to help call out information. Have a bunch of numbers? Organize them into a table, or call out numbers with larger text in their own columns, rather than writing everything out into a paragraph. 

Product Width Height
Green Entrance Rug 3′ 2′
Purple Area Rug 4′ 4′
Multicolor Area Rug 4′ 6′

Using a table to display lists of numerical information


Websites Built


Happy Clients

Using columns to call out important text

Use blockquotes or pullquotes to highlight specific quotations or important parts of your articles.

Need to direct someone to specific or timely information? Wrap that content in a callout box. 

Think outside of just paragraphs! When browsing a website or reading through a book or magazine, what kind of content catches your eye the most? What information do you remember later? Take inspiration from around you.

Tell a story

People are emotional. We remember stories, especially stories we can relate to. Can you frame your content as a story? Can you present it with a curious beginning and strong resolution? Is there conflict? 

During the US 2020 presidential election, Elizabeth Warren’s tech team put together a number of interactive digital experiences for voters to engage with, such as Warren’s Tax the Ultra-Rich plan and accompanying Billionaire Calculator. They wrote up a case study that goes into more depth, but I want to highlight how the informational scrolling pages they built (nicknamed “scrollytelling”) quite literally highlights the important information, providing relevant linked content to learn more, add citations, or inject some additional personality. The pages blend embedded interactive content to engage you as a reader, video, images, and text. It’s a compelling way to present Warren’s story.

Try new ways ways of presenting information

Remember, the web is an interactive platform — take advantage of that, where appropriate (less is more, accessibility is integral, and you need to know your audience). Whether that’s scrollytelling, captioned video, and heck, maybe for your audience, now’s the time to start looking into AR/VR! Who knows. Sometimes you just need to try stuff out and see what sticks. Just be careful. Experimentation is great, but we need to make sure we’re bringing everyone along for the ride. 

Of course, more complex interactions require more work and/or budget, so what you can create with your content depends on your resources. The kind of storytelling you create will different whether you’re a DIY blogger or have access to an agency or internal creative team. And that’s fine — not every article needs to be as media-rich an experience as Snowfall. Even the addition of some simple illustrations, like in Samir Nosrat’s SALT FAT ACID HEAT, can create a great vibe for your readers and help enrich understanding.

SALT FAT ACID HEAT uses illustrations in place of photographs to add visual interest, inform, or demonstrate technique

Whether you’re crafting your first website or you’re a seasoned writer, there’s probably more you can do to present your content in interesting and engaging ways. Next time you make something new, think about the different ways you can show, not tell, information to your audience.


, ,

Don’t Snore on CORS

Whatever, I just needed a title. Everyone’s favorite web security feature has crossed my desk a bunch of times lately and I always feel like that is a sign I should write something because that’s what blogging is.

The main problem with CORS is that developers don’t understand CORS. The basic concept of it is supposed to be easy: don’t run code across origins. Meaning if I, at css-tricks.com, try to fetch some JavaScript from an external URL, like any-other-website.com, the browser will just stop it by default. You’ll see an error in the console. Not allowed.

Unless, that is, the other website sends a header that specifically allows this. My domain can be whitelisted or there could be a wildcard that allows it. There is way more detail here (like preflighting and credentials) and, as ever, the MDN article does a good job on that front.

What have traditionally been hair-pulling moments for me are when CORS seems to behave inconsistently. Two requests will go through and a third will fail, which seems inexplicable, but was reproducible. (Perhaps there was a load balancer involved with half-cached headers? Who knows.) Or I’m trying to use a proxy and the proxy stops working. I can’t even remember all the examples, but I bet I’ve been in meetings trying to debug CORS issues over 100 times in my life.

Anyway, those times where CORS have crossed my desk recently:

  • This video, Learn CORS In 6 Minutes, has 10,000 likes and seems to have struck a chord with folks. A non-ironic npm install cors was the solution here.
  • You have to literally tell servers to have the correct headers. So, similar to the video above, I had to do that in a video about Cloudflare Workers, where I used cross-origin (but you don’t have to, which is actually a very cool feature of Cloudflare Workers).
  • Jake’s article “How to win at CORS” which includes a playground.
  • There are browser extensions (like ones for Firefox and Chrome) that yank in CORS headers for you, which feels like a questionable workaround, but I wouldn’t blame anybody for using in development.

The post Don’t Snore on CORS appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.


, ,

You don’t need external assets in an HTML file

A fun exercise from Terence Eden. You can send an HTML file over the wire including anything a website might need without requesting any other files. CSS and JavaScript are easy, because there are <script> and <style> tags. Images and fonts (and pretty much whatever other kind of asset) aren’t too hard because Data URLs exist. See Terence’s post for an extra-tricky final version including .zip files.

Reminds me of a couple of other tricks…

Direct Link to ArticlePermalink

The post You don’t need external assets in an HTML file appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.


, , , , ,

Don’t attach tooltips to document.body

Here’s Atif Afzal on using a <div> that is permanently on the page where tooltips are added/removed and how they perform vastly better than plopping those same tooltips right into the <body>. It’s not really discussed, but the reason you put them that high-up in the DOM is so you can absolutely position them exactly where you need to on the page without having to deal with hidden overflow or relative parents and the like.

To my amazement, just having a separate container without even adding the [CSS] contain property fixed the performance. The main problem now, was to explain it. First I thought this might be some internal browser heuristic optimizing the Recalculate Style, but there is no black magic and I discovered the reason.

The trick is to avoid forced recalculations of style:

[…] The tooltip container is not visible in the page, so modifying it doesn’t invalidate the complete page render tree. If the tooltip container would have been visible in the page, then the complete render tree would be invalidated but in this case only an independent subtree was invalidated. Recalculating Style for a small subtree of 3 doesn’t take a lot of time and hence is faster.

Looks like popper.js was used here, so you have to be smart about it. We use toast messages on CodePen, and it’s the only third-party component we use at the moment: react-hot-toast. I checked it, and not only do we tuck the messages in a <div> of our own, but the library itself does that, so I think we’re in the clear.

The post Don’t attach tooltips to document.body appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.


, , ,

Web Frameworks: Why You Don’t Always Need Them

Richard MacManus explaining Daniel Kehoe’s approach to building websites:

There are three key web technologies underpinning Kehoe’s approach:

  • ES6 Modules: JavaScript ES6 can support import modules, which are also supported by browsers.
  • Module CDNs: JavaScript modules can now be downloaded from third-party content delivery networks (CDNs).
  • Custom HTML elements: Developers can now create custom HTML tags, via Web Components.

Using a no build process and only features that are built into browser, and yet that still buys you a pretty powerful setup. You can still use stuff off npm. You can still get templating. You can still build with components. You still get isolation where needed.

I’d say today you’re:

  • Giving up some DX (hot module reloading, JSX, framework doodads)
  • Gaining some DX (can jump into project and just start working)
  • Giving up some performance (no tree shaking, loads of network requests)
  • Widening your hiring pool (more people know core technologies than specific tools)

But it’s not hard to imagine a tomorrow where we give up less and gain more, making the tools we use today less necessary. I’m quite sure we’ll always still find a way to jam more tools into what we’re doing. Hammer something something nail.

Direct Link to ArticlePermalink

The post Web Frameworks: Why You Don’t Always Need Them appeared first on CSS-Tricks.

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


, , , ,

Don’t put pointer-events: none on form labels

Bruce Lawson with the tip of the day, warning against the use of pointer-events: none on forms labels. We know that pointer-events is used to change how elements respond to click, tap, hover, and active states. But it apparently borks form labels, squashing their active hit target size to something small and tough to interact with. Bruce includes examples in his post.

That’s not the striking part of the post though. It’s that the issue was pinned to an implementation of Material Design’s floating labels component. Bruce fortunately had pointer events expert Patrick Lauke’s ear, who pointed (get it?) out the issue.

That isn’t a dig at frameworks. It’s just the reality of things. Front-end developers gotta be aware, and that includes awareness of third-party code.

Direct Link to ArticlePermalink

The post Don’t put pointer-events: none on form labels appeared first on CSS-Tricks.

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


, , , ,

“I Don’t Know”

I’ve learned to be more comfortable not knowing. “I don’t know”, comes easier now. “I don’t know anything about that.” It’s okay. It feels good to say.

Whether it’s service workers, Houdini, shadow DOM, web components, HTTP2, CSS grid, “micro-front ends”, AVIF… there are many paths before us. This list doesn’t even broach JavaScript frameworks and libraries. Much of this tech isn’t even novel in 2020—but together act as a clapperboard cueing in me a familiar fear of missing out or imposter syndrome.

How does someone stay current, let alone learn something new? I am reminded of a comment made by Melanie Sumner recently:

Anyone else feel like paying attention to any specific area of development causes the other skills to rust?

To achieve deeper understanding in a topic, one must seclude themselves to a focused path, etching only a tiny arc on the complete circle that is the web. Mastery of a subject comes with it both the elation of achievement and an awareness of the untraveled, much like Matt Might’s The Illustrated Guide to a Ph.D. Piercing or expanding the boundaries of our own spheres of knowledge is exhilarating, yes. But as Melanie observes, it’s a bit like reaching a remote mountain peak only to see more summits stretching out to the horizon. It’s a solitary place, not without reward, but not easily replicated. You must make that next trek from the bottom once more.

The seclusion is as physical as it is mental, given the challenges a global pandemic puts us in. Gone are the meetups, the watercooler moments, the overheard new thing. It was hard enough to ask for help when I could physically tap someone on the shoulder and interrupt their flow. Strangely, it feels more difficult to strike up a call or chat when I’m stuck. Everyone is at the same time a click and a mountain away.

I’ve learned to push through this tendency to seclude and embrace my teammates’ talent. Where I used to enjoy taking a heads-down day to research a problem, I now try to shareout in nearer-to-real-time my findings. The feedback loop is tighter. I’ve adjusted the internal clock that tells me when I am spending too much time on a problem. The team exists to help one another. We’ve set aside time to pair program, mob, and demo. These plans are not without occasional setbacks, however.

Or the time when we got stuck on a bug for 4 hours, only to have fresh eyes glance at the stack trace and find a new path in the span of 15 seconds.

Our more collaborative patterns create a union of skillsets too. We combine arcs of knowledge across the tech we need. We can unblock each other faster, like long-haul truckers tag-teaming a journey. Shared understanding helps us retain context and communicate with less writing. Working more closely on even the mundane has led to change. For example, that engineer that gives me regex tips every time? Where I once bristled, or leaned into their experience, gave way to preemption. “I don’t know how to do that” turned into better and better ideas where to take my first steps. I’d expanded the circumference of my skillset a teensy bit more, journeyed a bit up a new mountain, a guide to help me see the trailhead.

I still walk alone sometimes, and that’s where I can do some of my best work. But I have a better awareness of what I don’t know, and a working realization that my team can go further together than one of us individually. I fret less at the peaks I haven’t explored yet, and am more eager than ever to ask others if they know what’s over there.

The post “I Don’t Know” appeared first on CSS-Tricks.

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


, ,

Don’t Wait! Mock the API

Today we have a loose coupling between the front end and the back end of web applications. They are usually developed by separate teams, and keeping those teams and the technology in sync is not easy. To solve part of this problem, we can “fake” the API server that the back end tech would normally create and develop as if the API or endpoints already exist.

The most common term used for creating simulated or “faking” a component is mocking. Mocking allows you to simulate the API without (ideally) changing the front end. There are many ways to achieve mocking, and this is what makes it so scary for most people, at least in my opinion. 

Let’s cover what a good API mocking should look like and how to implement a mocked API into a new or existing application.

Note, the implementation that I am about to show is framework agnostic — so it can be used with any framework or vanilla JavaScript application.

Mirage: The mocking framework

The mocking approach we are going to use is called Mirage, which is somewhat new. I have tested many mocking frameworks and just recently discovered this one, and it’s been a game changer for me.

Mirage is marketed as a front-end-friendly framework that comes with a modern interface. It works in your browser, client-side, by intercepting XMLHttpRequest and Fetch requests.

We will go through creating a simple application with mocked API and cover some common problems along the way.

Mirage setup

Let’s create one of those standard to-do applications to demonstrate mocking. I will be using Vue as my framework of choice but of course, you can use something else since we’re working with a framework-agnostic approach.

So, go ahead and install Mirage in your project:

# Using npm npm i miragejs -D 
 # Using Yarn yarn add miragejs -D

To start using Mirage, we need to setup a “server” (in quotes, because it’s a fake server). Before we jump into the setup, I will cover the folder structure I found works best.

/ ├── public ├── src │   ├── api │   │   └── mock │   │       ├── fixtures │   │       │   └── get-tasks.js │   │       └── index.js │   └── main.js ├── package.json └── package-lock.json

In a mock directory, open up a new index.js file and define your mock server:

// api/mock/index.js import { Server } from 'miragejs'; 
 export default function ({ environment = 'development' } = {}) {   return new Server({     environment, 
     routes() {       // We will add our routes here     },   }); }

The environment argument we’re adding to the function signature is just a convention. We can pass in a different environment as needed.

Now, open your app bootstrap file. In our case, this is he src/main.js file since we are working with Vue. Import your createServer function, and call it in the development environment.

// main.js import createServer from './mock' 
 if (process.env.NODE_ENV === 'development') {     createServer(); }

We’re using the process.env.NODE_ENV environment variable here, which is a common global variable. The conditional allows Mirage to be tree-shaken in production, therefore, it won’t affect your production bundle.

That is all we need to set up Mirage! It’s this sort of ease that makes the DX of Mirage so nice.

Our createServer function is defaulting it to development environment for the sake of making this article simple. In most cases, this will default to test since, in most apps, you’ll call createServer once in development mode but many times in test files.

How it works

Before we make our first request, let’s quickly cover how Mirage works.

Mirage is a client-side mocking framework, meaning all the mocking will happen in the browser, which Mirage does using the Pretender library. Pretender will temporarily replace native XMLHttpRequest and Fetch configurations, intercept all requests, and direct them to a little pretend service that the Mirage hooks onto.

If you crack open DevTools and head into the Network tab, you won’t see any Mirage requests. That’s because the request is intercepted and handled by Mirage (via Pretender in the back end). Mirage logs all requests, which we’ll get to in just a bit.

Let’s make requests!

Let’s create a request to an /api/tasks endpoint that will return a list of tasks that we are going to show in our to-do app. Note that I’m using axios to fetch the data. That’s just my personal preference. Again, Mirage works with native XMLHttpRequest, Fetch, and any other library.

// components/tasks.vue export default {   async created() {     try {       const { data } = await axios.get('/api/tasks'); // Fetch the data       this.tasks = data.tasks;     } catch(e) {       console.error(e);     }   } };

Opening your JavaScript console — there should be an error from Mirage in there:

Mirage: Your app tried to GET '/api/tasks', but there was no route defined to handle this request.

This means Mirage is running, but the router hasn’t been mocked out yet. Let’s solve this by adding that route.

Mocking requests

Inside our mock/index.js file, there is a routes() hook. Route handlers allow us to define which URLs should be handled by the Mirage server.

To define a router handler, we need to add it inside the routes() function.

// mock/index.js export default function ({ environment = 'development' } = {}) {     // ...     routes() {       this.get('/api/tasks', () => ({         tasks: [           { id: 1, text: "Feed the cat" },           { id: 2, text: "Wash the dishes" },           //...         ],       }))     },   }); }

The routes() hook is the way we define our route handlers. Using a this.get() method lets us mock GET requests. The first argument of all request functions is the URL we are handling, and the second argument is a function that responds with some data.

As a note, Mirage accepts any HTTP request type, and each type has the same signature:

this.get('/tasks', (schema, request) => { ... }); this.post('/tasks', (schema, request) => { ... }); this.patch('/tasks/:id', (schema, request) => { ... }); this.put('/tasks/:id', (schema, request) => { ... }); this.del('/tasks/:id', (schema, request) => { ... }); this.options('/tasks', (schema, request) => { ... });

We will discuss the schema and request parameters of the callback function in a moment.

With this, we have successfully mocked our route and we should see inside our console a successful response from Mirage.

Screenshot of a Mirage response in the console showing data for two task objects with IDs 1 and 2.

Working with dynamic data

Trying to add a new to-do in our app won’t be possible because our data in the GET response has hardcoded values. Mirage’s solution to this is that they provide a lightweight data layer that acts as a database. Let’s fix what we have so far.

Like the routes() hook, Mirage defines a seeds() hook. It allows us to create initial data for the server. I’m going to move the GET data to the seeds() hook where I will push it to the Mirage database.

seeds(server) {   server.db.loadData({     tasks: [       { id: 1, text: "Feed the cat" },       { id: 2, text: "Wash the dishes" },     ],   }) },

I moved our static data from the GET method to seeds() hook, where that data is loaded into a faux database. Now, we need to refactor our GET method to return data from that database. This is actually pretty straightforward — the first argument of the callback function of any route() method is the schema.

this.get('/api/tasks', (schema) => {   return schema.db.tasks; })

Now we can add new to-do items to our app by making a POST request:

async addTask() {   const { data } = await axios.post('/api/tasks', { data: this.newTask });   this.tasks.push(data);   this.newTask = {}; },

We mock this route in Mirage by creating a POST /api/tasks route handler:

this.post('/tasks', (schema, request) => {})

Using the second parameter of the callback function, we can see the sent request.

Screenshot of the mocking server request. The requestBody property is highlighted in yellow and contains text data that says Hello CSS-Tricks.

Inside the requestBody property is the data that we sent. That means it’s now available for us to create a new task.

this.post('/api/tasks', (schema, request) => {   // Take the send data from axios.   const task = JSON.parse(request.requestBody).data 
   return schema.db.tasks.insert(task) })

The id of the task will be set by the Mirage’s database by default. Thus, there is no need to keep track of ids and send them with your request — just like a real server.

Dynamic routes? Sure!

The last thing to cover is dynamic routes. They allow us to use a dynamic segment in our URL, which is useful for deleting or updating a single to-do item in our app.

Our delete request should go to /api/tasks/1, /api/tasks/2, and so on. Mirage provides a way for us  to define a dynamic segment in the URL, like this:

this.delete('/api/tasks/:id', (schema, request) => {   // Return the ID from URL.   const id = request.params.id; 
   return schema.db.tasks.remove(id); })

Using a colon (:) in the URL is how we define a dynamic segment in our URL. After the colon, we specify the name of the segment which, in our case, is called id and maps to the ID of a specific to-do item. We can access the value of the segment via the request.params object, where the property name corresponds to the segment name — request.params.id. Then we use the schema to remove an item with that same ID from the Mirage database.

If you’ve noticed, all of my routes so far are prefixed with api/. Writing this over and over can be cumbersome and you may want to make it easier. Mirage offers the namespace property that can help. Inside the routes hook, we can define the namespace property so we don’t have to write that out each time.

routes() {  // Prefix for all routes.  this.namespace = '/api'; 
  this.get('/tasks', () => { ... })  this.delete('/tasks/:id', () => { ... })  this.post('/tasks', () => { ... }) }

OK, let’s integrate this into an existing app

So far, everything we’ve looked at integrates Mirage into a new app. But what about adding Mirage to an existing application? Mirage has you covered so you don’t have to mock your entire API.

The first thing to note is that adding Mirage to an existing application will throw an error if the site makes a request that isn’t handled by Mirage. To avoid this, we can tell Mirage to pass through all unhandled requests.

routes() {   this.get('/tasks', () => { ... })      // Pass through all unhandled requests.   this.passthrough() }

Now we can develop on top of an existing API with Mirage handling only the missing parts of our API.

Mirage can even change the base URL of which it captures the requests. This is useful because, usually, a server won’t live on localhost:3000 but rather on a custom domain.

routes() {  // Set the base route.  this.urlPrefix = 'https://devenv.ourapp.example'; 
  this.get('/tasks', () => { ... }) }

Now, all of our requests will point to the real API server, but Mirage will intercept them like it did when we set it up with a new app. This means that the transition from Mirage to the real API is pretty darn seamless — delete the route from the mock server and things are good to go.

Wrapping up

Over the course of five years, I have used many mocking frameworks, yet I never truly liked any of the solutions out there. That was until recently, when my team was faced with a need for a mocking solution and I found out about Mirage.

Other solutions, like the commonly used JSON-Server, are external processes that need to run alongside the front end. Furthermore, they are often nothing more than an Express server with utility functions on top. The result is that front-end developers like us need to know about middleware, NodeJS, and how servers work… things many of us probably don’t want to handle. Other attempts, like Mockoon, have a complex interface while lacking much-needed features. There’s another group of frameworks that are only used for testing, like the popular SinonJS. Unfortunately, these frameworks can’t be used to mock the regular behavior.

My team managed to create a functioning server that enables us to write front-end code as if we were working with a real back-end. We did it by writing the front-end codebase without any external processes or servers that are needed to run. This is why I love Mirage. It is really simple to set up, yet powerful enough to handle anything that’s thrown at it. You can use it for basic applications that return a static array to full-blown back-end apps alike — regardless of whether it’s a new or existing app.

There’s a lot more to Mirage beyond the implementations we covered here. A working example of what we covered can be found on GitHub. (Fun fact: Mirage also works with GraphQL!) Mirage has well-written documentation that includes a bunch of step-by-step tutorials, so be sure to check it out.

The post Don’t Wait! Mock the API appeared first on CSS-Tricks.

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


, ,