Categories
Article reset

Notes on Josh Comeau’s Custom CSS Reset

We recently talked with Elad Shechter on his new CSS reset, and shortly after that Josh Comeau blogged his. We’re in something of a new era of CSS resets where… you kind of don’t need one? There isn’t that many major differences between browsers on default styling, and by the time you’re off and running…

Want to Read more ? We recently talked with Elad Shechter on his new CSS reset, and shortly after that Josh Comeau blogged his.

We’re in something of a new era of CSS resets where… you kind of don’t need one? There isn’t that many major differences between browsers on default styling, and by the time you’re off and running styling stuff, you’ve probably steamrolled things into place. And so perhaps “modern” CSS resets are more of a collection of opinionated default styles that do useful things that you want on all your new projects because, well, that’s how you roll.

Looking through Josh’s choices, that’s what it seems like to me: a collection of things that aren’t particularly opinionated about design, but assist the design by being things that pretty much any project will want.

I’m gonna go through it and toss out 🔥 flamin’ hot opinions.

*, *::before, *::after {
box-sizing: border-box;
}

Heck yes. We used to consider this a global holiday ’round here. Although, with more and more layout being handled by grid and flexbox, I’m feeling like this is slightly less useful these days. When you’re setting up a layout with fr units and flexin’ stuff, the box-sizing model doesn’t come into play all that much, even when padding and border are involved. But hey, I still prefer it to be in place. I do think if it goes into a CSS reset it should use the inheritance model though, as it’s easier to undo on a component that way.

* {
margin: 0;
}

This is basically why the CSS-Tricks logo “star” exists. I used to love this little snippet in my CSS resets. There was a period where it started to feel heavy-handed, but I think I’m back to liking it. I like how explicit you have to be when applying any margin at all. Personally, I’d rock padding: 0; too, as list elements tend to have some padding pushing them around. If you’re nuking spacing stuff, may as well nuke it all.

html, body {
height: 100%;
}

Probably a good plan. Josh says “Allow percentage-based heights in the application,” which I can’t say comes up much in my day-today, but what it does is stuff like the body background not filling the space the way you might expect it to.

Too bad body { height: 100vh; } isn’t enough here, but I feel like that’s not as reliable for some reason I can’t think of right now. Maybe something to do with the footer navigation in iOS Safari?

body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}

I can’t get into the -webkit-font-smoothing: antialiased; thing. I think it tends to make type dramatically thin and I don’t love it. I don’t mind it as a tool, but I wouldn’t globally apply it on all my projects.

I also generally like to put global typographic sizing stuff on the html selector instead, just because the “root” part of rem implies the <html> element — not the <body> — and I like sizing stuff in rem and then adjusting the root font-size at the root level in media queries.

That 1.5 value feels like a good default line-height (more of a 1.4 guy myself, but I’d rather go up than down). But as soon as it’s set, I feel magnetically pulled into reducing it for heading elements where it’s always too much. That could happen via h1, h2, h3 kinda selectors (maybe h4–h6 don’t need it), but Josh has some CSS trickery at work with this snippet that didn’t make it into the final reset:

* {
line-height: calc(1em + 0.5rem);
}

That’s clever in how the 0.5rem goes a long way for small type, but isn’t as big of an influence for large type. I could see trying that on a greenfield project. Prior art here is by Jesús Ricarte in “Using calc to figure out optimal line-height.”

img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}

Good move for a CSS reset. The block display type there prevents those annoying line-height gaps that always kill me. And you almost never want any of these media blocks to be wider than the parent. I somehow don’t think picture is necessary, though, as it’s not really a style-able block? Could be wrong. I’d probably toss iframe and object in there as well.

p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}

Good move for sure. It’s bad news when a long word (like a URL) forces an element wide and borks a layout. I tend to chuck this on something — like article or .text-content or something — and let it cascade into that whole area (which would also catch text that happens to be contained in an improper element), but I don’t mind seeing it on specific text elements.

If doing that, you probably wanna chuck li, dl, dt, blockquote on that chain. Despite having attempted to research this several times (here’s a playground), I still don’t 100% know what the right cocktail of line-wrapping properties is best to use. There is word-break: break-word; that I think is basically the same thing. And I think it’s generally best to use hyphens: auto; too… right??

#root, #__next {
isolation: isolate;
}

I don’t quite understand what’s happening here. I get that this is a React/Next thing where you mount the app to these roots, and I get that it makes a stacking context, I just don’t get why it’s specifically useful to have that stacking context at this level. At the same time, I also don’t see any particular problem with it.

All in all — pretty cool! I always enjoy seeing what other people use (and go so far as to suggest) for CSS resets.

Notes on Josh Comeau’s Custom CSS Reset originally published on CSS-Tricks. You should get the newsletter and become a supporter.

Visit source: Post courtesy of : Notes on Josh Comeau’s Custom CSS Reset

Categories
angular Article components framework monorepo svelte vue

How to Make a Component That Supports Multiple Frameworks in a Monorepo

Your mission — should you decide to accept it — is to build a Button component in four frameworks, but, only use one button.css file! This idea is very important to me. I’ve been working on a component library called AgnosticUI where the purpose is building UI components that aren’t tied to any one particular…

Want to Read more ? Your mission — should you decide to accept it — is to build a Button component in four frameworks, but, only use one button.css file!

This idea is very important to me. I’ve been working on a component library called AgnosticUI where the purpose is building UI components that aren’t tied to any one particular JavaScript framework. AgnosticUI works in React, Vue 3, Angular, and Svelte. So that’s exactly what we’ll do today in this article: build a button component that works across all these frameworks.

The source code for this article is available on GitHub on the the-little-button-that-could-series branch.

Table of contents

Why a monorepo?Setting upFramework-specific workspacesWhat have we just done?Finishing touchesUpdating each component to take a mode propertyCode completeHomeworkPotential pitfallsConclusion

Why a monorepo?

We’re going to set up a tiny Yarn workspaces-based monorepo. Why? Chris actually has a nice outline of the benefits in another post. But here’s my own biased list of benefits that I feel are relevant for our little buttons endeavor:

Coupling

We’re trying to build a single button component that uses just one button.css file across multiple frameworks. So, by nature, there’s some purposeful coupling going on between the various framework implementations and the single-source-of-truth CSS file. A monorepo setup provides a convenient structure that facilitates copying our single button.css component into various framework-based projects.

Workflow

Let’s say the button needs a tweak — like the “focus-ring” implementation, or we screwed up the use of aria in the component templates. Ideally, we’d like to correct things in one place rather than making individual fixes in separate repositories.

Testing

We want the convenience of firing up all four button implementations at the same time for testing. As this sort of project grows, it’s safe to assume there will be more proper testing. In AgnosticUI, for example, I’m currently using Storybook and often kick off all the framework Storybooks, or run snapshot testing across the entire monorepo.

I like what Leonardo Losoviz has to say about the monorepo approach. (And it just so happens to align with with everything we’ve talked about so far.)

I believe the monorepo is particularly useful when all packages are coded in the same programming language, tightly coupled, and relying on the same tooling.

Setting up

Time to dive into code — start by creating a top-level directory on the command-line to house the project and then cd into it. (Can’t think of a name? mkdir buttons && cd buttons will work fine.)

First off, let’s initialize the project:

$ yarn init
yarn init v1.22.15
question name (articles): littlebutton
question version (1.0.0):
question description: my little button project
question entry point (index.js):
question repository url:
question author (Rob Levin):
question license (MIT):
question private:
success Saved package.json

That gives us a package.json file with something like this:

{
“name”: “littlebutton”,
“version”: “1.0.0”,
“description”: “my little button project”,
“main”: “index.js”,
“author”: “Rob Levin”,
“license”: “MIT”
}

Creating the baseline workspace

We can set the first one up with this command:

mkdir -p ./littlebutton-css

Next, we need to add the two following lines to the monorepo’s top-level package.json file so that we keep the monorepo itself private. It also declares our workspaces:

// …
“private”: true,
“workspaces”: [“littlebutton-react”, “littlebutton-vue”, “littlebutton-svelte”, “littlebutton-angular”, “littlebutton-css”]

Now descend into the littlebutton-css directory. We’ll again want to generate a package.json with yarn init. Since we’ve named our directory littlebutton-css (the same as how we specified it in our workspaces in package.json) we can simply hit the Return key and accept all the prompts:

$ cd ./littlebutton-css && yarn init
yarn init v1.22.15
question name (littlebutton-css):
question version (1.0.0):
question description:
question entry point (index.js):
question repository url:
question author (Rob Levin):
question license (MIT):
question private:
success Saved package.json

At this point, the directory structure should look like this:

├── littlebutton-css
│ └── package.json
└── package.json

We’ve only created the CSS package workspace at this point as we’ll be generating our framework implementations with tools like vite which, in turn, generate a package.json and project directory for you. We will have to remember that the name we choose for these generated projects must match the name we’ve specified in the package.json for our earlier workspaces to work.

Baseline HTML & CSS

Let’s stay in the ./littlebutton-css workspace and create our simple button component using vanilla HTML and CSS files.

touch index.html ./css/button.css

Now our project directory should look like this:

littlebutton-css
├── css
│ └── button.css
├── index.html
└── package.json

Let’s go ahead and connect some dots with some boilerplate HTML in ./index.html:

<!doctype html>
<html lang=”en”>
<head>
<meta charset=”utf-8″>
<title>The Little Button That Could</title>
<meta name=”description” content=””>
<meta name=”viewport” content=”width=device-width, initial-scale=1″>
<link rel=”stylesheet” href=”css/button.css”>
</head>
<body>
<main>
<button class=”btn”>Go</button>
</main>
</body>
</html>

And, just so we have something visual to test, we can add a little color in ./css/button.css:

.btn {
color: hotpink;
}

Now open up that index.html page in the browser. If you see an ugly generic button with hotpink text… success!

Framework-specific workspaces

So what we just accomplished is the baseline for our button component. What we want to do now is abstract it a bit so it’s extensible for other frameworks and such. For example, what if we want to use the button in a React project? We’re going to need workspaces in our monorepo for each one. We’ll start with React, then follow suit for Vue 3, Angular, and Svelte.

React

We’re going to generate our React project using vite, a very lightweight and blazingly fast builder. Be forewarned that if you attempt to do this with create-react-app, there’s a very good chance you will run into conflicts later with react-scripts and conflicting webpack or Babel configurations from other frameworks, like Angular.

To get our React workspace going, let’s go back into the terminal and cd back up to the top-level directory. From there, we’ll use vite to initialize a new project — let’s call it littlebutton-react — and, of course, we’ll select react as the framework and variant at the prompts:

$ yarn create vite
yarn create v1.22.15
[1/4] 🔍 Resolving packages…
[2/4] 🚚 Fetching packages…
[3/4] 🔗 Linking dependencies…
[4/4] 🔨 Building fresh packages…

success Installed “create-vite@2.6.6” with binaries:
– create-vite
– cva
✔ Project name: … littlebutton-react
✔ Select a framework: › react
✔ Select a variant: › react

Scaffolding project in /Users/roblevin/workspace/opensource/guest-posts/articles/littlebutton-react…

Done. Now run:

cd littlebutton-react
yarn
yarn dev

✨ Done in 17.90s.

We initialize the React app with these commands next:

cd littlebutton-react
yarn
yarn dev

With React installed and verified, let’s replace the contents of src/App.jsx to house our button with the following code:

import “./App.css”;

const Button = () => {
return <button>Go</button>;
};

function App() {
return (
<div className=”App”>
<Button />
</div>
);
}

export default App;

Now we’re going to write a small Node script that copies our littlebutton-css/css/button.css right into our React application for us. This step is probably the most interesting one to me because it’s both magical and ugly at the same time. It’s magical because it means our React button component is truly deriving its styles from the same CSS written in the baseline project. It’s ugly because, well, we are reaching up out of one workspace and grabbing a file from another. ¯_(ツ)_/¯

Add the following little Node script to littlebutton-react/copystyles.js:

const fs = require(“fs”);
let css = fs.readFileSync(“../littlebutton-css/css/button.css”, “utf8”);
fs.writeFileSync(“./src/button.css”, css, “utf8”);

Let’s place a node command to run that in a package.json script that happens before the dev script in littlebutton-react/package.json. We’ll add a syncStyles and update the dev to call syncStyles before vite:

“syncStyles”: “node copystyles.js”,
“dev”: “yarn syncStyles && vite”,

Now, anytime we fire up our React application with yarn dev, we’ll first be copying the CSS file over. In essence, we’re “forcing” ourselves to not diverge from the CSS package’s button.css in our React button.

But we want to also leverage CSS Modules to prevent name collisions and global CSS leakage, so we have one more step to do to get that wired up (from the same littlebutton-react directory):

touch src/button.module.css

Next, add the following to the new src/button.module.css file:

.btn {
composes: btn from ‘./button.css’;
}

I find composes (also known as composition) to be one of the coolest features of CSS Modules. In a nutshell, we’re copying our HTML/CSS version of button.css over wholesale then composing from our one .btn style rule.

With that, we can go back to our src/App.jsx and import the CSS Modules styles into our React component with this:

import “./App.css”;
import styles from “./button.module.css”;

const Button = () => {
return <button className={styles.btn}>Go</button>;
};

function App() {
return (
<div className=”App”>
<Button />
</div>
);
}

export default App;

Whew! Let’s pause and try to run our React app again:

yarn dev

If all went well, you should see that same generic button, but with hotpink text. Before we move on to the next framework, let’s move back up to our top-level monorepo directory and update its package.json:

{
“name”: “littlebutton”,
“version”: “1.0.0”,
“description”: “toy project”,
“main”: “index.js”,
“author”: “Rob Levin”,
“license”: “MIT”,
“private”: true,
“workspaces”: [“littlebutton-react”, “littlebutton-vue”, “littlebutton-svelte”, “littlebutton-angular”],
“scripts”: {
“start:react”: “yarn workspace littlebutton-react dev”
}
}

Run the yarn command from the top-level directory to get the monorepo-hoisted dependencies installed.

The only change we’ve made to this package.json is a new scripts section with a single script to start the React app. By adding start:react we can now run yarn start:react from our top-level directory and it will fire up the project we just built in ./littlebutton-react without the need for cd‘ing — super convenient!

We’ll tackle Vue and Svelte next. It turns out that we can take a pretty similar approach for these as they both use single file components (SFC). Basically, we get to mix HTML, CSS, and JavaScript all into one single file. Whether you like the SFC approach or not, it’s certainly adequate enough for building out presentational or primitive UI components.

Vue

Following the steps from vite’s scaffolding docs we’ll run the following command from the monorepo’s top-level directory to initialize a Vue app:

yarn create vite littlebutton-vue –template vue

This generates scaffolding with some provided instructions to run the starter Vue app:

cd littlebutton-vue
yarn
yarn dev

This should fire up a starter page in the browser with some heading like “Hello Vue 3 + Vite.” From here, we can update src/App.vue to:

<template>
<div id=”app”>
<Button class=”btn”>Go</Button>
</div>
</template>

<script>
import Button from ‘./components/Button.vue’

export default {
name: ‘App’,
components: {
Button
}
}
</script>

And we’ll replace any src/components/* with src/components/Button.vue:

<template>
<button :class=”classes”><slot /></button>
</template>

<script>
export default {
name: ‘Button’,
computed: {
classes() {
return {
[this.$style.btn]: true,
}
}
}
}
</script>

<style module>
.btn {
color: slateblue;
}
</style>

Let’s break this down a bit:

:class=”classes” is using Vue’s binding to call the computed classes method.The classes method, in turn, is utilizing CSS Modules in Vue with the this.$style.btn syntax which will use styles contained in a <style module> tag.

For now, we’re hardcoding color: slateblue simply to test that things are working properly within the component. Try firing up the app again with yarn dev. If you see the button with our declared test color, then it’s working!

Now we’re going to write a Node script that copies our littlebutton-css/css/button.css into our Button.vue file similar to the one we did for the React implementation. As mentioned, this component is a SFC so we’re going to have to do this a little differently using a simple regular expression.

Add the following little Node.js script to littlebutton-vue/copystyles.js:

const fs = require(“fs”);
let css = fs.readFileSync(“../littlebutton-css/css/button.css”, “utf8”);
const vue = fs.readFileSync(“./src/components/Button.vue”, “utf8”);
// Take everything between the starting and closing style tag and replace
const styleRegex = /<style module>([sS]*?)</style>/;
let withSynchronizedStyles = vue.replace(styleRegex, `<style module>n${css}n</style>`);
fs.writeFileSync(“./src/components/Button.vue”, withSynchronizedStyles, “utf8”);

There’s a bit more complexity in this script, but using replace to copy text between opening and closing style tags via regex isn’t too bad.

Now let’s add the following two scripts to the scripts clause in the littlebutton-vue/package.json file:

“syncStyles”: “node copystyles.js”,
“dev”: “yarn syncStyles && vite”,

Now run yarn syncStyles and look at ./src/components/Button.vue again. You should see that our style module gets replaced with this:

<style module>
.btn {
color: hotpink;
}
</style>

Run the Vue app again with yarn dev and verify you get the expected results — yes, a button with hotpink text. If so, we’re good to move on to the next framework workspace!

Svelte

Per the Svelte docs, we should kick off our littlebutton-svelte workspace with the following, starting from the monorepo’s top-level directory:

npx degit sveltejs/template littlebutton-svelte
cd littlebutton-svelte
yarn && yarn dev

Confirm you can hit the “Hello World” start page at http://localhost:5000. Then, update littlebutton-svelte/src/App.svelte:

<script>
import Button from ‘./Button.svelte’;
</script>
<main>
<Button>Go</Button>
</main>

Also, in littlebutton-svelte/src/main.js, we want to remove the name prop so it looks like this:

import App from ‘./App.svelte’;

const app = new App({
target: document.body
});

export default app;

And finally, add littlebutton-svelte/src/Button.svelte with the following:

<button class=”btn”>
<slot></slot>
</button>

<script>
</script>

<style>
.btn {
color: saddlebrown;
}
</style>

One last thing: Svelte appears to name our app: “name”: “svelte-app” in the package.json. Change that to “name”: “littlebutton-svelte” so it’s consistent with the workspaces name in our top-level package.json file.

Once again, we can copy our baseline littlebutton-css/css/button.css into our Button.svelte. As mentioned, this component is a SFC, so we’re going to have to do this using a regular expression. Add the following Node script to littlebutton-svelte/copystyles.js:

const fs = require(“fs”);
let css = fs.readFileSync(“../littlebutton-css/css/button.css”, “utf8”);
const svelte = fs.readFileSync(“./src/Button.svelte”, “utf8”);
const styleRegex = /<style>([sS]*?)</style>/;
let withSynchronizedStyles = svelte.replace(styleRegex, `<style>n${css}n</style>`);
fs.writeFileSync(“./src/Button.svelte”, withSynchronizedStyles, “utf8”);

This is super similar to the copy script we used with Vue, isn’t it? We’ll add similar scripts to our package.json script:

“dev”: “yarn syncStyles && rollup -c -w”,
“syncStyles”: “node copystyles.js”,

Now run yarn syncStyles && yarn dev. If all is good, we once again should see a button with hotpink text.

If this is starting to feel repetitive, all I have to say is welcome to my world. What I’m showing you here is essentially the same process I’ve been using to build my AgnosticUI project!

Angular

You probably know the drill by now. From the monorepo’s top-level directory, install Angular and create an Angular app. If we were creating a full-blown UI library we’d likely use ng generate library or even nx. But to keep things as straightforward as possible we’ll set up a boilerplate Angular app as follows:

npm install -g @angular/cli ### unless you already have installed
ng new littlebutton-angular ### choose no for routing and CSS
? Would you like to add Angular routing? (y/N) N
❯ CSS
SCSS [ https://sass-lang.com/documentation/syntax#scss ]
Sass [ https://sass-lang.com/documentation/syntax#the-indented-syntax ]
Less [ http://lesscss.org ]

cd littlebutton-angular && ng serve –open

With the Angular setup confirmed, let’s update some files. cd littlebutton-angular, delete the src/app/app.component.spec.ts file, and add a button component in src/components/button.component.ts, like this:

import { Component } from ‘@angular/core’;

@Component({
selector: ‘little-button’,
templateUrl: ‘./button.component.html’,
styleUrls: [‘./button.component.css’],
})
export class ButtonComponent {}

Add the following to src/components/button.component.html:

<button class=”btn”>Go</button>

And put this in the src/components/button.component.css file for testing:

.btn {
color: fuchsia;
}

In src/app/app.module.ts:

import { NgModule } from ‘@angular/core’;
import { BrowserModule } from ‘@angular/platform-browser’;

import { AppComponent } from ‘./app.component’;
import { ButtonComponent } from ‘../components/button.component’;

@NgModule({
declarations: [AppComponent, ButtonComponent],
imports: [BrowserModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}

Next, replace src/app/app.component.ts with:

import { Component } from ‘@angular/core’;

@Component({
selector: ‘app-root’,
templateUrl: ‘./app.component.html’,
styleUrls: [‘./app.component.css’],
})
export class AppComponent {}

Then, replace src/app/app.component.html with:

<main>
<little-button>Go</little-button>
</main>

With that, let’s run yarn start and verify our button with fuchsia text renders as expected.

Again, we want to copy over the CSS from our baseline workspace. We can do that by adding this to littlebutton-angular/copystyles.js:

const fs = require(“fs”);
let css = fs.readFileSync(“../littlebutton-css/css/button.css”, “utf8”);
fs.writeFileSync(“./src/components/button.component.css”, css, “utf8”);

Angular is nice in that it uses ViewEncapsulation that defaults to to emulate which mimics, according to the docs,

[…] the behavior of shadow DOM by preprocessing (and renaming) the CSS code to effectively scope the CSS to the component’s view.

This basically means we can literally copy over button.css and use it as-is.

Finally, update the package.json file by adding these two lines in the scripts section:

“start”: “yarn syncStyles && ng serve”,
“syncStyles”: “node copystyles.js”,

With that, we can now run yarn start once more and verify our button text color (which was fuchsia) is now hotpink.

What have we just done?

Let’s take a break from coding and think about the bigger picture and what we’ve just done. Basically, we’ve set up a system where any changes to our CSS package’s button.css will get copied over into all the framework implementations as a result of our copystyles.js Node scripts. Further, we’ve incorporated idiomatic conventions for each of the frameworks:

SFC for Vue and SvelteCSS Modules for React (and Vue within the SFC <style module> setup)ViewEncapsulation for Angular

Of course I state the obvious that these aren’t the only ways to do CSS in each of the above frameworks (e.g. CSS-in-JS is a popular choice), but they are certainly accepted practices and are working quite well for our greater goal — to have a single CSS source of truth to drive all framework implementations.

If, for example, our button was in use and our design team decided we wanted to change from 4px to 3px border-radius, we could update the one file, and any separate implementations would stay synced.

This is compelling if you have a polyglot team of developers that enjoy working in multiple frameworks, or, say an offshore team (that’s 3× productive in Angular) that’s being tasked to build a back-office application, but your flagship product is built in React. Or, you’re building an interim admin console and you’d love to experiment with using Vue or Svelte. You get the picture.

Finishing touches

OK, so we have the monorepo architecture in a really good spot. But there’s a few things we can do to make it even more useful as far as the developer experience goes.

Better start scripts

Let’s move back up to our top-level monorepo directory and update its package.json scripts section with the following so we can kick any framework implementation without cd‘ing:

// …
“scripts”: {
“start:react”: “yarn workspace littlebutton-react dev”,
“start:vue”: “yarn workspace littlebutton-vue dev “,
“start:svelte”: “yarn workspace littlebutton-svelte dev”,
“start:angular”: “yarn workspace littlebutton-angular start”
},

Better baseline styles

We can also provide a better set of baseline styles for the button so it starts from a nice, neutral place. Here’s what I did in the littlebutton-css/css/button.css file.

View Full Snippet

.btn {
–button-dark: #333;
–button-line-height: 1.25rem;
–button-font-size: 1rem;
–button-light: #e9e9e9;
–button-transition-duration: 200ms;
–button-font-stack:
system-ui,
-apple-system,
BlinkMacSystemFont,
“Segoe UI”,
Roboto,
Ubuntu,
“Helvetica Neue”,
sans-serif;

display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
user-select: none;
appearance: none;
cursor: pointer;
box-sizing: border-box;
transition-property: all;
transition-duration: var(–button-transition-duration);
color: var(–button-dark);
background-color: var(–button-light);
border-color: var(–button-light);
border-style: solid;
border-width: 1px;
font-family: var(–button-font-stack);
font-weight: 400;
font-size: var(–button-font-size);
line-height: var(–button-line-height);
padding-block-start: 0.5rem;
padding-block-end: 0.5rem;
padding-inline-start: 0.75rem;
padding-inline-end: 0.75rem;
text-decoration: none;
text-align: center;
}

/* Respect users reduced motion preferences */
@media (prefers-reduced-motion) {
.btn {
transition-duration: 0.001ms !important;
}
}

Let’s test this out! Fire up each of the four framework implementations with the new and improved start scripts and confirm the styling changes are in effect.

One CSS file update proliferated to four frameworks — pretty cool, eh!?

Set a primary mode

We’re going to add a mode prop to each of our button’s and implement primary mode next. A primary button could be any color but we’ll go with a shade of green for the background and white text. Again, in the baseline stylesheet:

.btn {
–button-primary: #14775d;
–button-primary-color: #fff;
/* … */
}

Then, just before the @media (prefers-reduced-motion) query, add the following btn-primary to the same baseline stylesheet:

.btn-primary {
background-color: var(–button-primary);
border-color: var(–button-primary);
color: var(–button-primary-color);
}

There we go! Some developer conveniences and better baseline styles!

Updating each component to take a mode property

Now that we’ve added our new primary mode represented by the .btn-primary class, we want to sync the styles for all four framework implementations. So, let’s add some more package.json scripts to our top level scripts:

“sync:react”: “yarn workspace littlebutton-react syncStyles”,
“sync:vue”: “yarn workspace littlebutton-vue syncStyles”,
“sync:svelte”: “yarn workspace littlebutton-svelte syncStyles”,
“sync:angular”: “yarn workspace littlebutton-angular syncStyles”

Be sure to respect JSON’s comma rules! Depending on where you place these lines within your scripts: {…}, you’ll want to make sure there are no missing or trailing commas.

Go ahead and run the following to fully synchronize the styles:

yarn sync:angular && yarn sync:react && yarn sync:vue && yarn sync:svelte

Running this doesn’t change anything because we haven’t applied the primary class yet, but you should at least see the CSS has been copied over if you go look at the framework’s button component CSS.

React

If you haven’t already, double-check that the updated CSS got copied over into littlebutton-react/src/button.css. If not, you can run yarn syncStyles. Note that if you forget to run yarn syncStyles our dev script will do this for us when we next start the application anyway:

“dev”: “yarn syncStyles && vite”,

For our React implementation, we additionally need to add a composed CSS Modules class in littlebutton-react/src/button.module.css that is composed from the new .btn-primary:

.btnPrimary {
composes: btn-primary from ‘./button.css’;
}

We’ll also update littlebutton-react/src/App.jsx:

import “./App.css”;
import styles from “./button.module.css”;

const Button = ({ mode }) => {
const primaryClass = mode ? styles[`btn${mode.charAt(0).toUpperCase()}${mode.slice(1)}`] : ”;
const classes = primaryClass ? `${styles.btn} ${primaryClass}` : styles.btn;
return <button className={classes}>Go</button>;
};

function App() {
return (
<div className=”App”>
<Button mode=”primary” />
</div>
);
}

export default App;

Fire up the React app with yarn start:react from the top-level directory. If all goes well, you should now see your green primary button.

As a note, I’m keeping the Button component in App.jsx for brevity. Feel free to tease out the Button component into its own file if that bothers you.

Vue

Again, double-check that the button styles were copied over and, if not, run yarn syncStyles.

Next, make the following changes to the <script> section of littlebutton-vue/src/components/Button.vue:

<script>
export default {
name: ‘Button’,
props: {
mode: {
type: String,
required: false,
default: ”,
validator: (value) => {
const isValid = [‘primary’].includes(value);
if (!isValid) {
console.warn(`Allowed types for Button are primary`);
}
return isValid;
},
}
},
computed: {
classes() {
return {
[this.$style.btn]: true,
[this.$style[‘btn-primary’]]: this.mode === ‘primary’,
}
}
}
}
</script>

Now we can update the markup in littlebutton-vue/src/App.vue to use the new mode prop:

<Button mode=”primary”>Go</Button>

Now you can yarn start:vue from the top-level directory and check for the same green button.

Svelte

Let’s cd into littlebutton-svelte and verify that the styles in littlebutton-svelte/src/Button.svelte have the new .btn-primary class copied over, and yarn syncStyles if you need to. Again, the dev script will do that for us anyway on the next startup if you happen to forget.

Next, update the Svelte template to pass the mode of primary. In src/App.svelte:

<script>
import Button from ‘./Button.svelte’;
</script>
<main>
<Button mode=”primary”>Go</Button>
</main>

We also need to update the top of our src/Button.svelte component itself to accept the mode prop and apply the CSS Modules class:

<button class=”{classes}”>
<slot></slot>
</button>
<script>
export let mode = “”;
const classes = [
“btn”,
mode ? `btn-${mode}` : “”,
].filter(cls => cls.length).join(” “);
</script>

Note that the <styles> section of our Svelte component shouldn’t be touched in this step.

And now, you can yarn dev from littlebutton-svelte (or yarn start:svelte from a higher directory) to confirm the green button made it!

Angular

Same thing, different framework: check that the styles are copied over and run yarn syncStyles if needed.

Let’s add the mode prop to the littlebutton-angular/src/app/app.component.html file:

<main>
<little-button mode=”primary”>Go</little-button>
</main>

Now we need to set up a binding to a classes getter to compute the correct classes based on if the mode was passed in to the component or not. Add this to littlebutton-angular/src/components/button.component.html (and note the binding is happening with the square brackets):

<button [class]=”classes”>Go</button>

Next, we actually need to create the classes binding in our component at littlebutton-angular/src/components/button.component.ts:

import { Component, Input } from ‘@angular/core’;

@Component({
selector: ‘little-button’,
templateUrl: ‘./button.component.html’,
styleUrls: [‘./button.component.css’],
})
export class ButtonComponent {
@Input() mode: ‘primary’ | undefined = undefined;

public get classes(): string {
const modeClass = this.mode ? `btn-${this.mode}` : ”;
return [
‘btn’,
modeClass,
].filter(cl => cl.length).join(‘ ‘);
}
}

We use the Input directive to take in the mode prop, then we create a classes accessor which adds the mode class if it’s been passed in.

Fire it up and look for the green button!

Code complete

If you’ve made it this far, congratulations — you’ve reached code complete! If something went awry, I’d encourage you to cross-reference the source code over at GitHub on the the-little-button-that-could-series branch. As bundlers and packages have a tendency to change abruptly, you might want to pin your package versions to the ones in this branch if you happen to experience any dependency issues.

Take a moment to go back and compare the four framework-based button component implementations we just built. They’re still small enough to quickly notice some interesting differences in how props get passed in, how we bind to props, and how CSS name collisions are prevented among other subtle differences. As I continue to add components to AgnosticUI (which supports these exact same four frameworks), I’m continually pondering which offers the best developer experience. What do you think?

Homework

If you’re the type that likes to figure things out on your own or enjoys digging in deeper, here are ideas.

Button states

The current button styles do not account for various states, like :hover. I believe that’s a good first exercise.

/* You should really implement the following states
but I will leave it as an exercise for you to
decide how to and what values to use.
*/
.btn:focus {
/* If you elect to remove the outline, replace it
with another proper affordance and research how
to use transparent outlines to support windows
high contrast
*/
}
.btn:hover { }
.btn:visited { }
.btn:active { }
.btn:disabled { }

Variants

Most button libraries support many button variations for things like sizes, shapes, and colors. Try creating more than the primary mode we already have. Maybe a secondary variation? A warning or success? Maybe filled and outline? Again, you can look at AgnosticUI’s buttons page for ideas.

CSS custom properties

If you haven’t started using CSS custom properties yet, I’d strongly recommend it. You can start by having a look at AgnosticUI’s common styles. I heavily lean on custom properties in there. Here are some great articles that cover what custom properties are and how you might leverage them:

A Complete Guide to Custom PropertiesA DRY Approach to Color Themes in CSS

Types

No… not typings, but the <button> element’s type attribute. We didn’t cover that in our component but there’s an opportunity to extend the component to other use cases with valid types, like button, submit, and reset. This is pretty easy to do and will greatly improve the button’s API.

More ideas

Gosh, you could do so much — add linting, convert it to Typescript, audit the accessibility, etc.

The current Svelte implementation is suffering from some pretty loose assumptions as we have no defense if the valid primary mode isn’t passed — that would produce a garbage CSS class:

mode ? `btn-${mode}` : “”,

You could say, “Well, .btn-garbage as a class isn’t exactly harmful.” But it’s probably a good idea to style defensively when and where possible.

Potential pitfalls

There are some things you should be aware of before taking this approach further:

Positional CSS based on the structure of the markup will not work well for the CSS Modules based techniques used here.Angular makes positional techniques even harder as it generates :host element representing each component view. This means you have these extra elements in between your template or markup structure. You’ll need to work around that.Copying styles across workspace packages is a bit of an anti-pattern to some folks. I justify it because I believe the benefits outweigh the costs; also, when I think about how monorepos use symlinks and (not-so-failproof) hoisting, I don’t feel so bad about this approach.You’ll have to subscribe to the decoupled techniques used here, so no CSS-in-JS.

I believe that all approaches to software development have their pros and cons and you ultimately have to decide if sharing a single CSS file across frameworks works for you or your specific project. There are certainly other ways you could do this (e.g. using littlebuttons-css as an npm package dependency) if needed.

Conclusion

Hopefully I’ve whet your appetite and you’re now really intrigued to create UI component libraries and/or design systems that are not tied to a particular framework. Maybe you have a better idea on how to achieve this — I’d love to hear your thoughts in the comments!

I’m sure you’ve seen the venerable TodoMVC project and how many framework implementations have been created for it. Similarly, wouldn’t it be nice to have a UI component library of primitives available for many frameworks? Open UI is making great strides to properly standardize native UI component defaults, but I believe we’ll always need to insert ourselves to some extent. Certainly, taking a good year to build a custom design system is quickly falling out of favor and companies are seriously questioning their ROI. Some sort of scaffolding is required to make the endeavor practical.

The vision of AgnosticUI is to have a relatively agnostic way to build design systems quickly that are not tied down to a particular frontend framework. If you’re compelled to get involved, the project is still very early and approachable and I’d love some help! Plus, you’re already pretty familiar with the how the project works now that you’ve gone through this tutorial!

How to Make a Component That Supports Multiple Frameworks in a Monorepo originally published on CSS-Tricks. You should get the newsletter and become a supporter.

Visit source: Post courtesy of : How to Make a Component That Supports Multiple Frameworks in a Monorepo

Categories
Article opinion user agent stylesheets

Should CSS Override Default Browser Styles?

CSS overrides can change the default look of almost anything: You can use CSS to override what a checkbox or radio button looks like, but if you don’t, the checkbox will look like a default checkbox on your operating system and some would say that’s best for accessibility and usability.You can use CSS to override…

Want to Read more ? CSS overrides can change the default look of almost anything:

You can use CSS to override what a checkbox or radio button looks like, but if you don’t, the checkbox will look like a default checkbox on your operating system and some would say that’s best for accessibility and usability.You can use CSS to override what a select menu looks like, but if you don’t, the select will look like a default select menu on your operating system and some would say that’s best for accessibility and usability.You can override what anchor links look like, but some would say they should be blue with underlines because that is the default and it’s best for accessibility and usability.You can override what scrollbars look like, but if you don’t, the scrollbars will look (and behave) the way default scrollbars do on your operating system, and some would say that’s best for accessibility and usability.

It just goes on and on…

You can customize what a button looks like, but…You can customize what the cursor looks like, or particular elements on your site, but…You can change the text highlighting color, you can change the accent color, heck, soon you’ll be able to customize what spelling and grammer mistakes look like in editable text areas, but…

Where do you draw the line?

In my experience, everyone has a different line. Nearly everybody styles their buttons. Nearly everybody styles their links, but some might only customize the hue of blue and leave the underline, drawing the line at more elaborate changes. It’s fairly popular to style form elements like checkboxes, radio buttons, and selects, but some people draw the line before that.

Some people draw a line saying you should never change a default cursor, some push that line back to make the cursor into a pointer for created interactive elements, some push that line so far they are OK with custom images as cursors. Some people draw the line with scrollbars saying they should never be customized, while some people implement elaborate designs.

CSS is a language for changing the design of websites. Every ruleset you write likely changes the defaults of something. The lines are relatively fuzzy, but I’d say there is nothing in CSS that should be outright banned from use — it’s more about the styling choices you make. So when you do choose to style something, it remains usable and accessible. Heck, background-color can be terribly abused making for inaccessible and unusable areas of a site, but nobody raises pitchforks over that.

Should CSS Override Default Browser Styles? originally published on CSS-Tricks. You should get the newsletter and become a supporter.

Visit source: Post courtesy of : Should CSS Override Default Browser Styles?

Categories
Article chrome links text-decoration

CSS Underlines Are Too Thin and Too Low in Chrome

I’ve encountered two bugs in Chrome while testing the new CSS text-decoration-thickness and text-underline-offset properties, and I want to share them with you here in this article. But first, let’s acknowledge one thing: Default underlines are inconsistent Let’s add a text link to a plain web page, set its font-family to Arial, and compare the…

Want to Read more ? I’ve encountered two bugs in Chrome while testing the new CSS text-decoration-thickness and text-underline-offset properties, and I want to share them with you here in this article.

But first, let’s acknowledge one thing:

Default underlines are inconsistent

Let’s add a text link to a plain web page, set its font-family to Arial, and compare the underlines across browsers and operating systems.

From left to right: Chrome, Safari, and Firefox on macOS; Safari on iOS; Chrome, and Firefox on Windows; Chrome, and Firefox on Android.

As you can see, the default underline is inconsistent across browsers. Each browser chooses their own default thickness and vertical position (offset from the baseline) for the underline. This is in line with the CSS Text Decoration module, which specifies the following default behavior (auto value):

The user agent chooses an appropriate thickness for text decoration lines. […] The user agent chooses an appropriate offset for underlines.

Luckily, we can override the browsers’ defaults

There are two new, widely supported CSS properties that allow us to precisely define the thickness and offset for our underlines:

text-decoration-thicknesstext-underline-offset

With these properties, we can create consistent underlines even across two very different browsers, such as the Gecko-based Firefox on Android and the WebKit-based Safari on macOS.

h1 {
text-decoration: underline;
text-decoration-thickness: 0.04em;
text-underline-offset: 0.03em;
}

Top row: the browsers’ default underlines; bottom row: consistent underlines with CSS. (Demo)

Note: The text-decoration-thickness property also has a special from-font value that instructs browsers to use the font’s own preferred underline width, if available. I tested this value with a few different fonts, but the underlines were inconsistent.

OK, so let’s move on to the two Chrome bugs I noted earlier.

Chrome bug 1: Underlines are too thin on macOS

If you set the text-decoration-thickness property to a font-relative length value that computes to a non-integer pixel value, Chrome will “floor” that value instead of rounding it to the nearest integer. For example, if the declared thickness is 0.06em, and that computes to 1.92px, Chrome will paint a thickness of 1px instead of 2px. This issue is limited to macOS.

a {
font-size: 2em; /* computes to 32px */
text-decoration-thickness: 0.06em; /* computes to 1.92px */
}

In the following screenshot, notice how the text decoration lines are twice as thin in Chrome (third row) than in Safari and Firefox.

From top to bottom: Safari, Firefox, and Chrome on macOS. (Demo)

For more information about this bug, see Chromium issue #1255280.

Chrome bug 2: Underlines are too low

The text-underline-offset property allows us to precisely set the distance between the alphabetic baseline and the underline (the underline’s offset from the baseline). Unfortunately, this feature is currently not implemented correctly in Chrome and, as a result, the underline is positioned too low.

h1 {
text-decoration: underline;
text-decoration-color: #f707;

/* disable “skip ink” */
-webkit-text-decoration-skip: none; /* Safari */
text-decoration-skip-ink: none;

/* cover the entire descender */
text-decoration-thickness: 0.175em; /* descender height */
text-underline-offset: 0; /* no offset from baseline */
}

Because of this bug, it is not possible to move the underline all the way up to the baseline in Chrome.

From left to right: Safari, Firefox, and Chrome on macOS. View this demo on CodePen.

For more information about this bug, see Chromium issue #1172623.

Note: As you might have noticed from the image above, Safari draws underlines on top of descenders instead of beneath them. This is a WebKit bug that was fixed very recently. The fix should ship in the next version of Safari.

Help prioritize the Chrome bugs

The two new CSS properties for styling underlines are a welcome addition to CSS. Hopefully, the two related Chrome bugs will be fixed sooner rather than later. If these CSS features are important to you, make your voice heard by starring the bugs in Chromium’s bug tracker.

Sign in with your Google account and click the star button on issues #1172623 and #1255280.

CSS Underlines Are Too Thin and Too Low in Chrome originally published on CSS-Tricks. You should get the newsletter and become a supporter.

Visit source: Post courtesy of : CSS Underlines Are Too Thin and Too Low in Chrome

Categories
Article Sponsored

Jetpack Features We Love and Use at CSS-Tricks

(This is a sponsored post.) We use and love Jetpack around here. It’s a WordPress plugin that brings a whole suite of functionality to your site ranging from security to marketing with lots of ridiculously useful stuff in between! Here’s our favorite features around here. Powerful Search Jetpack’s Search feature gives your site an incredibly powerful…

Want to Read more ? (This is a sponsored post.)
We use and love Jetpack around here. It’s a WordPress plugin that brings a whole suite of functionality to your site ranging from security to marketing with lots of ridiculously useful stuff in between! Here’s our favorite features around here.

Powerful Search

Jetpack’s Search feature gives your site an incredibly powerful search engine with the flip of a switch. You get a very fast, truly intelligent search for your entire site that is easily sortable and filterable with literally zero work on your part. You can’t rely on default WordPress search — this is a must-have. Bonus: it’s all handled offsite, so there is minimal burden on your server.

Read More

Backups & Activity

We sleep easy knowing CSS-Tricks is entirely backed up in real-time. Everything is backed up from the site’s content, comments, settings, theme files, media, even WordPress itself.

An activity log shows off everything that happens on the site, and I use that same log to restore the site to any point in time.

Read More

Performant Media

There are at least four things you have to do with images on websites to make sure you’re serving them in a performance responsible way: (1) use the responsive images syntax to serve an appropriately sized version, (2) optimize the image, (3) lazy load the image, and (4) serve the image from a CDN. Fortunately, WordPress itself helps with #1, which can be tricky. Jetpack helps with the others with the flip of a switch.

Read More

And don’t forget about video! VideoPress does even more for your hosted videos. No ads, beautiful feature-rich player, CDN-hosted optimized video, poster graphics for mobile, and you do absolutely nothing different with your workflow: just drag and drop videos into posts.

Markdown

Writing content in Markdown can be awful handy. Especially on a developer-focused site, it makes sense to offer it to users in the comment section.

With Jetpack Markdown, you also get a Markdown block to use in the block editor so you can write in chunks of Markdown wherever needed.

Read More

Related Posts

CSS-Tricks has thousands of pages of content! It’s a challenge for us to surface all the best stuff, particularly on a per-topic basis and without having to hand-pick everything. Showing related posts is tricky to pull off and we love that Jetpack does a great job with it, all without burdening our servers the way other related content solutions can.

Read More

Social Connections

We like to tell the world as best as we can when we publish new content. Rather than having to do that manually, we can share the post to Twitter and Facebook the second we hit that “Publish” button. You can always head back to older content and re-publish to social media as well.

Read More

This isn’t a complete list. The official features page will show you even more. Every site’s needs will be different. There are all sorts of security, design, and promotion features that might be your favorites. If you manage a lot of WordPress sites, as agencies often too, take note there is a new Licensing Portal to manage billing across multiple sites much more easily.

Jetpack Features We Love and Use at CSS-Tricks originally published on CSS-Tricks. You should get the newsletter and become a supporter.

Visit source: Post courtesy of : Jetpack Features We Love and Use at CSS-Tricks

Categories
Article security

CSS-Based Fingerprinting

Fingerprinting is bad. It’s a term that refers to building up enough metadata about a user that you can essentially figure out who they are. JavaScript has access to all sorts of fingerprinting possibilities, which then combined with the IP address that the server has access to, means fingerprinting is all too common. You don’t…

Want to Read more ? Fingerprinting is bad. It’s a term that refers to building up enough metadata about a user that you can essentially figure out who they are. JavaScript has access to all sorts of fingerprinting possibilities, which then combined with the IP address that the server has access to, means fingerprinting is all too common.

You don’t generally think of CSS as being a fingerprinting vector though, and thus “safe” in that way. But Oliver Brotchie has documented an idea that allows for some degree of fingerprinting with CSS alone.

Think of all the @media queries we have. We can test for pointer type with any-pointer. Imagine that for each value, we request a totally unique background-image from a server. If that image was requested, we know those @media queries were true. We can start to fingerprint with something like this:

.pointer {
background-image: url(‘/unique-id/pointer=none’)
}

@media (any-pointer: coarse) {
.pointer {
background-image: url(‘/unique-id/pointer=coarse’)
}
}

@media (any-pointer: fine) {
.pointer {
background-image: url(‘/unique-id/pointer=fine’)
}
}

Combine that with the fact that we can test for a dark mode preference with prefers-color-scheme, the fingerprint gets a bit clearer. In fact, it’s the current draft for CSS user prefer media queries that Oliver is most concerned about:

Not only will the upcoming draft make this method scalable, but it will also increase its precision. Currently, without alternative means, it is hard to conclusively link every request to a specific visitor as the only feasible way to determine their origin, is to group the requests by the IP address of the connection. However, with the new draft, by generating a randomised string and interpolating it into the URL tag for every visitor, we can accurately identify all requests from said visitor.

There are tons more. We can make media queries that are 1px apart and request a background image for each, perfectly guessing the visitor’s window size. There are probably a dozen or more exotic media queries that are rarely used, but are useful specifically to fingerprinting with CSS. Combine that with @supports queries for all sorts of things to essentially guess the exact browser. And combine that with the classic technique of testing for installation of specific local fonts, and you have a half-decent fingerprinting machine.

@font-face {
font-family: ‘some-font’;
src: local(some font), url(‘/unique-id/some-font’);
}

.some-font {
font-family:’some-font’;
}

The generated CSS to do it is massive (here’s the Sass to generate it), but apparently it’s heavily reduced once we can use custom properties in URLs.

I’m not heavily worried about it, mostly because I don’t disable JavaScript and JavaScript is so much more widely capable of fingerprinting already. Plus, there are already other types of CSS security vulnerabilities, from reading visited links (which browsers have addressed), keylogging, and user-generated inline styles, among others that folks have pointed out in another article on the topic.

But Oliver’s research on fingerprinting is really good and worthy of a look by everyone who knows more about web security than I do.

CSS-Based Fingerprinting originally published on CSS-Tricks. You should get the newsletter and become a supporter.

Visit source: Post courtesy of : CSS-Based Fingerprinting

Categories
Article browser extension

How to Create a Browser Extension

I’ll bet you are using browser extensions right now. Some of them are extremely popular and useful, like ad blockers, password managers, and PDF viewers. These extensions (or “add-ons”) are not limited to those purposes — you can do a lot more with them! In this article, I will give you an introduction on how…

Want to Read more ? I’ll bet you are using browser extensions right now. Some of them are extremely popular and useful, like ad blockers, password managers, and PDF viewers. These extensions (or “add-ons”) are not limited to those purposes — you can do a lot more with them! In this article, I will give you an introduction on how to create one. Ultimately, we’ll make it work in multiple browsers.

What we’re making

We’re making an extension called “Transcribers of Reddit” and it’s going to improve Reddit’s accessibility by moving specific comments to the top of the comment section and adding aria- attributes for screen readers. We will also take our extension a little further with options for adding borders and backgrounds to comments for better text contrast.

The whole idea is that you’ll get a nice introduction for how to develop a browser extension. We will start by creating the extension for Chromium-based browsers (e.g. Google Chrome, Microsoft Edge, Brave, etc.). In a future post we will port the extension to work with Firefox, as well as Safari which recently added support for Web Extensions in both the MacOS and iOS versions of the browser.

GitHub repo

Ready? Let’s take this one step at a time.

Create a working directory

Before anything else, we need a working space for our project. All we really need is to create a folder and give it a name (which I’m calling transcribers-of-reddit). Then, create another folder inside that one named src for our source code.

Define the entry point

The entry point is a file that contains general information about the extension (i.e. extension name, description, etc.) and defines permissions or scripts to execute.

Our entry point can be a manifest.json file located in the src folder we just created. In it, let’s add the following three properties:

{
“manifest_version”: 3,
“name”: “Transcribers of Reddit”,
“version”: “1.0”
}

The manifest_version is similar to version in npm or Node. It defines what APIs are available (or not). We’re going to work on the bleeding edge and use the latest version, 3 (also known as as mv3).

The second property is name and it specifies our extension name. This name is what’s displayed everywhere our extension appears, like Chrome Web Store and the chrome://extensions page in the Chrome browser.

Then there’s version. It labels the extension with a version number. Keep in mind that this property (in contrast to manifest_version) is a string that can only contain numbers and dots (e.g. 1.3.5).

More manifest.json information

There’s actually a lot more we can add to help add context to our extension. For example, we can provide a description that explains what the extension does. It’s a good idea to provide these sorts of things, as it gives users a better idea of what they’re getting into when they use it.

In this case, we’re not only adding a description, but supplying icons and a web address that Chrome Web Store points to on the extension’s page.

{
“description”: “Reddit made accessible for disabled users.”,
“icons”: {
“16”: “images/logo/16.png”,
“48”: “images/logo/48.png”,
“128”: “images/logo/128.png”
},
“homepage_url”: “https://lars.koelker.dev/extensions/tor/”
}

The description is displayed on Chrome’s management page (chrome://extensions) and should be brief, less than 132 characters.The icons are used in lots of places. As the docs state, it’s best to provide three versions of the same icon in different resolutions, preferably as a PNG file. Feel free to use the ones in the GitHub repository for this example.The homepage_url can be used to connect your website with the extension. A button including the link will be displayed when clicking on “More details” on the management page.

Our opened extension card inside the extension management page.

Setting permissions

One major advantage extensions have is that their APIs allow you to interact directly with the browser. But we have to explicitly give the extension those permissions, which also goes inside the manifest.json file.

{
“manifest_version”: 3,
“name”: “Transcribers of Reddit”,
“version”: “1.0”,
“description”: “Reddit made accessible for disabled users.”,
“icons”: {
“16”: “images/logo/16.png”,
“48”: “images/logo/48.png”,
“128”: “images/logo/128.png”
},
“homepage_url”: “https://lars.koelker.dev/extensions/tor/”,

“permissions”: [
“storage”,
“webNavigation”
]
}

What did we just give this extension permission to? First, storage. We want this extension to be able to save the user’s settings, so we need to access the browser’s web storage to hold them. For example, if the user wants red borders on the comments, then we’ll save that for next time rather than making them set it again.

We also gave the extension permission to look at how the user navigated to the current screen. Reddit is a single-page application (SPA) which means it doesn’t trigger a page refresh. We need to “catch” this interaction, as Reddit will only load the comments of a post if we click on it. So, that’s why we’re tapping into webNavigation.

We’ll get to executing code on a page later as it requires a whole new entry inside manifest.json.

/explanation Depending on which permissions are allowed, the browser might display a warning to the user to accept the permissions. It’s only certain ones, though, and Chrome has a nice outline of them.

Managing translations

Browser extensions have a built-in internalization (i18n) API. It allows you to manage translations for multiple languages (full list). To use the API, we have to define our translations and default language right in the manifest.json file:

“default_locale”: “en”

This sets English as the language. In the event that a browser is set to any other language that isn’t supported, the extension will fall back to the default locale (en in this example).

Our translations are defined inside the _locales directory. Let’s create another folder in there each language you want to support. Each subdirectory gets its own messages.json file.

src
└─ _locales
└─ en
└─ messages.json
└─ fr
└─ messages.json

A translation file consists of multiple parts:

Translation key (“id”): This key is used to reference the translation.Message: The actual translation contentDescription (optional): Describes the translation (I wouldn’t use them, they just bloat up the file and your translation key should be descriptive enough)Placeholders (optional): Can be used to insert dynamic content inside a translation

Here’s an example that pulls all that together:

{
“userGreeting”: { // Translation key (“id”)
“message”: “Good $daytime$, $user$!” // Translation
“description”: “User Greeting”, // Optional description for translators
“placeholders”: { // Optional placeholders
“daytime”: { // As referenced inside the message
“content”: “$1”,
“example”: “morning” // Example value for our content
},
“user”: {
“content”: “$1”,
“example”: “Lars”
}
}
}
}

Using placeholders is a bit more challenging. At first we need to define the placeholder inside the message. A placeholder needs to be wrapped in between $ characters. Afterwards, we have to add our placeholder to the “placeholder list.” This is a bit unintuitive, but Chrome wants to know what value should be inserted for our placeholders. We (obviously) want to use a dynamic value here, so we use the special content value $1 which references our inserted value.

The example property is optional. It can be used to give translators a hint what value the placeholder could be (but is not actually displayed).

We need to define the following translations for our extension. Copy and paste them into the messages.json file. Feel free to add more languages (e.g. if you speak German, add a de folder inside _locales, and so on).

{
“name”: {
“message”: “Transcribers of Reddit”
},
“description”: {
“message”: “Accessible image descriptions for subreddits.”
},
“popupManageSettings”: {
“message”: “Manage settings”
},
“optionsPageTitle”: {
“message”: “Settings”
},
“sectionGeneral”: {
“message”: “General settings”
},
“settingBorder”: {
“message”: “Show comment border”
},
“settingBackground”: {
“message”: “Show comment background”
}
}

You might be wondering why we registered the permissions when there is no sign of an i18n permission, right? Chrome is a bit weird in that regard, as you don’t need to register every permission. Some (e.g. chrome.i18n) don’t require an entry inside the manifest. Other permissions require an entry but won’t be displayed to the user when installing the extension. Some other permissions are “hybrid” (e.g. chrome.runtime), meaning some of their functions can be used without declaring a permission—but other functions of the same API require one entry in the manifest. You’ll want to take a look at the documentation for a solid overview of the differences.

Using translations inside the manifest

The first thing our end user will see is either the entry inside the Chrome Web Store or the extension overview page. We need to adjust our manifest file to make sure everything os translated.

{
// Update these entries
“name”: “__MSG_name__”,
“description”: “__MSG_description__”
}

Applying this syntax uses the corresponding translation in our messages.json file (e.g. _MSG_name_ uses the name translation).

Using translations in HTML pages

Applying translations in an HTML file takes a little JavaScript.

chrome.i18n.getMessage(‘name’);

That code returns our defined translation (which is Transcribers of Reddit). Placeholders can be done in a similar way.

chrome.i18n.getMessage(‘userGreeting’, {
daytime: ‘morning’,
user: ‘Lars’
});

It would be a pain in the butt to apply translations to all elements this way. But we can write a little script that performs the translation based on a data- attribute. So, let’s create a new js folder inside the src directory, then add a new util.js file in it.

src
└─ js
└─ util.js

This gets the job done:

const i18n = document.querySelectorAll(“[data-intl]”);
i18n.forEach(msg => {
msg.innerHTML = chrome.i18n.getMessage(msg.dataset.intl);
});

chrome.i18n.getAcceptLanguages(languages => {
document.documentElement.lang = languages[0];
});

Once that script is added to an HTML page, we can add the data-intl attribute to an element to set its content. The document language will also be set based on the user language.

<!– Before JS execution –>
<html>
<body>
<button data-intl=”popupManageSettings”></button>
</body>
</html>

<!– After JS execution –>
<html lang=”en”>
<body>
<button data-intl=”popupManageSettings”>Manage settings</button>
</body>
</html>

Adding a pop-up and options page

Before we dive into actual programming, we we need to create two pages:

An options page that contains user settingsA pop-up page that opens when interacting with the extension icon right next to our address bar. This page can be used for various scenarios (e.g. for displaying stats or quick settings).

The options page containg our settings.

The pop-up containg a link to the options page.

Here’s an outline of the folders and files we need in order to make the pages:

src
├─ css
| └─ paintBucket.css
├─ popup
| ├─ popup.html
| ├─ popup.css
| └─ popup.js
└─ options
├─ options.html
├─ options.css
└─ options.js

The .css files contain plain CSS, nothing more and nothing less. I won’t into detail because I know most of you reading this are already fully aware of how CSS works. You can copy and paste the styles from the GitHub repository for this project.

Note that the pop-up is not a tab and that its size depends on the content in it. If you want to use a fixed popup size, you can set the width and height properties on the html element.

Creating the pop-up

Here’s an HTML skeleton that links up the CSS and JavaScript files and adds a headline and button inside the <body>.

<!doctype html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0″>
<meta http-equiv=”X-UA-Compatible” content=”ie=edge”>
<title data-intl=”name”></title>

<link rel=”stylesheet” href=”../css/paintBucket.css”>
<link rel=”stylesheet” href=”popup.css”>

<!– Our “translation” script –>
<script src=”../js/util.js” defer></script>
<script src=”popup.js” defer></script>
</head>
<body>
<h1 id=”title”></h1>
<button data-intl=”popupManageSettings”></button>
</body>
</html>

The h1 contains the extension name and version; the button is used to open the options page. The headline will not be filled with a translation (because it lacks a data-intl attribute), and the button doesn’t have any click handler yet, so we need to populate our popup.js file:

const title = document.getElementById(‘title’);
const settingsBtn = document.querySelector(‘button’);
const manifest = chrome.runtime.getManifest();

title.textContent = `${manifest.name} (${manifest.version})`;

settingsBtn.addEventListener(‘click’, () => {
chrome.runtime.openOptionsPage();
});

This script first looks for the manifest file. Chrome offers the runtime API which contains the getManifest method (this specific method does not require the runtime permission). It returns our manifest.json as a JSON object. After we populate the title with the extension name and version, we can add an event listener to the settings button. If the user interacts with it, we will open the options page using chrome.runtime.openOptionsPage() (again no permission entry needed).

The pop-up page is now finished, but the extension doesn’t know it exists yet. We have to register the pop-up by appending the following property to the manifest.json file.

“action”: {
“default_popup”: “popup/popup.html”,
“default_icon”: {
“16”: “images/logo/16.png”,
“48”: “images/logo/48.png”,
“128”: “images/logo/128.png”
}
},

Creating the options page

Creating this page follows a pretty similar process as what we just completed. First, we populate our options.html file. Here’s some markup we can use:

<!doctype html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0″>
<meta http-equiv=”X-UA-Compatible” content=”ie=edge”>
<title data-intl=”name”></title>

<link rel=”stylesheet” href=”../css/paintBucket.css”>
<link rel=”stylesheet” href=”options.css”>

<!– Our “translation” script –>
<script src=”../js/util.js” defer></script>
<script src=”options.js” defer></script>
</head>
<body>
<header>
<h1>
<!– Icon provided by feathericons.com –>
<svg xmlns=”http://www.w3.org/2000/svg” viewBox=”0 0 24 24″ fill=”none” stroke=”currentColor” stroke-width=”1.2″ stroke-linecap=”round” stroke-linejoin=”round” role=”presentation”>
<circle cx=”12″ cy=”12″ r=”3″></circle>
<path d=”M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z”></path>
</svg>
<span data-intl=”optionsPageTitle”></span>
</h1>
</header>

<main>
<section id=”generalOptions”>
<h2 data-intl=”sectionGeneral”></h2>

<div id=”generalOptionsWrapper”></div>
</section>
</main>

<footer>
<p>Transcribers of Reddit extension by <a href=”https://lars.koelker.dev” target=”_blank”>lars.koelker.dev</a>.</p>
<p>Reddit is a registered trademark of Reddit, Inc. This extension is not endorsed or affiliated with Reddit, Inc. in any way.</p>
</footer>
</body>
</html>

There are no actual options yet (just their wrappers). We need to write the script for the options page. First, we define variables to access our wrappers and default settings inside options.js. “Freezing” our default settings prevents us from accidentally modifying them later.

const defaultSettings = Object.freeze({
border: false,
background: false,
});
const generalSection = document.getElementById(‘generalOptionsWrapper’);

Next, we need to load the saved settings. We can use the (previously registered) storage API for that. Specifically, we need to define if we want to store the data locally (chrome.storage.local) or sync settings through all devices the end user is logged in to (chrome.storage.sync). Let’s go with local storage for this project.

Retrieving values needs to be done with the get method. It accepts two arguments:

The entries we want to loadA callback containing the values

Our entries can either be a string (e.g. like settings below) or an array of entries (useful if we want to load multiple entries). The argument inside the callback function contains an object of all entries we previously defined in { settings: … }:

chrome.storage.local.get(‘settings’, ({ settings }) => {
const options = settings ?? defaultSettings; // Fall back to default if settings are not defined
if (!settings) {
chrome.storage.local.set({
settings: defaultSettings,
});
}

// Create and display options
const generalOptions = Object.keys(options).filter(x => !x.startsWith(‘advanced’));

generalOptions.forEach(option => createOption(option, options, generalSection));
});

To render the options, we also need to create a createOption() function.

function createOption(setting, settingsObject, wrapper) {
const settingWrapper = document.createElement(“div”);
settingWrapper.classList.add(“setting-item”);
settingWrapper.innerHTML = `
<div class=”label-wrapper”>
<label for=”${setting}” id=”${setting}Desc”>
${chrome.i18n.getMessage(`setting${setting}`)}
</label>
</div>

<input type=”checkbox” ${settingsObject[setting] ? ‘checked’ : ”} id=”${setting}” />
<label for=”${setting}”
tabindex=”0″
role=”switch”
aria-checked=”${settingsObject[setting]}”
aria-describedby=”${setting}-desc”
class=”is-switch”
></label>
`;

const toggleSwitch = settingWrapper.querySelector(“label.is-switch”);
const input = settingWrapper.querySelector(“input”);

input.onchange = () => {
toggleSwitch.setAttribute(‘aria-checked’, input.checked);
updateSetting(setting, input.checked);
};

toggleSwitch.onkeydown = e => {
if(e.key === ” ” || e.key === “Enter”) {
e.preventDefault();
toggleSwitch.click();
}
}

wrapper.appendChild(settingWrapper);
}

Inside the onchange event listener of our switch (aká radio button) we call the function updateSetting. This method will write the updated value of our radio button inside the storage.

To accomplish this, we will make use of the set function. It has two arguments: The entry we want to overwrite and an (optional) callback (which we don’t use in our case). As our settings entry is not a boolean or a string but an object containing different settings, we use the spread operator (…) and only overwrite our actual key (setting) inside the settings object.

function updateSetting(key, value) {
chrome.storage.local.get(‘settings’, ({ settings }) => {
chrome.storage.local.set({
settings: {
…settings,
[key]: value
}
})
});
}

Once again, we need to “inform” the extension about our options page by appending the following entry to the manifest.json:

“options_ui”: {
“open_in_tab”: true,
“page”: “options/options.html”
},

Depending on your use case you can also force the options dialog to open as a popup by setting open_in_tab to false.

Installing the extension for development

Now that we’ve successfully set up the manifest file and have added both the pop-up and options page to the mix, we can install our extension to check if our pages actually work. Navigate to chrome://extensions and enable “Developer mode.” Three buttons will appear. Click the one labeled “Load unpacked” and select the src folder of your extension to load it up.

The extension should now be successfully installed and our “Transcribers of Reddit” tile should be on the page.

We can already interact with our extension. Click on the puzzle piece (🧩) icon right next to the browser’s address bar and click on the newly-added “Transcribers of Reddit” extension. You should now be greeted by a small pop-up with the button to open the options page.

Lovely, right? It might look a bit different on your device, as I have dark mode enabled in these screenshots.

If you enable the “Show comment background” and “Show comment border” settings, then reload the page, the state will persist because we’re saving it in the browser’s local storage.

Adding the content script

OK, so we can already trigger the pop-up and interact with the extension settings, but the extension doesn’t do anything particularly useful yet. To give it some life, we will add a content script.

Add a file called comment.js inside the js directory and make sure to define it in the manifest.json file:

“content_scripts”: [
{
“matches”: [ “*://www.reddit.com/*” ],
“js”: [ “js/comment.js” ]
}
],

The content_scripts is made up of two parts:

matches: This array holds URLs that tell the browser where we want our content scripts to run. Being an extension for Reddit and all, we want this to run on any page matching ://www.redit.com/*, where the asterisk is a wild card to match anything after the top-level domain.js: This array contains the actual content scripts.

Content scripts can’t interact with other (normal) JavaScripts. This means if a website’s scripts defines a variable or function, we can’t access it. For example:

// script_on_website.js
const username = ‘Lars’;

// content_script.js
console.log(username); // Error: username is not defined

Now let’s start writing our content script. First, we add some constants to comment.js. These constants contain RegEx expressions and selectors that will be used later on. The CommentUtils is used to determine whether or not a post contains a “tor comment,” or if comment wrappers exists.

const messageTypes = Object.freeze({
COMMENT_PAGE: ‘comment_page’,
SUBREDDIT_PAGE: ‘subreddit_page’,
MAIN_PAGE: ‘main_page’,
OTHER_PAGE: ‘other_page’,
});

const Selectors = Object.freeze({
commentWrapper: ‘div[style*=”–commentswrapper-gradient-color”] > div, div[style*=”max-height: unset”] > div’,
torComment: ‘div[data-tor-comment]’,
postContent: ‘div[data-test-id=”post-content”]’
});

const UrlRegex = Object.freeze({
commentPage: //r/.*/comments/.*/,
subredditPage: //r/.*//
});

const CommentUtils = Object.freeze({
isTorComment: (comment) => comment.querySelector(‘[data-test-id=”comment”]’) ? comment.querySelector(‘[data-test-id=”comment”]’).textContent.includes(‘m a human volunteer content transcriber for Reddit’) : false,
torCommentsExist: () => !!document.querySelector(Selectors.torComment),
commentWrapperExists: () => !!document.querySelector(‘[data-reddit-comment-wrapper=”true”]’)
});

Next, we check whether or not a user directly opens a comment page (“post”), then perform a RegEx check and update the directPage variable. This case occurs when a user directly opens the URL (e.g. by typing it into the address bar or clicking on<a> element on another page, like Twitter).

let directPage = false;
if (UrlRegex.commentPage.test(window.location.href)) {
directPage = true;
moveComments();
}

Besides opening a page directly, a user normally interacts with the SPA. To catch this case, we can add a message listener to our comment.js file by using the runtime API.

chrome.runtime.onMessage.addListener(msg => {
if (msg.type === messageTypes.COMMENT_PAGE) {
waitForComment(moveComments);
}
});

All we need now are the functions. Let’s create a moveComments() function. It moves the special “tor comment” to the start of the comment section. It also conditionally applies a background color and border (if borders are enabled in the settings) to the comment. For this, we call the storage API and load the settings entry:

function moveComments() {
if (CommentUtils.commentWrapperExists()) {
return;
}

const wrapper = document.querySelector(Selectors.commentWrapper);
let comments = wrapper.querySelectorAll(`${Selectors.commentWrapper} > div`);
const postContent = document.querySelector(Selectors.postContent);

wrapper.dataset.redditCommentWrapper = ‘true’;
wrapper.style.flexDirection = ‘column’;
wrapper.style.display = ‘flex’;

if (directPage) {
comments = document.querySelectorAll(“[data-reddit-comment-wrapper=’true’] > div”);
}

chrome.storage.local.get(‘settings’, ({ settings }) => { // HIGHLIGHT 18
comments.forEach(comment => {
if (CommentUtils.isTorComment(comment)) {
comment.dataset.torComment = ‘true’;
if (settings.background) {
comment.style.backgroundColor = ‘var(–newCommunityTheme-buttonAlpha05)’;
}
if (settings.border) {
comment.style.outline = ‘2px solid red’;
}
comment.style.order = “-1″;
applyWaiAria(postContent, comment);
}
});
})
}

The applyWaiAria() function is called inside the moveComments() function—it adds aria- attributes. The other function creates a unique identifier for use with the aria- attributes.

function applyWaiAria(postContent, comment) {
const postMedia = postContent.querySelector(‘img[class*=”ImageBox-image”], video’);
const commentId = uuidv4();

if (!postMedia) {
return;
}

comment.setAttribute(‘id’, commentId);
postMedia.setAttribute(‘aria-describedby’, commentId);
}

function uuidv4() {
return ‘xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx’.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == ‘x’ ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}

The following function waits for the comments to load and calls the callback parameter if it finds the comment wrapper.

function waitForComment(callback) {
const config = { childList: true, subtree: true };
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
if (document.querySelector(Selectors.commentWrapper)) {
callback();
observer.disconnect();
clearTimeout(timeout);
break;
}
}
});

observer.observe(document.documentElement, config);
const timeout = startObservingTimeout(observer, 10);
}

function startObservingTimeout(observer, seconds) {
return setTimeout(() => {
observer.disconnect();
}, 1000 * seconds);
}

Adding a service worker

Remember when we added a listener for messages inside the content script? This listener isn’t currently receiving messages. We need to send it to the content script ourselves. For this purpose we need to register a service worker.

We have to register our service worker inside the manifest.json by appending the following code to it:

“background”: {
“service_worker”: “sw.js”
}

Don’t forget to create the sw.js file inside the src directory (service workers always need to be created in the extension’s root directory, src.

Now, let’s create some constants for the message and page types:

const messageTypes = Object.freeze({
COMMENT_PAGE: ‘comment_page’,
SUBREDDIT_PAGE: ‘subreddit_page’,
MAIN_PAGE: ‘main_page’,
OTHER_PAGE: ‘other_page’,
});

const UrlRegex = Object.freeze({
commentPage: //r/.*/comments/.*/,
subredditPage: //r/.*//
});

const Utils = Object.freeze({
getPageType: (url) => {
if (new URL(url).pathname === ‘/’) {
return messageTypes.MAIN_PAGE;
} else if (UrlRegex.commentPage.test(url)) {
return messageTypes.COMMENT_PAGE;
} else if (UrlRegex.subredditPage.test(url)) {
return messageTypes.SUBREDDIT_PAGE;
}

return messageTypes.OTHER_PAGE;
}
});

We can add the service worker’s actual content. We do this with an event listener on the history state (onHistoryStateUpdated) that detects when a page has been updated with the History API (which is commonly used in SPAs to navigate without a page refresh). Inside this listener, we query the active tab and extract its tabId. Then we send a message to our content script containing the page type and URL.

chrome.webNavigation.onHistoryStateUpdated.addListener(async ({ url }) => {
const [{ id: tabId }] = await chrome.tabs.query({ active: true, currentWindow: true });

chrome.tabs.sendMessage(tabId, {
type: Utils.getPageType(url),
url
});
});

All done!

We’re finished! Navigate to Chrome’s extension management page ( chrome://extensions) and hit the reload icon on the unpacked extension. If you open a Reddit post that contains a “Transcribers of Reddit” comment with an image transcription (like this one), it will be moved to the start of the comment section and be highlighted as long as we’ve enabled it in the extension settings.

The “Transcribers of Reddit” extension highlights a particular comment by moving it to the top of the Reddit thread’s comment list and giving it a bright red border

Conclusion

Was that as hard as you thought it would be? It’s definitely a lot more straightforward than I thought before digging in. After setting up the manifest.json and creating any page files and assets we need, all we’re really doing is writing HTML, CSS, and JavaScript like normal.

If you ever find yourself stuck along the way, the Chrome API documentation is a great resource to get back on track.

Once again, here’s the GitHub repo with all of the code we walked through in this article. Read it, use it, and let me know what you think of it!

How to Create a Browser Extension originally published on CSS-Tricks. You should get the newsletter and become a supporter.

Visit source: Post courtesy of : How to Create a Browser Extension

Categories
Article thank-you

Thank You (2021 Edition)

The year has come to a close and it’s time again for our end-of-year wrapup. The most important message is this: thank you. (thankyouthankyou) Thanks for stopping by and reading this site. If you didn’t, I’d be out of a job around here, and I quite like this job so I owe it all to…

Want to Read more ? The year has come to a close and it’s time again for our end-of-year wrapup. The most important message is this: thank you. (thankyouthankyou)

Thanks for stopping by and reading this site. If you didn’t, I’d be out of a job around here, and I quite like this job so I owe it all to you. Like a family holiday card, allow me to share how the year went from our perspective, both with numbers and feelings, and then do a review of our goals.

Overall Traffic Analytics Numbers

The site saw 88m pageviews this year down 6% from the 93m last year. Traffic has yo-yo’d up and down a smidge like that a little over the last 4-5 years, but this 6% is a bit of an alarming drop that I don’t like to see. These numbers are from Google Analytics, and some of my own research this year suggests perhaps 20-30% of visitors to this site actually block the run-of-the-mill client-side JavaScript-powered Google Analytics I use. So perhaps the real traffic is higher, but as the analytics implementation is exactly the same and I don’t see any reason blocking would have skyrocketed just this past year alone, the downward movement seems real.

A ~3% drop in organic search traffic was largely responsible for the dip. That’s big, as search is 74.6% of all traffic. This points to us just not hitting the mark well enough for what people are searching for. A nice 36% increase in direct traffic points to pretty decent brand awareness, but direct traffic is only 5% of overall traffic anyway so it doesn’t make much of a difference compared to search engine traffic. Referral traffic is down, social is up, but both are such small slices right now they just don’t move the needle.

You might think, well hey content ages out, search engine traffic to existing content will decline over time. That’s true, but we publish a ton of new content every year as well as maintain and improve existing content, hence the concern.

We invest well into 6-figures in new and updated content every year. So seeing a decline in traffic is disheartening.

But hey that’s the game sometimes. I suspect it’s heavy competition in the developer writing space, which is something we all benefit from as developers, so it ain’t all bad. We’ll live and learn and do our best to turn it around for the sake of the health of this site. I’ve already got (counts fingers and toes) a million ideas.

All that said, while I do think pageviews is an interesting and relevant metric to a site that uses advertising as a primary business model, there are many others. Unique Visitors are up year over year to 26.3m from 25.8m, suggesting more different people came to the site this year, which is great, they just didn’t bop around the site as much or come back quite as often. Pages per visit is very steady at 1.35 meaning for the most part people come, they read, they leave. No surprise there. It’s mostly that “come back” thing to work on.

The Biggest Leap in Mobile Traffic Yet

Pretty big jump in mobile usage this year!

2021: 20%2020: 15%2019: 15%2018: 12%

A fifth of all traffic is pretty interesting. Before 2018, even though mobile traffic was surging then too, we were in the low single digits, which I always thought hey this is a reference site for coding and people code on desktop. But clearly, that’s changing and perhaps people are reading the site in a more news kinda way, which I like. For years I had goals of making this site both full of referential long-green content and a site you could subscribe to for news, like an industry rag. So far so good.

Content by the Numbers

You’d think if we missed the mark on new content this year, that perhaps some better year would beat articles-written-in-2021 in traffic, but that’s not the case. Articles written in 2021 drove the most traffic to the site in 2021 (13.5% of overall traffic). Here are the articles that were top-by-pageviews in 2021 that were written in 2021:

VS Code Extensions for HTML — Chris CoyierHow to Create Neon Text With CSS — Silvia O’DwyerAnimating with Lottie — Idorenyin Udoh Did You Know About the :has CSS Selector? — Robin RendleA table with both a sticky header and a sticky first column — Chris CoyierComparing the New Generation of Build Tools — Hugh Haworth Mistakes I’ve Made as an Engineering Manager — Sarah Drasner Let’s Create a Custom Audio Player — Idorenyin Udoh HTML Inputs and Labels: A Love Story — Amber WilsonFront-End Testing is For Everyone — Evgeny Klimenchenko

I almost shouldn’t post these lists! Look at what happens to Daniel Aleksandersen.

Those articles above range from 100k pageviews to 71k pageviews. What’s interesting is that if you group together all posts that got 40k or more pageviews, there are 44 of them, putting them at about 2.5-3m pageviews. That’s kinda cool I think — the “medium tail” of content is pretty thick around here. The flexbox guide page alone did 6.7m pageviews, so that’s still a beast, but it is bested by all content published in 2021 which clocks in at 11.8m. So investing in content works, it just needs to get tuned such that we aren’t dropping overall. Perhaps that means SEO tuning of both new content and old.

Here’s 11-20 from 2021 just for fun:

To the brain, reading computer code is not the same as reading language — Geoff Graham In Praise of the Unambiguous Click Menu — Mark Root-Wiley aspect-ratio — Geoff Graham Theming and Theme Switching with React and styled-components — Tapas Adhikary The Holy Grail Layout with CSS Grid — Chris CoyierCreating the Perfect Commit in Git — Tobias Günther What if… you could use Visual Studio Code as the editor of in-browser Developer Tools? — Geoff Graham Is CSS a Programming Language? — Chris CoyierA Love Letter to HTML & CSS — Ashley Kolodziej JSON in CSS — Chris Coyier

And here’s the top 10 regardless of year, but still scoped to traffic-in-2021:

A Complete Guide to FlexboxA Complete Guide to GridPerfect Full Page Background ImageUsing SVGThe Shapes of CSSMedia Queries for Standard Devicesbox-shadowCSS TriangleHow to use @font-face in CSSHow to Scale SVG

I like seeing the Almanac not only perform pretty well overall but have some individual pages be top-performers on their own.

Comments

We had about 4,320 legit comments on the site this year, almost exactly the number from last year. Weird!

That seems like a lot, especially as we approve… I’d say half?… of commenters that are left. There is a lot of junk posts (e.g. “good post!” kinda stuff, that we just don’t post as to not bother the author with useless email notifications of new comments, nor readers with useless content). We just delete those junk posts (as in, not approve them in the first place).

There is spam too of course. We crossed the 2m spam comments threshold, but through a combination of Akismet and Anti-Spam not too much spam sneaks through and is easily trashed before approval.

Mentally, I really rollercoaster on comments. Sometimes they are great and helpful. Sometimes they are full of rudeness, hate, and anger. Those need to be looked at and trashed, meaning comments represent an entry point into my brain for all that negativity. Part of me thinks we should just shut them off, and if people have something important to say, we can encourage them to use their own blog (it ain’t hard to spin one up!) to comment and we’ll link to it if it’s good.

But then I think of all the helpful comments and comments that help keep me up to date. Heck I just learned that Chrome is postponing all that removal of alert() stuff via a comment from Kyle, and I probably would have missed that otherwise. Plus the fact that there are 4,320 of them this year that pass muster feels like the scale is tipped toward keeping them.

Newsletter

We’re at about 91,000 newsletter subscribers as this year wraps, up from 81,000 last year. A respectable march forward and makes it likely we’ll hit that 100k milestone sometime in 2022.

Huge props to Robin for leading up the newsletter with wonderful writing. I think he really found a voice and stride on the newsletter this year.

We didn’t miss a single week. Part of what helps there is that they have sponsors so there is some clear obligation to get them out on time, but I think it’s more like we have a system and the system works.

I’d really like to juice up newsletter subscriptions moreso because I think it’s actually a darn nice weekly read than for any specific business reason.

Video

Thanks to Dave’s idea that we get ShopTalk more into video, we’ve been using the CSS-Tricks YouTube channel and thus had a banner year in publishing video! 35 brand new videos!

Site Updates

The design evolved a bit this year, but nothing overly dramatic. Normally this time of year my fingers are itching for a new design, and believe me there are Figma drafts cooking, but I just haven’t had the time or inspiration for a true v19 just yet.

So no major changes to the tech behind the site, but plenty of minor ones. For instance:

The Yoast SEO plugin was giving me problems. It had super frequent updates, which I guess is good, but there was a high frequency of problems with the updates where either the core plugin or the pro plugin wouldn’t update correctly (up to causing such problems as literally taking down the site) and settings getting messed up during updates. For a while I just turned it off entirely. But then I started hearing good things about RankMath so I’m trying that, and so far so good. It’s got me kinda inspired to take content SEO more seriously. Yoast had some claws in the site in other ways, for example it provides a pretty nice Table of Contents block that I’m still searching for a solution for (maybe it’s coming to core?). It also had pretty nice breadcrumbs, and had to switch over to Breadcrumb NavXT.Jetpack Boost is new to the site this year, and I’m impressed at how it handles critical CSS. Jetpack (full disclosure: a long time sponsor) is generally extremely helpful. I particularly like how the site search works, which is just out-of-the-box Jetpack Instant Search.We really dialed in the social media images this year. We also dialed in the eCommerce situation. The MVP Supporter membership unlocks additional content on the site, which I can now provide in eBook formats. So I’m really all set to produce more of that type of content.

Goal Review

🚫 Publish Three Guides. I thought this would be easy since last year our goal was 2 guides and we published 9! But this year we only managed one: A Complete Guide to Custom Properties. We did publish some other pretty big series like Tobias Günther’s 9-part Advanced Git series and four more entries in Jay Hoffman’s Web History series.

🚫 Stay focused on how-to technical content around our strengths. Kind of a close call here. It’s not like we didn’t publish quite a bit of how-to technical content. But I’m going to say we failed because I don’t think we kept this in mind strongly enough throughout the year. We didn’t say “we’re good at this type of content so we’re going to lean into that specifically” like this goal suggested we should.

🚫 Complete all missing Almanac entries. I hate marking this as failed, but I’m only doing that because of how it was worded with “all”. I think I had in mind that there was a really clear finite number of Alamanc articles to finish and we just had to do that. I think it’s a lot more wishy-washy than that, partially because of editorial choices (do you publish a unique entry for every single logical property or group them, for example).

But also, should we build an SVG-specific section? Should we have a new section for all the @at rules? It’s hard to say when the Almanac would be “complete”, so I’d just rather not. This page really needs a cleanup, but it’s got many ideas in there for more work that needs to be done/commissioned if anyone is so inclined.

We did do a pretty good job on publishing new entries though — more than any relatively recent year!

Almanac EntryPublishedscale2021-11-10translate2021-11-09rotate2021-11-08mask-border2021-11-03padding-inline2021-09-23overscroll-behavior2021-09-14border-block2021-09-02outline-color2021-09-01accent-color2021-08-26block-size2021-08-25outline-style2021-08-16outline-width2021-08-10text-emphasis2021-08-04::backdrop2021-08-03hyphenate-limit-chars2021-07-15:fullscreen2021-07-14mask2021-07-02content-visibility2021-06-21place-content2021-05-13mask-composite2021-05-10:empty2021-04-27:where2021-03-23justify-self2021-03-18mask-type2021-03-02place-self2021-03-02:current2021-02-23:future2021-02-23border-boundary2021-02-09mask-mode2021-02-03caret-shape2021-01-27caret2021-01-27aspect-ratio2021-01-20margin-inline2021-01-14margin-inline-end2021-01-14margin-block-start2021-01-08margin-block-end2021-01-08margin-block2021-01-06

Settting 2022 Goals

More SEO focus. I’ve almost shunned SEO in the past. Partially because the HTML best practices seem pretty easy and obvious, and my inbox is so full of total slimeball link builders I’d like to see do literally anything else with their time. Butttt. I’m just being ignorant about it. I think it will be fun, interesting, and likely useful to take a more considered look at SEO best practices for a content site like this and make a stab at improving it. The related goal being: Gain 10% in pageview traffic. We lost 6% this year, so I think 10% will get us back on track and moving upward. But it’s a big goal so I’m already nervous about it.Another digital book. All the infrastructure is there for this and I’ve got ideas. I just need to write and put it in place.More social media experimentation. That’s a loosey-goosey goal but whatever, we’ve got our work cut out for us in other ways. Like SEO, for a few years there I kinda shunned dedicated social media work for the CSS-Tricks brand. Mostly because when I look at the traffic numbers, so very little of it comes from social media, especially considering how much time we were spending on it in the past. We don’t really benefit much from brand social media, so why bother? Well, maybe I was thinking about it the wrong way. Maybe we can just not care what traffic it drives but care about the connection with readers directly there. If we’re more fun and interesting on social media, maybe we continue to build trust in what we’re doing here. Maybe it can help drive sales if we get that second goal done. Maybe its more directly monetizeable.

Thank You

Special thanks to Geoff! If you didn’t know, he’s our lead editor around here and keeping this entire site humming along nicely. You’ll work with Geoff if you do any guest writing here at all.

Special thanks to our biggest year-long sponsors Automattic and Frontend Masters. Our year-end series is both a thank you to you the readers and to them.

To another year!

🙏

Thank You (2021 Edition) originally published on CSS-Tricks. You should get the newsletter and become a supporter.

Visit source: Post courtesy of : Thank You (2021 Edition)

Categories
Article

Defensive CSS

Ahmad Shadeed nails it again with “Defensive CSS.” The idea is that you should write CSS to be ready for issues caused by dynamic content. More items than you thought would be there? No problem, the area can expand or scroll. Title too long? No problem, it either wraps or truncates, and won’t bump into anything…

Want to Read more ? Ahmad Shadeed nails it again with “Defensive CSS.” The idea is that you should write CSS to be ready for issues caused by dynamic content.

More items than you thought would be there? No problem, the area can expand or scroll. Title too long? No problem, it either wraps or truncates, and won’t bump into anything weird because margins or gaps are set up. Image come over in an unexpected size? No worries, the layout is designed to make sure the dedicated area is filled with image and will handle the sizing/cropping accordingly.

There is no such thing as being a good CSS developer and not coding defensively. This is what being a CSS developer is, especially when you factor in progressive enhancement concepts and cross-browser/device unknowns.
To Shared Link — Permalink on CSS-Tricks
Defensive CSS originally published on CSS-Tricks. You should get the newsletter and become a supporter.

Visit source: Post courtesy of : Defensive CSS

Categories
Article opinion

The Web is More Gooder, and Other Observations on Today’s Web Tech

I’m actually working on a talk (whew! been a while! kinda feels good!) about just how good the world of building websites has gotten. I plan to cover a wide swath of web tech, on purpose, because I feel like things have gotten good all around. CSS is doing great, but so is nearly everything…

Want to Read more ? I’m actually working on a talk (whew! been a while! kinda feels good!) about just how good the world of building websites has gotten. I plan to cover a wide swath of web tech, on purpose, because I feel like things have gotten good all around. CSS is doing great, but so is nearly everything else involved in making websites, especially if we take care in what we’re doing.

It also strikes me that updates to the web platform and the ecosystem around it are generally additive. If you feel like the web used to be simpler, well, perhaps it was—but it also still is. Whatever you could do then you can do now, if you want to, although, it would be a fair point if you’re job searching and the expectations to get hired involve a wheelbarrow of complicated tech.

This idea of the web getting better feels like it’s in the water a bit…

Chris Ferdinandi in “Web tech is better. Developer norms are worse.”:

What the modern web can actually do, easily and out-of-the-box, is amazing. My friend Sarah Dayan started her career at around the same time as me, and has a wonderful thread on how things have changed since then.In particular, Sarah talks about the dramatically improved capabilities of the web and expectations from customers and the people who use it.Modern web technology is lightyears ahead of the late 2000s.

Wes and Scott on Syntax.fm 410 also talk about all kinds of stuff that is great now, from HTML, CSS, and JavaScript to tooling and hosting.

Simeon Griggs in “There’s never been a better time to build websites” has a totally different take on what is great on the web these days than mine, but I appreciate that. The options around building websites have also widened, meaning there are approaches to things that just feel better to people who think and work in different ways.

While there’s absolutely a learning curve to getting started, once you’ve got momentum, modern web development feels like having rocket boosters. The distance between idea and execution is as short as it’s ever been.

The Web is More Gooder, and Other Observations on Today’s Web Tech originally published on CSS-Tricks. You should get the newsletter and become a supporter.

Visit source: Post courtesy of : The Web is More Gooder, and Other Observations on Today’s Web Tech

Categories
2021 End-of-Year Thoughts Article

Add Less

When you’re about to start a new website, what do you think first? Do you start with a library or framework you know, like React or Vue, or a meta-framework on top of that, like Next or Nuxt? Do you pull up a speedy build tool like Vite, or configure your webpack? There’s a great…

Want to Read more ? When you’re about to start a new website, what do you think first? Do you start with a library or framework you know, like React or Vue, or a meta-framework on top of that, like Next or Nuxt? Do you pull up a speedy build tool like Vite, or configure your webpack?

There’s a great tweet by Phil Hawksworth that I bookmarked a few years back and still love to this day:

A few people have asked me what I did to make this so fast.The answer is: nothing.I just didn't add anything to make it slow.I kept it simple.The pages are pre-rendered.The CSS is inlined.I didn't add unnecessary javascript.The work was done before you got there.— Phil Hawksworth (@philhawksworth) September 8, 2018

Your websites start fast until you add too much to make them slow. Do you need any framework at all? Could you do what you want natively in the browser? Would doing it without a framework at all make your site lighter, or actually heavier in the long run as you create or optimize what others have already done?

I personally love the idea of shipping less code to ultimately ship more value to the browser. Understanding browser APIs and what comes “for free” could actually lead to less reinventing the wheel, and potentially more accessibility as you use the tools provided.

Instead of pulling in a library for every single task you want to do, try to look under the hood at what they are doing. For example, in a project I was maintaining, I noticed that we had a React component imported that was shipping an entire npm package for a small (less than 10-line) component with some CSS sprinkled on top (that we were overriding with our own design system). When we re-wrote that component from scratch, our bundle size was smaller, we were able to customize it more, and we didn’t have to work around someone else’s decisions.

Now, I’m not saying you shouldn’t use any libraries or frameworks or components out there. Open source exists for a reason! What I am saying is to be discerning about what you bring into your projects. Let the power of the browser work for you, and use less stuff!

Add Less originally published on CSS-Tricks. You should get the newsletter and become a supporter.

Visit source: Post courtesy of : Add Less

Categories
2021 End-of-Year Thoughts Accessibility animation Article prefers-reduced-motion

Empathetic Animation

Animation on the web is often a contentious topic. I think, in part, it’s because bad animation is blindingly obvious, whereas well-executed animation fades seamlessly into the background. When handled well, animation can really elevate a website, whether it’s just adding a bit of personality or providing visual hints and lessening cognitive load. Unfortunately, it…

Want to Read more ? Animation on the web is often a contentious topic. I think, in part, it’s because bad animation is blindingly obvious, whereas well-executed animation fades seamlessly into the background. When handled well, animation can really elevate a website, whether it’s just adding a bit of personality or providing visual hints and lessening cognitive load. Unfortunately, it often feels like there are two camps, accessibility vs. animation. This is such a shame because we can have it all! All it requires is a little consideration.

Here’s a couple of important questions to ask when you’re creating animations.

Does this animation serve a purpose?

This sounds serious, but don’t worry — the site’s purpose is key. If you’re building a personal portfolio, go wild! However, if someone’s trying to file a tax return, whimsical loading animations aren’t likely to be well-received. On the other hand, an animated progress bar could be a nice touch while providing visual feedback on the user’s action.

Is it diverting focus from important information?

It’s all too easy to get caught up in the excitement of whizzing things around, but remember that the web is primarily an information system. When people are trying to read, animating text or looping animations that play nearby can be hugely distracting, especially for people with ADD or ADHD. Great animation aids focus; it doesn’t disrupt it.

So! Your animation’s passed the test, what next? Here are a few thoughts…

Did we allow users to opt-out?

It’s important that our animations are safe for people with motion sensitivities. Those with vestibular (inner ear) disorders can experience dizziness, headaches, or even nausea from animated content.

Luckily, we can tap into operating system settings with the prefers-reduced-motion media query. This media query detects whether the user has requested the operating system to minimize the amount of animation or motion it uses.

The reduced motion settings in macOS.

Here’s an example:

@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

This snippet taps into that user setting and, if enabled, it gets rid of all your CSS animations and transitions. It’s a bit of a sledgehammer approach though — remember, the key word in this media query is reduced. Make sure functionality isn’t breaking and that users aren’t losing important context by opting out of the animation. I prefer tailoring reduced motion options for those users. Think simple opacity fades instead of zooming or panning effects.

What about JavaScript, though?

Glad you asked! We can make use of the reduced motion media query in JavaScript land, too!

let motionQuery = matchMedia(‘(prefers-reduced-motion)’);

const handleReduceMotion = () => {
if (motionQuery.matches) {
// reduced motion options
}
}

motionQuery.addListener(handleReduceMotion);
handleReduceMotion()

Tapping into system preferences isn’t bulletproof. After all, it’s there’s no guarantee that everyone affected by motion knows how to change their settings. To be extra safe, it’s possible to add a reduced motion toggle in the UI and put the power back in the user’s hands to decide. We {the collective} has a really nice implementation on their site

Here’s a straightforward example:

CodePen Embed Fallback

Scroll animations

One of my favorite things about animating on the web is hooking into user interactions. It opens up a world of creative possibilities and really allows you to engage with visitors. But it’s important to remember that not all interactions are opt-in — some (like scrolling) are inherently tied to how someone navigates around your site.

The Nielson Norman Group has done some great research on scroll interactions. One particular part really stuck out for me. They found that a lot of task-focused users couldn’t tell the difference between slow load times and scroll-triggered entrance animations. All they noticed was a frustrating delay in the interface’s response time. I can relate to this; it’s annoying when you’re trying to scan a website for some information and you have to wait for the page to slowly ease and fade into view.

If you’re using GreenSock’s ScrollTrigger plugin for your animations, you’re in luck. We’ve added a cool little property to help avoid this frustration: fastScrollEnd.

fastScrollEnd detects the users’ scroll velocity. ScrollTrigger skips the entrance animations to their end state when the user scrolls super fast, like they’re in a hurry. Check it out!

CodePen Embed Fallback

There’s also a super easy way to make your scroll animations reduced-motion-friendly with ScrollTrigger.matchMedia():

CodePen Embed Fallback

I hope these snippets and insights help. Remember, consider the purpose, lead with empathy, and use your animation powers responsibly!

Empathetic Animation originally published on CSS-Tricks. You should get the newsletter and become a supporter.

Visit source: Post courtesy of : Empathetic Animation

Categories
2021 End-of-Year Thoughts Article service workers

Add a Service Worker to Your Site

One of the best things you can do for your website in 2022 is add a service worker, if you don’t have one in place already. Service workers give your website super powers. Today, I want to show you some of the amazing things that they can do, and give you a paint-by-numbers boilerplate that…

Want to Read more ? One of the best things you can do for your website in 2022 is add a service worker, if you don’t have one in place already. Service workers give your website super powers. Today, I want to show you some of the amazing things that they can do, and give you a paint-by-numbers boilerplate that you can use to start using them on your site right away.

What are service workers?

A service worker is a special type of JavaScript file that acts like middleware for your site. Any request that comes from the site, and any response it gets back, first goes through the service worker file. Service workers also have access to a special cache where they can save responses and assets locally.

Together, these features allow you to…

Serve frequently accessed assets from your local cache instead of the network, reducing data usage and improving performance.Provide access to critical information (or even your entire site or app) when the visitor goes offline.Prefetch important assets and API responses so they’re ready when the user needs them.Provide fallback assets in response to HTTP errors.

In short, service workers allow you to build faster and more resilient web experiences.

Unlike regular JavaScript files, service workers do not have access to the DOM. They also run on their own thread, and as a result, don’t block other JavaScript from running. Service workers are designed to be fully asynchronous.

Security

Because service workers intercept every request and response for your site or app, they have some important security limitations.

Service workers follow a same-origin policy.

You can’t run your service worker from a CDN or third party. It has to be hosted at the same domain as where it will be run.

Service workers only work on sites with an installed SSL certificate.

Many web hosts provide SSL certificates at no cost or for a small fee. If you’re comfortable with the command line, you can also install one for free using Let’s Encrypt.

There is an exception to the SSL certificate requirement for localhost testing, but you can’t run your service worker from the file:// protocol. You need to have a local server running.

Adding a service worker to your site or web app

To use a service worker, the first thing we need to do is register it with the browser. You can register a service worker using the navigator.serviceWorker.register() method. Pass in the path to the service worker file as an argument.

navigator.serviceWorker.register(‘sw.js’);

You can run this in an external JavaScript file, but prefer to run it directly in a script element inline in my HTML so that it runs as soon as possible.

Unlike other types of JavaScript files, service workers only work for the directory in which they exist (and any of its sub-directories). A service worker file located at /js/sw.js would only work for files in the /js directory. As a result, you should place your service worker file inside the root directory of your site.

While service workers have fantastic browser support, it’s a good idea to make sure the browser supports them before running your registration script.

if (navigator && navigator.serviceWorker) {
navigator.serviceWorker.register(‘sw.js’);
}

After the service worker installs, the browser can activate it. Typically, this only happens when…

there is no service worker currently active, orthe user refreshes the page.

The service worker won’t run or intercept requests until it’s activated.

Listening for requests in a service worker

Once the service worker is active, it can start intercepting requests and running other tasks. We can listen for requests with self.addEventListener() and the fetch event.

// Listen for request events
self.addEventListener(‘fetch’, function (event) {
// Do stuff…
});

Inside the event listener, the event.request property is the request object itself. For ease, we can save it to the request variable.

Certain versions of the Chromium browser have a bug that throws an error if the page is opened in a new tab. Fortunately, there’s a simple fix from Paul Irish that I include in all of my service workers, just in case:

// Listen for request events
self.addEventListener(‘fetch’, function (event) {

// Get the request
let request = event.request;

// Bug fix
// https://stackoverflow.com/a/49719964
if (event.request.cache === ‘only-if-cached’ && event.request.mode !== ‘same-origin’) return;

});

Once your service worker is active, every single request is sent through it, and will be intercepted with the fetch event.

Service worker strategies

Once your service worker is installed and activated, you can intercept requests and responses, and handle them in various ways. There are two primary strategies you can use in your service worker:

Network-first. With a network-first approach, you pass along requests to the network. If the request isn’t found, or there’s no network connectivity, you then look for the request in the service worker cache.Offline-first. With an offline-first approach, you check for a requested asset in the service worker cache first. If it’s not found, you send the request to the network.

Network-first and offline-first approaches work in tandem. You will likely mix-and-match approaches depending on the type of asset being requested.

Offline-first is great for large assets that don’t change very often: CSS, JavaScript, images, and fonts. Network-first is a better fit for frequently updated assets like HTML and API requests.

Strategies for caching assets

How do you get assets into your browser’s cache? You’ll typically use two different approaches, depending on the types of assets.

Pre-cache on install. Every site and web app has a set of core assets that are used on almost every page: CSS, JavaScript, a logo, favicon, and fonts. You can pre-cache these during the install event, and serve them using an offline-first approach whenever they’re requested.Cache as you browser. Your site or app likely has assets that won’t be accessed on every visit or by every visitor; things like blog posts and images that go with articles. For these assets, you may want to cache them in real-time as the visitor accesses them.

You can then serve those cached assets, either by default or as a fallback, depending on your approach.

Implementing network-first and offline-first strategies in your service worker

Inside a fetch event in your service worker, the request.headers.get(‘Accept’) method returns the MIME type for the content. We can use that to determine what type of file the request is for. MDN has a list of common files and their MIME types. For example, HTML files have a MIME type of text/html.

We can pass the type of file we’re looking for into the String.includes() method as an argument, and use if statements to respond in different ways based on the file type.

// Listen for request events
self.addEventListener(‘fetch’, function (event) {

// Get the request
let request = event.request;

// Bug fix
// https://stackoverflow.com/a/49719964
if (event.request.cache === ‘only-if-cached’ && event.request.mode !== ‘same-origin’) return;

// HTML files
// Network-first
if (request.headers.get(‘Accept’).includes(‘text/html’)) {
// Handle HTML files…
return;
}

// CSS & JavaScript
// Offline-first
if (request.headers.get(‘Accept’).includes(‘text/css’) || request.headers.get(‘Accept’).includes(‘text/javascript’)) {
// Handle CSS and JavaScript files…
return;
}

// Images
// Offline-first
if (request.headers.get(‘Accept’).includes(‘image’)) {
// Handle images…
}

});

Network-first

Inside each if statement, we use the event.respondWith() method to modify the response that’s sent back to the browser.

For assets that use a network-first approach, we use the fetch() method, passing in the request, to pass through the request for the HTML file. If it returns successfully, we’ll return the response in our callback function. This is the same behavior as not having a service worker at all.

If there’s an error, we can use Promise.catch() to modify the response instead of showing the default browser error message. We can use the caches.match() method to look for that page, and return it instead of the network response.

// Send the request to the network first
// If it’s not found, look in the cache
event.respondWith(
fetch(request).then(function (response) {
return response;
}).catch(function (error) {
return caches.match(request).then(function (response) {
return response;
});
})
);

Offline-first

For assets that use an offline-first approach, we’ll first check inside the browser cache using the caches.match() method. If a match is found, we’ll return it. Otherwise, we’ll use the fetch() method to pass the request along to the network.

// Check the cache first
// If it’s not found, send the request to the network
event.respondWith(
caches.match(request).then(function (response) {
return response || fetch(request).then(function (response) {
return response;
});
})
);

Pre-caching core assets

Inside an install event listener in the service worker, we can use the caches.open() method to open a service worker cache. We pass in the name we want to use for the cache, app, as an argument.

The cache is scoped and restricted to your domain. Other sites can’t access it, and if they have a cache with the same name the contents are kept entirely separate.

The caches.open() method returns a Promise. If a cache already exists with this name, the Promise will resolve with it. If not, it will create the cache first, then resolve.

// Listen for the install event
self.addEventListener(‘install’, function (event) {
event.waitUntil(caches.open(‘app’));
});

Next, we can chain a then() method to our caches.open() method with a callback function.

In order to add files to the cache, we need to request them, which we can do with the new Request() constructor. We can use the cache.add() method to add the file to the service worker cache. Then, we return the cache object.

We want the install event to wait until we’ve cached our file before completing, so let’s wrap our code in the event.waitUntil() method:

// Listen for the install event
self.addEventListener(‘install’, function (event) {

// Cache the offline.html page
event.waitUntil(caches.open(‘app’).then(function (cache) {
cache.add(new Request(‘offline.html’));
return cache;
}));

});

I find it helpful to create an array with the paths to all of my core files. Then, inside the install event listener, after I open my cache, I can loop through each item and add it.

let coreAssets = [
‘/css/main.css’,
‘/js/main.js’,
‘/img/logo.svg’,
‘/img/favicon.ico’
];

// On install, cache some stuff
self.addEventListener(‘install’, function (event) {

// Cache core assets
event.waitUntil(caches.open(‘app’).then(function (cache) {
for (let asset of coreAssets) {
cache.add(new Request(asset));
}
return cache;
}));

});

Cache as you browse

Your site or app likely has assets that won’t be accessed on every visit or by every visitor; things like blog posts and images that go with articles. For these assets, you may want to cache them in real-time as the visitor accesses them. On subsequent visits, you can load them directly from cache (with an offline-first approach) or serve them as a fallback if the network fails (using a network-first approach).

When a fetch() method returns a successful response, we can use the Response.clone() method to create a copy of it.

Next, we can use the caches.open() method to open our cache. Then, we’ll use the cache.put() method to save the copied response to the cache, passing in the request and copy of the response as arguments. Because this is an asynchronous function, we’ll wrap our code in the event.waitUntil() method. This prevents the event from ending before we’ve saved our copy to cache. Once the copy is saved, we can return the response as normal.

/explanation We use cache.put() instead of cache.add() because we already have a response. Using cache.add() would make another network call.

// HTML files
// Network-first
if (request.headers.get(‘Accept’).includes(‘text/html’)) {
event.respondWith(
fetch(request).then(function (response) {

// Create a copy of the response and save it to the cache
let copy = response.clone();
event.waitUntil(caches.open(‘app’).then(function (cache) {
return cache.put(request, copy);
}));

// Return the response
return response;

}).catch(function (error) {
return caches.match(request).then(function (response) {
return response;
});
})
);
}

Putting it all together

I’ve put together a copy-paste boilerplate for you on GitHub. Add your core assets to the coreAssets array, and register it on your site to get started.

If you do nothing else, this will be a huge boost to your site in 2022.

But there’s so much more you can do with service workers. There are advanced caching strategies for APIs. You can provide an offline page with critical information if a visitor loses their network connection. You can clean up bloated caches as the user browses.

Jeremy Keith’s book, Going Offline, is a great primer on service workers. If you want to take things to the next level and dig into progressive web apps, Jason Grigsby’s book dives into the various strategies you can use.

And for a pragmatic deep dive you can complete in about an hour, I also have a course and ebook on service workers with lots of code examples and a project you can work on.

Add a Service Worker to Your Site originally published on CSS-Tricks. You should get the newsletter and become a supporter.

Visit source: Post courtesy of : Add a Service Worker to Your Site

Categories
Article scrolling

Doom Damage Flash on Scroll

The video game Doom famously would flash the screen red when you were hit. Chris Johnson not only took that idea, but incorporated a bunch of the UI from Doom into this tounge-in-cheek JavaScript library called Doom Scroller. Get it? Like, doom scrolling, but like, Doom scrolling. It’s funny, trust me. I extracted bits from Chris’ cool project to focus on the damage animation itself. The red flash is done in HTML and CSS. First, we create a full screen overlay: #doom-damage { background-color: red; position: fixed; top: 0; left: 0; width: 100%; height: 100%; opacity: 0; pointer-events: none; } Note that it’s not display: none. It’s much harder to animate that as we have to wait until the animation is completed to apply it. That’s because display isn’t animatable. It’s doable, just annoying. To flash it, we’ll apply a class that does it, but only temporarily. const damage = document.getElementById(“doom-damage”); function doomTakeDamage() { damage.classList.add(“do-damage”); setTimeout(function () { damage.classList.remove(“do-damage”); }, 400); } When that class activates, we’ll immediately turn the screen red (really giving it that shock appeal) and then fade the red away: .do-damage { background-color: red; animation: 0.4s doom-damage forwards; } @keyframes doom-damage { 0% { opacity: 1; } 100% { opacity: 0; } } The next bit calls the function that does the damage flash. Essentially it tracks the current scroll position, and if it’s past the nextDamagePosition, it will red flash and reset the next nextDamagePostition one full screen height length away. If you want to see all that, I’ve abstracted it into this Pen: CodePen Embed Fallback The post Doom Damage Flash on Scroll appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

Read the rest Doom Damage Flash on Scroll

Read at the original Source: css tricks.