Blog

38 posts 2009 16 posts 2010 50 posts 2011 28 posts 2012 15 posts 2013 7 posts 2014 10 posts 2015 5 posts 2016 4 posts 2017 7 posts 2018 2 posts 2019 17 posts 2020 7 posts 2021 7 posts 2022 11 posts 2023 6 posts 2024

New decade, new theme

2 min read 0 comments Report broken page

It has been almost a decade since this blog last saw a redesign.

This blog’s theme 2011 - 2020. RIP!

In these 9 years, my life changed dramatically. I joined and left W3C, joined the CSS WG, went to MIT for a PhD, published a book, got married, had a baby, among other things. I designed dozens of websites for dozens of projects, but this theme remained constant, with probably a hasty tweak here and there but nothing more than that. Even its mobile version was a few quick media queries to make it palatable on mobile.

To put this into perspective, when I designed that theme:

  • CSS gradients were still cutting edge
  • We were still using browser prefixes all over the place
  • RSS was still a thing that websites advertised
  • Skeuomorphism was all the rage
  • Websites were desktop first, and often desktop-only.
  • Opera was a browser we tested in.
  • IE8 was the latest IE version. It didn’t support SVG, gradients, border-radius, shadows, web fonts (except .eot), transforms, <video>, <audio>, <canvas>
  • We were still hacking layout with floats, clearfix and overflow: hidden

Over the course of these years, I kept saying “I need to update my website’s theme”, but never got around to it, there was always something more high priority.

The stroke that broke the camel’s back was this Monday. I came up with a nice CSS tip on another website I was working on, and realized I was hesitating to blog about it because I was embarrassed at how my website looked. This is it, I thought. If it has gotten so bad that I avoid blogging because I don’t want people to be reminded of how old my website looks, I need to get my shit together and fix this, I told myself.

My plan was to design something entirely from scratch, like I had done the previous time (the previous theme used a blank HTML5 starter theme as its only starting point). However, when I previewed the new Wordpress default (Twenty Twenty), I fell in love, especially with its typography: it used a very Helvetica-esque variable font as its heading typeface, and Hoefler Text for body text. 😍

It would surely be very convenient to be able to adapt an existing theme, but on the other hand, isn’t it embarrassing to be known for CSS and use the default theme or something close to it?

In the end, I kept the things I liked about it and it certainly still looks a lot like Twenty Twenty, but I think I’ve made enough tweaks that it’s also very Lea. And of course there are animated conic gradients in it, because duh. 😂

Do keep in mind that this is just a day’s work, so it will be rough around the edges and still very much a work in progress. Let me know about any issues you find in the comments!

PS: Yes, yes, I will eventually get around to enforcing https://!


Today's Javascript, from an outsider's perspective

3 min read 0 comments Report broken page

Today I tried to help a friend who is a great computer scientist, but not a JS person use a JS module he found on Github. Since for the past 6 years my day job is doing usability research & teaching at MIT, I couldn’t help but cringe at the slog that this was. Lo and behold, a pile of unnecessary error conditions, cryptic errors, and lack of proper feedback. And I don’t feel I did a good job communicating the frustration he went through in the one hour or so until he gave up.

It went a bit like this…

Note: N_ames of packages and people have been changed to protect their identity. I’ve also omitted a few issues he faced that were too specific to the package at hand. Some of the errors are reconstructed from memory, so let me know if I got anything wrong!_

John: Hey, I want to try out this algorithm I found on Github, it says to use import functionName from packageName and then call functionName(arguments). Seems simple enough! I don’t really need a UI, so I’m gonna use Node!

Lea: Sure, Node seems appropriate for this!

John runs npm install packageName --save as recommended by the package’s README John runs node index.js

Node:

Warning: To load an ES module, set “type”: “module” in the package.json or use the .mjs extension. SyntaxError: Cannot use import statement outside a module

John: But I don’t have a package.json… Lea: Run npm init, it will generate it for you!

John runs npm init, goes through the wizard, adds type: "module" manually to the generated package.json. John runs node index.js

Node:

SyntaxError: Cannot use import statement outside a module

Oddly, the error was thrown from an internal module of the project this time. WAT?!

Lea: Ok, screw this, just run it in a browser, it’s an ES6 module and it’s just a pure JS algorithm that doesn’t use any Node APIs, it should work.

John makes a simple index.html with a <script type="module" src="index.js"> John loads index.html in a browser

Nothing in the console. Nada. Crickets. 🦗

Lea: Oh, you need to adjust your module path to import packageName. Node does special stuff to resolve based on node_modules, now you’re in a browser you need to specify an explicit path yourself.

John looks, at his filesystem, but there was no node_modules directory.

Lea: Oh, you ran npm install before you had a package.json, that’s probably it! Try it again!

John runs npm install packageName --save again

John: Oh yeah, there is a node_modules now!

John desperately looks in node_modules to find the entry point John edits his index.js accordingly, reloads index.html

Firefox:

Incorrect MIME type: text/html

Lea: Oh, you’re in file://! Dude, what are you doing these days without a localhost? Javascript is severely restricted in file:// today.

John: But why do I… ok fine, I’m going to start a localhost.

John starts localhost, visits his index.html under http://localhost:80

Firefox:

Incorrect MIME type: text/html

John: Sigh. Do I need to configure my localhost to serve JS files with a text/javascript MIME type? Lea: What? No! It knows this. Um… look at the Networks tab, I suspect it can’t find your module, so it’s returning an HTML page for the 404, then it complains because the MIME type of the error page is not text/javascript.

Looks at node_modules again, corrects path. Turns out VS Code collapses folders with only 1 subfolder, which is why we hadn’t noticed.

FWIW I do think this is a good usability improvement on VS Code’s behalf, it improves efficiency, but they need to make it more visible that this is what has happened.

Firefox:

SyntaxError: missing ) after formal parameters

Lea: What? That’s coming from the package source, it’s not your fault. I don’t understand… can we look at this line?

John clicks at line throwing the error

Lea: Oh my goodness. This is not Javascript, it’s Typescript!! With a .js extension!! John: I just wanted to run one line of code to test this algorithm… 😭😭😭

John gives up. Concludes never to touch Node, npm, or ES6 modules with a barge pole.

The End.

Note that John is a computer scientist that knows a fair bit about the Web: He had Node & npm installed, he knew what MIME types are, he could start a localhost when needed. What hope do actual novices have?


LCH colors in CSS: what, why, and how?

7 min read 0 comments Report broken page

I was always interested in color science. In 2014, I gave a talk about CSS Color 4 at various conferences around the world called “The Chroma Zone”. Even before that, in 2009, I wrote a color picker that used a hidden Java applet to support ICC color profiles to do CMYK properly, a first on the Web at the time (to my knowledge). I never released it, but it sparked this angry rant.

Color is also how I originally met my now husband, Chris Lilley: In my first CSS WG meeting in 2012, he approached me to ask a question about CSS and Greek, and once he introduced himself I said “You’re Chris Lilley, the color expert?!? I have questions for you!”. I later discovered that he had done even more cool things (he was a co-author of PNG and started SVG 🤯), but at the time, I only knew of him as “the W3C color expert”, that’s how much into color I was (I got my color questions answered much later, in 2015 that we actually got together).

My interest in color science was renewed in 2019, after I became co-editor of CSS Color 5, with the goal of fleshing out my color modification proposal, which aims to allow arbitrary tweaking of color channels to create color variations, and combine it with Una’s color modification proposal. LCH colors in CSS is something I’m very excited about, and I strongly believe designers would be outraged we don’t have them yet if they knew more about them.

What is LCH?

CSS Color 4 defines lch() colors, among other things, and as of recently, all major browsers have started implementing them or are seriously considering it:

LCH is a color space that has several advantages over the RGB/HSL colors we’re familiar with in CSS. In fact, I’d go as far as to call it a game-changer, and here’s why.

1. We actually get access to about 50% more colors.

This is huge. Currently, every CSS color we can specify, is defined to be in the sRGB color space. This was more than sufficient a few years ago, since all but professional monitors had gamuts smaller than sRGB. However, that’s not true any more. Today, the gamut (range of possible colors displayed) of most monitors is closer to P3, which has a 50% larger volume than sRGB. CSS right now cannot access these colors at all. Let me repeat: We have no access to one third of the colors in most modern monitors. And these are not just any colors, but the most vivid colors the screen can display. Our websites are washed out because monitor hardware evolved faster than CSS specs and browser implementations.

Gamut volume of sRGB vs P3

2. LCH (and Lab) is perceptually uniform

In LCH, the same numerical change in coordinates produces the same perceptual color difference. This property of a color space is called “perceptual uniformity”. RGB or HSL are not perceptually uniform. A very illustrative example is the following [example source]:

Both the colors in the first row, as well as the colors in the second row, only differ by 20 degrees in hue. Is the perceptual difference between them equal?

3. LCH lightness actually means something

In HSL, lightness is meaningless. Colors can have the same lightness value, with wildly different perceptual lightness. My favorite examples are yellow and blue. Believe it or not, both have the same HSL lightness!

Both of these colors have a lightness of 50%, but they are most certainly not equally light. What does HSL lightness actually mean then?

You might argue that at least lightness means something for constant hue and saturation, i.e. for adjustments within the same color. It is true that we do get a lighter color if we increase the HSL lightness and a darker one if we decrease it, but it’s not necessarily the same color:

Both of these have the same hue and saturation, but do they really look like darker and lighter variants of the same color?

With LCH, any colors with the same lightness are equally perceptually light, and any colors with the same chroma are equally perceptually saturated.

How does LCH work?

LCH stands for “Lightness Chroma Hue”. The parameters loosely correspond to HSL’s, however there are a few crucial differences:

The hue angles don’t fully correspond to HSL’s hues. E.g. 0 is not red, but more of a magenta and 180 is not turquoise but more of a bluish green, and is exactly complementary.

Note how these colors, while wildly different in hue, perceptually have the same lightness.

In HSL, saturation is a neat 0-100 percentage, since it’s a simple transformation of RGB into polar coordinates. In LCH however, Chroma is theoretically unbounded. LCH (like Lab) is designed to be able to represent the entire spectrum of human vision, and not all of these colors can be displayed by a screen, even a P3 screen. Not only is the maximum chroma different depending on screen gamut, it’s actually different per color.

This may be better understood with an example. For simplicity, assume you have a screen whose gamut exactly matches the sRGB color space (for comparison, the screen of a 2013 MacBook Air was about 60% of sRGB, although most modern screens are about 150% of sRGB, as discussed above). For L=50 H=180 (the cyan above), the maximum Chroma is only 35! For L=50 H=0 (the magenta above), Chroma can go up to 77 without exceeding the boundaries of sRGB. For L=50 H=320 (the purple above), it can go up to 108!

While the lack of boundaries can be somewhat unsettling (in people and in color spaces), don’t worry: if you specify a color that is not displayable in a given monitor, it will be scaled down so that it becomes visible while preserving its essence. After all, that’s not new: before monitors got gamuts wider than sRGB, this is what was happening with regular CSS colors when they were displayed in monitors with gamuts smaller than sRGB.

An LCH color picker

Hopefully, you are now somewhat excited about LCH, but how to visualize it?

I actually made this a while ago, primarily to help me, Chris, Adam, and Una in wrapping our heads around LCH sufficiently to edit CSS Color 5. It’s different to know the theory, and it’s different to be able to play with sliders and see the result. I even bought a domain, css.land, to host similar demos eventually. We used it a fair bit, and Chris got me to add a few features too, but I never really posted about it, so it was only accessible to us, and anybody that noticed its Github repo.

Why not just use an existing LCH color picker?

  • The conversion code for this is written by Chris, and he was confident the math is at least intended to be correct (i.e. if it’s wrong it’s a bug in the code, not a gap in understanding)
  • The Chroma is not 0-100 like in some color pickers we found
  • We wanted to allow inputting arbitrary CSS colors (the “Import…” button above)
  • We wanted to allow inputting decimals (the sliders only do integers, but the black number inputs allow any number)
  • I wanted to be able to store colors, and see how they interpolate.
  • We wanted to be able to see whether the LCH color was within sRGB, P3, (or Rec.2020, an even larger color space).
  • We wanted alpha
  • And lastly, because it’s fun! Especially since it’s implemented with Mavo (and a little bit of JS, this is not a pure Mavo HTML demo).

Recently, Chris posted it in a whatwg/html issue thread and many people discovered it, so it nudged me to post about it, so, here it is: css.land/lch

FAQ

Based on the questions I got after I posted this article, I should clarify a few common misconceptions.

“You said that these colors are not implemented yet, but I see them in your article”

All of the colors displayed in this article are within the sRGB gamut, exactly because we can’t display those outside it yet. sRGB is a color space, not a syntax. E.g. rgb(255 0 0) and lch(54.292% 106.839 40.853) specify the same color.

“How does the LCH picker display colors outside sRGB?”

It doesn’t. Neither does any other on the Web (to my knowledge). The color picker is implemented with web technologies, and therefore suffers from the same issues. It has to scale them down to display something similar, that is within sRGB (it used to just clip the RGB components to 0-100%, but thanks to this PR from Tab it now uses a far superior algorithm: it just reduces the Chroma until the color is within sRGB). This is why increasing the Chroma doesn’t produce a brighter color beyond a certain point: because that color cannot be displayed with CSS right now.

“I’ve noticed that Firefox displays more vivid colors than Chrome and Safari, is that related?”

Firefox does not implement the spec that restricts CSS colors to sRGB. Instead, it just throws the raw RGB coordinates on the screen, so e.g. rgb(100% 0% 0%) is the brightest red your screen can display. While this may seem like a superior solution, it’s incredibly inconsistent: specifying a color is approximate at best, since every screen displays it differently. By restricting CSS colors to a known color space (sRGB) we gained device independence. LCH and Lab are also device independent as they are based on actual measured color.

What about color(display-p3 r g b)? Safari supports that since 2017!

I was notified of this after I posted this article. I was aware Safari was implementing this syntax a while ago, but somehow missed that they shipped it. In fact, WebKit published an article about this syntax last month! How exciting!

color(colorspaceid params) is another syntax added by CSS Color 4 and is the swiss army knife of color management in CSS: in its full glory it allows specifying an ICC color profile and colors from it (e.g. you want real CMYK colors on a webpage? You want Pantone? With color profiles, you can do that too!). It also supports some predefined color spaces, of which display-p3 is one. So, for example, color(display-p3 0 1 0) gives us the brightest green in the P3 color space. You can use this test case to test support: you’ll see red if color() is not supported and bright green if it is.

Exciting as it may be (and I should tweak the color picker to use it when available!), do note that it only addresses the first issue I mentioned: getting to all gamut colors. However, since it’s RGB-based, it still suffers from the other issues of RGB. It is not perceptually uniform, and is difficult to create variants (lighter or darker, more or less vivid etc) by tweaking its parameters.

Furthermore, it’s a short-term solution. It works now, because screens that can display a wider gamut than P3 are rare. Once hardware advances again, color(display-p3 ...) will have the same problem as sRGB colors have today. LCH and Lab are device independent, and can represent the entire gamut of human vision so they will work regardless of how hardware advances.

How does LCH relate to the Lab color space that I know from Photoshop and other applications?

LCH is the same color space as Lab, just viewed differently! Take a look at the following diagram that I made for my students:

The L in Lab and LCH is exactly the same (perceptual Lightness). For a given lightness L, in Lab, a color has cartesian coordinates (L, a, b) and polar coordinates (L, C, H). Chroma is just the length of the line from 0 to point (a, b) and Hue is the angle of that ray. Therefore, the formulae to convert Lab to LCH are trivial one liners: C is sqrt(a² + b²) and H is atan(b/a) (with different handling if a = 0). atan() is just the reverse of tan(), i.e. tan(H) = b/a.


Issue closing stats for any repo

6 min read 0 comments Report broken page

tl;dr: If you just want to quickly get stats for a repo, you can find the app here. The rest of this post explains how it’s built with Mavo HTML, CSS, and 0 lines of JS. Or, if you’d prefer, you can just View Source — it’s all there!

The finished app we’re going to make, find it at https://projects.verou.me/issue-closing

One of the cool things about Mavo is how it enables one to quickly build apps that utilize the Github API. At some point I wanted to compute stats about how quickly (or rather, slowly…) Github issues are closed in the Mavo repo. And what better way to build this than a Mavo app? It was fairly easy to build a prototype for that.

Displaying a list of the last 100 closed issues and the time it took to close them

To render the last 100 closed issues in the Mavo app, I first looked up the appropriate API call in Github’s API documentation, then used it in the mv-source attribute on the Mavo root, i.e. the element with mv-app that encompasses everything in my app:

<div mv-app="issueClosing"
     mv-source="https://api.github.com/repos/mavoweb/mavo/issues?state=closed&sort=updated&per_page=100"
     mv-mode="read">
	<!-- app here -->
</div>

Then, I displayed a list of these issues with:

<div mv-multiple property="issue">
	<a class="issue-number" href="https://github.com/mavoweb/mavo/issues/[number]" title="[title]" target="_blank">#[number]</a>
	took [closed_at - created_at] ms
</div>

See the Pen Step 1 - Issue Closing App Tutorial by Lea Verou (@leaverou) on CodePen.

This would work, but the way it displays results is not very user friendly (e.g. “#542 took 149627000 ms”). We need to display the result in a more readable way.

We can use the duration() function to display a readable duration such as “1 day”:

<div mv-multiple property="issue">
	<a class="issue-number" href="https://github.com/mavoweb/mavo/issues/[number]" title="[title]" target="_blank">#[number]</a>
	took [duration(closed_at - created_at)]
</div>

See the Pen Step 2 - Issue Closing App Tutorial by Lea Verou (@leaverou) on CodePen.

Displaying aggregate statistics

However, a list of issues is not very easy to process. What’s the overall picture? Does this repo close issues fast or not? Time for some statistics! We want to calculate average, median, minimum and maximum issue closing time. To calculate these statistics, we need to use the times we have displayed in the previous step.

First, we need to give our calculation a name, so we can refer to its value in expressions:

<span property="timeToClose">[duration(closed_at - created_at)]</span>

However, as it currently stands, the value of this property is text (e.g. “1 day”, “2 months” etc). We cannot compute averages and medians on text! We need the property value to be a number. We can hide the actual raw value in an attribute and use the nicely formatted value as the visible content of the element, like so (we use the content attribute here but you can use any, e.g. a data-* attribute would work just as well):

<span property="timeToClose" mv-attribute="content" content="[closed_at - created_at]">[duration(timeToClose)]</span>

Note: There is a data formatting feature in the works which would simplify this kind of thing by allowing you to separate the raw value and its presentation without having to use separate attributes for them.

We can also add a class to color it red, green, or black depending on whether the time is longer than a month, shorter than a day, or in-between respectively:

<span property="timeToClose" mv-attribute="content" content="[closed_at - created_at]" class="[if(timeToClose > month(), 'long', if (timeToClose < day(), 'short'))]">[duration(timeToClose)]</span>

Now, on to calculate our statistics! We take advantage of the fact that timeToClose outside the issue collection gives us all the times, so we can compute aggregates on them. Therefore, the stats we want to calculate are simply average(timeToClose), median(timeToClose), min(timeToclose), and max(timeToClose). We put all these in a definition list:

<dl>
	<dt>Median</dt>
	<dd>[duration(median(timeToClose))]</dd>
	<dt>Average</dt>
	<dd>[duration(average(timeToClose))]</dd>
	<dt>Slowest</dt>
	<dd>[duration(max(timeToClose))]</dd>
	<dt>Fastest</dt>
	<dd>[duration(min(timeToClose))]</dd>
</dl>

See the Pen Step 3 - Issue Closing App Tutorial by Lea Verou (@leaverou) on CodePen.

Making repo a variable

Now that all the functionality of my app was in place, I realized this could be useful for more repos as well. Why not make the repo a property that can be changed? So I added an input for specifying the repo: <input property="repo" mv-default="mavoweb/mavo"> and then replaced mavoweb/mavo with [repo] everywhere else, i.e. mv-source became https://api.github.com/repos/[repo]/issues?state=closed&sort=updated&per_page=100.

Avoid reload on every keystroke

This worked, but since Mavo properties are reactive, it kept trying to reload data with every single keystroke, which was annoying and wasteful. Therefore, I needed to do a bit more work so that there is a definite action that submits the change. Enter Mavo Actions!

I created two properties: repo for the actual repo and repoInput for the input. repoInput still changes on every keystroke, but it’s repo that is actually being used in the app. I wrapped the input with a <form> and added an action on the form that does this (mv-action="set(repo, repoInput)"). I also added a submit button. Since Mavo actions on forms are triggered when the form is submitted, it doesn’t matter if I press Enter on the input, or click the Submit button, both work.

Setting the repo via a URL parameter

Eventually I also wanted to be able to set the repo from the URL, so I also added a hidden repoDefault property: <meta property="repoDefault" content="[url('repo') or 'mavoweb/mavo']">, and then changed the hardcoded mv-default="mavoweb/mavo" to mv-default="[repoDefault]" on both the repo and the repoInput properties. That way one can link to stats for a specific repo, e.g. https://projects.verou.me/issue-closing/?repo=prismjs/prism

Why a repoDefault property and not just mv-default="[url('repo') or 'mavoweb/mavo']? Just keeping things DRY and avoiding having to repeat the same expression twice.

See the Pen Step 5 - Issue Closing App Tutorial by Lea Verou (@leaverou) on CodePen.

Filtering by label

At some point I wondered: What would the issue closing times be if we only counted bugs? What if we only counted enhancements? Surely these would be different: When looking at issue closing times for a repo, one primarily cares about how fast bugs are fixed, not how quickly every random feature suggestion is implemented. Wouldn’t it be cool to also have a label filter?

For that, I added a series of radio buttons:

Show:
<label><input type="radio" property="labels" name="labels" checked value=""> All</label>
<label><input type="radio" name="labels" value="bug"> Bugs only</label>
<label><input type="radio" name="labels" value="enhancement"> Enhancements only</label>

Then, I modified mv-source to also use this value in its API call: mv-source="https://api.github.com/repos/[repo]/issues?state=closed&sort=updated&labels=[labels]&per_page=100".

Note that when turning radio buttons into a Mavo property you only use the property attribute on the first one. This is important because Mavo has special handling when you use the property attribute with the same name multiple times in the same group, which we don’t want here. You can add the property attribute on any of the radio buttons, it doesn’t have to be the first. Just make sure it’s only one of them.

Then I became greedy: Why not also allow filtering by custom labels too? So I added another radio with an input:

Show:
<label><input type="radio" property="labels" name="labels" checked value=""> All</label>
<label><input type="radio" name="labels" value="bug"> Bugs only</label>
<label><input type="radio" name="labels" value="enhancement"> Enhancements only</label>
<label><input type="radio" name="labels" value="[customLabel]"> Label <input property="customLabel"></label>

Note that since this is a text field, when the last value is selected, we’d have the same problem as we did with the repo input: Every keystroke would fire a new request. We can solve this in the same way as we solved it for the repo property, by having an intermediate property and only setting labels when the form is actually submitted:

Show:
<label><input type="radio" property="labelFilter" name="labels" checked value=""> All</label>
<label><input type="radio" name="labels" value="bug"> Bugs only</label>
<label><input type="radio" name="labels" value="enhancement"> Enhancements only</label>
<label><input type="radio" name="labels" value="[customLabel]"> Label <input property="customLabel"></label>
<meta property="labels" content="">

Adding label autocomplete

Since we now allow filtering by a custom label, wouldn’t it be cool to allow autocomplete too? HTML allows us to offer autocomplete in our forms via <datalist> and we can use Mavo to populate the contents!

First, we add a <datalist> and link it with our custom label input, like so:

<label><input type="radio" name="labels" value="[customLabel]"> Label <input property="customLabel" list="label-suggestions"></label>
<datalist id="label-suggestions">
</datalist>

Currently, our suggestion list is empty. How do we populate it with the labels that have actually been used in this repo? Looking at the API documentation, we see that each returned issue has a labels field with its labels as an object, and each of these objects has a name field with the textual label. This means that if we use issue.labels.name in Mavo outside of the issues collection, we get a list with all of these values, which we can then use to populate our <datalist> by passing it on to mv-value which allows us to create dynamic collections:

<label><input type="radio" name="labels" value="[customLabel]"> Label <input property="customLabel" list="label-suggestions"></label>
<datalist id="label-suggestions">
	<option mv-multiple mv-value="unique(issue.labels.name)"></option>
</datalist>

Note that we also used unique() to eliminate duplicates, since otherwise each label would appear as many times as it is used.

See the Pen Issue Closing App - Tutorial Step 6 by Lea Verou (@leaverou) on CodePen.

Adding a visual summary graphic

Now that we got the functionality down, we can be a little playful and add some visual flourish. How about a bar chart that summarizes the proportion of long vs short vs normal closing times? We start by setting the CSS variables we are going to need for our graphic, i.e. the number of issues in each category:

<summary style="--short: [count(timeToClose < day())]; --long: [count(timeToClose > month())]; --total: [count(issue)];">
	Based on [count(issue)] most recently updated issues
</summary>

Then, we draw our graphic:

summary::before {
	content: "";
	position: fixed;
	bottom: 0;
	left: 0;
	right: 0;
	z-index: 1;
	height: 5px;
	background: linear-gradient(to right, var(--short-color) calc(var(--short, 0) / var(--total) * 100%), hsl(220, 10%, 75%) 0, hsl(220, 10%, 75%) calc(100% - var(--long, 0) / var(--total) * 100%), var(--long-color) 0) bottom / auto 100% no-repeat border-box;
}

Now, wouldn’t it be cool to also show a small pie chart next to the heading, if conic gradients are supported so we can draw it? The color stops would be the same, so we define a --summary-stops variable on summary, so we can reuse them across both gradients:

summary {
	--summary-stops: var(--short-color) calc(var(--short, 0) / var(--total) * 100%), hsl(220, 10%, 75%) 0, hsl(220, 10%, 75%) calc(100% - var(--long, 0) / var(--total) * 100%), var(--long-color) 0;
}

summary::before { content: ""; position: fixed; bottom: 0; left: 0; right: 0; z-index: 1; height: 5px; background: linear-gradient(to right, var(–summary-stops)) bottom / auto 100% no-repeat border-box; }

@supports (background: conic-gradient(red, red)) { summary::after { content: ""; display: inline-block; vertical-align: middle; width: 1.2em; height: 1.2em; margin-left: .3em; border-radius: 50%; background: conic-gradient(var(–summary-stops)); } }

See the Pen Issue Closing App - Tutorial Step 7 by Lea Verou (@leaverou) on CodePen.


Utility: Convert SVG path to all-relative or all-absolute commands

2 min read 0 comments Report broken page

I like hand-editing my SVGs. Often I will create an initial version in Illustrator, and then export and continue with hand editing. Not only is it a bit of a meditative experience and it satisfies my obsessive-compulsive tendencies to clean up the code, it has actual practical benefits when you need to make certain changes or introduce animation. Some things are easier to do in a GUI, and others are easier to do in code, and I like having the flexibility to pick which one fits my use case best.

However, there was always a thing that was a PITA: modifying paths. Usually if I need anything more complicated than just moving them, I’d do it in Illustrator, but even moving them can be painful if they are not all relative (and no, I don’t like introducing pointless transforms for things that should really be in the d attribute).

For example, this was today’s result of trying to move an exported “a” glyph from Raleway Bold by modifying its first M command:

Trying to move a path by changing its first M command when not all of its commands are relative.

This happened because even though most commands were exported as relative, several were not and I had not noticed. I have no idea why some commands were exported as absolute, it seems kind of random.

When all commands are relative, moving a path is as simple as manipulating its initial M command and the rest just adapts, because that’s the whole point of relative commands. Same with manipulating every other part of the path, the rest of it just adapts. It’s beautiful. I honestly have no idea why anybody would favor absolute commands. And yet, googling “convert SVG path to relative” yields one result, whereas there are plenty of results about converting paths to absolute. No idea why that’s even desirable, ever (?).

I remembered I had come across that result before. Thankfully, there’s also a fiddle to go with it, which I had used in the past to convert my path. I love it, it uses this library called Snap.svg which supports converting paths to relative as a just-add-water utility method. However, that fiddle is a quick demo to answer a StackOverflow question, so the UI is not super pleasant to use (there is no UI: you just manipulate the path in the SVG and wait for the fiddle to run). This time around, I needed to convert multiple paths, so I needed a more efficient UI.

So I created this demo which is also based on Snap.svg, but has a slightly more efficient UI. You just paste your path in a textarea and it both displays it and instantly converts it to all-relative and all-absolute paths (also using Snap.svg). It also displays both your original path and the two converted ones, so you can make sure they still look the same. It even follows a pending-delete pattern so you can just focus on the output textarea and hit Cmd-C in one fell swoop.

I wasn’t sure about posting this or just tweeting it (it literally took less than 30 minutes — including this blog post — and I tend to only post small things like that on my twitter), but I thought it might be useful to others googling the same thing, so I may as well post it here for posterity. Enjoy!


ReferenceError: x is not defined?

2 min read 0 comments Report broken page

Today for a bit of code I was writing, I needed to be able to distinguish “x is not defined” ReferenceErrors from any other error within a try...catch block and handle them differently.

Now I know what you’re thinking. Trying to figure out exactly what kind of error you have programmatically is a well-known fool’s errand. If you express a desire to engage in such a risky endeavor, any JS veteran in sight will shake their head in remembrance of their early days, but have the wisdom to refrain from trying to convince you otherwise; they know that failing will teach you what it taught them when they were young and foolish enough to attempt such a thing.

Despite writing JS for 13 years, today I was feeling adventurous. “But what if, just this once, I could get it to work? It’s a pretty standard error message! What if I tested in so many browsers that I would be confident I’ve covered all cases?”

I made a simple page on my server that just prints out the error message written in a way that would maximize older browser coverage. Armed with that, I started visiting every browser in my BrowserStack account. Here are my findings for anyone interested:

  • Chrome (all versions, including mobile): x is not defined
  • Firefox (all versions, including mobile): x is not defined
  • Safari 4-12 : Can't find variable: x
  • Edge (16 - 18): 'x' is not defined
  • Edge 15: 'x' is undefined
  • IE6-11 and Windows Phone IE: 'x' is undefined
  • UC Browser (all versions): x is not defined
  • Samsung browser (all versions): x is not defined
  • Opera Mini and Pre-Chromium Opera: Undefined variable: x

Even if you, dear reader, are wise enough to never try and detect this error, I thought you may find the variety (or lack thereof) above interesting.

I also did a little bit of testing with a different UI language (I picked Greek), but it didn’t seem to localize the error messages. If you’re using a different UI language, please open the page above and if the message is not in English, let me know!

In the end, I decided to go ahead with it, and time will tell if it was foolish to do so. For anyone wishing to also dabble in such dangerous waters, this was my checking code:

if (e instanceof ReferenceError
    && /is (not |un)defined$|^(Can't find|Undefined) variable/.test(e.message)) {
    // do stuff
}

Found any cases I missed? Or perhaps you found a different ReferenceError that would erroneously match the regex above? Let me know in the comments!

One thing that’s important to note is that even if the code above is bulletproof for today’s browser landscape, the more developers that do things like this, the harder it is for browser makers to improve these error messages. However, until there’s a better way to do this, pointing fingers at developers for wanting to do perfectly reasonable things, is not the solution. This is why HTTP has status codes, so we don’t have to string match on the text. Imagine having to string match “Not Found” to figure out if a request was found or not! Similarly, many other technologies have error codes, so that different types of errors can be distinguished without resulting to flimsy string matching. I’m hoping that one day JS will also have a better way to distinguish errors more precisely than the general error categories of today, and we’ll look back to posts like this with a nostalgic smile, being so glad we don’t have to do crap like this ever again.


Refresh CSS Bookmarklet v2

2 min read 0 comments Report broken page

Almost 11 years ago, Paul Irish posted this brilliant bookmarklet to refresh all stylesheets on the current page. Despite the amount of tools, plugins, servers to live reload that have been released over the years, I’ve always kept coming back to it. It’s incredibly elegant in its simplicity. It works everywhere: locally or remotely, on any domain and protocol. No need to set up anything, no need to alter my process in any way, no need to use a specific local server or tool. It quietly just accepts your preferences and workflow instead of trying to change them. Sure, it doesn’t automatically detect changes and reload, but in most cases, I don’t want it to.

I’ve been using this almost daily for a decade and there’s always been one thing that bothered me: It doesn’t work with iframes. If the stylesheet you’re editing is inside an iframe, tough luck. If you can open the frame in a new tab, that works, but often that’s nontrivial (e.g. the frame is dynamically generated). After dealing with this issue today once more, I thought “this is just a few lines of JS, why not fix it?”.

The first step was to get Paul’s code in a readable format, since the bookmarklet is heavily minified:

(function() {
	var links = document.getElementsByTagName('link');
	for (var i = 0; i < links.length; i++) {
		var link = links[i];
		if (link.rel.toLowerCase().match(/stylesheet/) && link.href) {
			var href = link.href.replace(/(&|%5C?)forceReload=\d+/, '');
			link.href = href + (href.match(/\?/) ? '&' : '?') + 'forceReload=' + (new Date().valueOf())
		}
	}
})()

Once I did that, it became obvious to me that this could be shortened a lot; the last 10 years have been wonderful for JS evolution!

(()=>{
	for (let link of Array.from(document.querySelectorAll("link[rel=stylesheet][href]"))) {
		var href = new URL(link.href, location);
		href.searchParams.set("forceReload", Date.now());
		link.href = href;
	}
})()

Sure, this reduces browser support a bit (most notably it excludes IE11), but since this is a local development tool, that’s not such a big problem.

Now, let’s extend this to support iframes as well:

{
	let $$ = (selector, root = document) => Array.from(root.querySelectorAll(selector));

let refresh = (document) => { for (let link of $$("link[rel=stylesheet][href]", document)) { let href = new URL(link.href); href.searchParams.set("forceReload", Date.now()); link.href = href; }

for (let iframe of $$("iframe", document)) { iframe.contentDocument && refresh(iframe.contentDocument); } }

refresh(); }

That’s it! Do keep in mind that this will not work with cross-origin iframes, but then again, you probably don’t expect it to in that case.

Now all we need to do to turn it into a bookmarklet is to prepend it with javascript: and minify the code. Here you go:

🔄 CSS v2

Hope this is useful to someone else as well :) Any improvements are always welcome!

Credits

  • Paul Irish, for the original bookmarklet
  • MaurĂ­cio Kishi, for making the iframe traversal recursive (comment)

Easy Dynamic Regular Expressions with Tagged Template Literals and Proxies

3 min read 0 comments Report broken page

If you use regular expressions a lot, you probably also create them from existing strings that you first need to escape in case they contain special characters that need to be matched literally, like $ or +. Usually, a helper function is defined (hopefully this will soon change as RegExp.escape() is coming!) that basically looks like this:

var escapeRegExp = s => s.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");

and then regexps are created by escaping the static strings and concatenating them with the rest of the regex like this:

var regex = RegExp(escapeRegExp(start) + '([\\S\\s]+?)' + escapeRegExp(end), "gi")

or, with ES6 template literals, like this:

var regex = RegExp(`${escapeRegExp(start)}([\\S\\s]+?)${escapeRegExp(end)}`, "gi")

(In case you were wondering, this regex is taken directly from the Mavo source code)

Isn’t this horribly verbose? What if we could define a regex with just a template literal (`${start}([\\S\\s]+?)${end}` for the regex above) and it just worked? Well, it turns out we can! If you haven’t seen tagged template literals before, I suggest you click that MDN link and read up. Basically, you can prepend an ES6 template literal with a reference to a function and the function accepts the static parts of the string and the dynamic parts separately, allowing you to operate on them!

So, what if we defined such a function that returns a RegExp object and escapes the dynamic parts? Let’s try to do that:

var regexp = (strings, ...values) => {
	return RegExp(strings[0] + values.map((v, i) => escapeRegExp(v) + strings[i+1]).join(""))
};

And now we can try it in the console:

> regexp`^${'/*'}([\\S\\s]+?)${'*/'}`;
< /^\/\*([\S\s]+?)\*\//

Won’t somebody, please, think of the flags?!

This is all fine and dandy, but how do we specify flags? Note that the original regexp had flags (“gi”). The tagged template syntax doesn’t really allow us to pass in any additional parameters. However, thanks to functions being first-class objects in JS, we can have a function that takes the flags in as parameters and returns a function that generates regexps with the right flags:

var regexp = flags => {
	return (strings, ...values) => {
		var pattern = strings[0] + values.map((v, i) => escapeRegExp(v) + strings[i+1]).join("")
		return RegExp(pattern, flags);
	}
};

And now we can try it in the console:

> regexp("gi")`^${'/*'}([\\S\\s]+?)${'*/'}`;
< /^\/\*([\S\s]+?)\*\//gi

This works nice, but now even if we don’t want any flags, we can’t use the nice simple syntax we had earlier, we need to include a pair of empty parens:

> regexp()`^${'/*'}([\\S\\s]+?)${'*/'}`;
< /^\/\*([\S\s]+?)\*\//

Can we have our cake and eat it too? Can we have the short parenthesis-less syntax when we have no flags, and still be able to specify flags? Of course! We can check the arguments we have and either return a function, or call the function. If our function is used as a tag, the first argument will be an array (thanks Roman!). If we’re expecting it to return a function, the first argument would be a string: the flags. So, let’s try this approach!

var regexp = (...args) => {
	var ret = (flags, strings, ...values) => {
		var pattern = strings[0] + values.map((v, i) => escapeRegExp(v) + strings[i+1]).join("");
		return RegExp(pattern, flags);
	};

if (Array.isArray(args[0])) { // Used as a template tag return ret("", …args); }

return ret.bind(undefined, args[0]); };

And now we can try it in the console and verify that both syntaxes work:

> regexp("gi")`^${'/*'}([\\S\\s]+?)${'*/'}`;
< /^\/\*([\S\s]+?)\*\//gi
regexp`^${'/*'}([\\S\\s]+?)${'*/'}`;
< /^\/\*([\S\s]+?)\*\//

Even nicer syntax, with proxies!

Is there a better way? If this is not super critical for performance, we could use proxies to return the right function with a template tag like regexp.gi, no parentheses or quotes needed and the code is actually shorter too:

var _regexp = (flags, strings, ...values) => {
	var pattern = strings[0] + values.map((v, i) => escapeRegExp(v) + strings[i+1]).join("");
	return RegExp(pattern, flags);
};
var regexp = new Proxy(_regexp.bind(undefined, ""), {
	get: (t, property) => _regexp.bind(undefined, property)
});

And now we can try it in the console, both with and without flags!

> regexp.gi`^${'/*'}([\\S\\s]+?)${'*/'}`;
< /^\/\*([\S\s]+?)\*\//gi
> regexp`^${'/*'}([\\S\\s]+?)${'*/'}`;
< /^\/\*([\S\s]+?)\*\//

That’s some beauty right there! ?

PS: If you liked this, take a look at this mini-library by Dr. Axel Rauschmayer that uses a similar idea and turns it into a library that does more than just escaping strings (different syntax for flags though, they become part of the template string, like in PHP)


Never forget type="button" on generated buttons!

2 min read 0 comments Report broken page

I just dealt with one of the weirdest bugs and thought you may find it amusing too.

In one of my slides for my upcoming talk “Even More CSS Secrets”, I had a Mavo app on a <form>, and the app included a collection to quickly create a UI to manage pairs of values for something I wanted to calculate in one of my live demos. A Mavo collection is a repeatable HTML element with affordances to add items, delete items, move items etc. Many of these affordances are implemented via <button> elements generated by Mavo.

Normally, hitting Enter inside a text field within a collection adds a new item, as one would expect. However, I noticed that when I hit Enter inside any item, not only no item was added, but an item was being deleted, with the usual “Item deleted [Undo]” UI and everything!

At first I thought it was a bug with the part of Mavo code that adds items on Enter and deletes empty items on backspace, so I commented that out. Nope, still happening. I was already very puzzled, since I couldn’t remember any other part of the codebase that deletes items in response to keyboard events.

So, I added breakpoints on the delete(item) method of Mavo.Collection to inspect the call stack and see how execution got there. Turned out, it got there via a normal …click event on the actual delete button! What fresh hell was this? I never clicked any delete button!

And then it dawned on me: <button> elements with no type attribute set are submit buttons by default! Quote from spec: The missing value default and invalid value default are the Submit Button state.. This makes no difference in most cases, UNLESS you’re inside a form. The delete button of the first item had been turned into the de facto default submit button just because it was the first button in that form and it had no type!

I also remembered that regardless of how you submit a form (e.g. by hitting Enter on a single-line text field) it also fires a click event on the default submit button, because people often listen to that instead of the form’s submit event. Ironically, I was cancelling the form’s submit event in my code, but it still generated that fake click event, making it even harder to track down as no form submission was actually happening.

The solution was of course to go through every part of the Mavo code that generates buttons and add type=“button” to them. I would recommend this to everyone who is writing libraries that will operate in unfamiliar HTML code. Most of the time a type-less <button> will work just fine, but when it doesn’t, things get really weird.


Responsive tables, revisited

2 min read 0 comments Report broken page

Screenshot showing a table with 3 rows turning into 3 sets of key-value pairs

Many people have explored responsive tables. The usual idea is turning the table into key-value pairs so that cells become rows and there are only 2 columns total, which fit in any screen. However, this means table headers need to now be repeated for every row. The current ways to do that are:

  • Duplicating content in CSS or via a data-* attribute, using generated content to insert it before every row.
  • Using a definition list which naturally has duplicated <dt>s, displaying it as a table in larger screens.

A few techniques that go in an entirely different direction are:

  • Hiding non-essential columns in smaller screens
  • Showing a thumbnail of the table instead, and display the full table on click
  • Displaying a graph in smaller screens (e.g. a pie chart)

I think the key-value display is probably best because it works for any kind of table, and provides the same information. So I wondered, is there any way to create it without duplicating content either in the markup or in the CSS? After a bit of thinking, I came up with two ways, each with their own pros and cons.

Both techniques are very similar: They set table elements to display: block; so that they behave like normal elements and duplicate the <thead> contents in two different ways:

  1. Using text-shadow and creating one shadow for each row
  2. Using the element() function to duplicate the entire thead, styles and all.

Each method has its own pros and cons, but the following pros and cons apply to both:

  • Pros: Works with normal table markup
  • Cons:
    • All but the first set of headers are unselectable (since neither shadows nor element()-generated images are real text). However, keep in mind that the techniques based on generated content also have this problem — and for all rows. Also, that the markup screen readers see is the same as a normal table. However, it’s still a pretty serious flaw and makes this a hack. I’m looking forward to seeing more viable solutions.
    • Only works if none of the table cells wrap, since it depends on table cells being aligned with their headers.

Using text-shadow to copy text to other rows

  • Additional Pros: Works in every browser
  • Additional Cons: Max Number of rows needs to be hardcoded in the CSS, since each row needs another text shadow on <thead>. However, you can specify more shadows than needed, since overflow: hidden on the table prevents extra ones from showing up. Also, number of columns needs to be specified in the CSS (the --cols variable).

Demo

Using element() to copy the entire <thead> to other rows

  • Additional Cons: element() is currently only supported in Firefox :(

Demo


Quicker Storify export

2 min read 0 comments Report broken page

If you’ve used Storify, you probably know by now it’s closing down soon. They have an FAQ up to help people with the transition which explains that to export your content you need to…

  1. Log in to Storify at www.storify.com.
  2. Mouse over the story that contains content you would like to export and select “View.”
  3. Click on the ellipses icon and select “Export.”
  4. Choose your preferred format for download.
  5. To save your content and linked assets in HTML, select - File > Save as > Web Page, Complete. To export your content to PDF, select Export to HTML > File > Print > Save as PDF.
  6. Repeat the process for each story whose content you would like to preserve.

So I started doing that. I wasn’t sure if JSON or HTML would be more useful to me, so I was exporting both. It was painful. Each export required 3 page loads, and they were slow. After 5 stories, I started wondering if there’s a quicker way. I’m a programmer after all, my job is to automate things. However, I also didn’t want to spend too long on that, since I only had 40 stories, so the effort should definitely not be longer than it would have taken to manually export the remaining 35 stories.

I noticed that the HTML and JSON URLs for each story could actually be recreated by using the slug of the Story URL:

https://storify.com/LeaVerou/**css-variables-var-subtitle-cssconf-asia**.html https://api.storify.com/v1/stories/LeaVerou/**css-variables-var-subtitle-cssconf-asia**

The bold part is the only thing that changes. I tried that with a different slug and it worked just fine. Bingo! So I could write a quick console script to get all these URLs and open them in separate tabs and then all I have to do is go through each tab and hit Cmd + S to save. It’s not perfect, but it took minutes to write and saved A LOT of time.

Following is the script I wrote. Go to your profile page, click “Show more” and scroll until all your stories are visible, then paste it into the console. You will probably need to do it twice: once to disable popup blocking because the browser rightfully freaks out when you try to open this many tabs from script, and once to actually open all of them.

var slugs = [... new Set($$(".story-tile").map(e => e.dataset.path))]
slugs.forEach(s => { open(`https://api.storify.com/v1/stories/${s}`); open(`https://storify.com/${s}.html`) })

This gets a list of all unique (hence the [...new Set(array)]) slugs and opens both the JSON and HTML export URLs in new tabs. Then you can go through each tab and save.

You will notice that the browser becomes REALLY SLOW when you open this many tabs (in my case 41 stories × 2 tabs each = 82 tabs!) so you may want to do it in steps, by using array.slice(). Also, if you don’t want to save the HTML version, the whole process becomes much faster, the HTML pages took AGES to load and kept freezing the browser.

Hope this helps!

PS: If you’re content with your data being held hostage by a different company, you could also use this tool by Wakelet. I’ve done that too, but I also wanted to own my data as well.


Free Intro to Web Development slides (with demos)

2 min read 0 comments Report broken page

This semester I’m teaching 6.813 User Interface Design and Implementation at MIT, as an instructor.

Many of the assignments of this course include Web development and the course included two 2-hour labs to introduce students to these technologies. Since I’m involved this year, I decided to make new labs from scratch and increase the number of labs from 2 to 3. Even so, trying to decide what to include and what not to from the entirety of web development in only 6 hours was really hard, and I still feel I failed to include important bits.

Since many people asked me for the slides on Twitter, I decided to share them. You will find my slides here and an outline of what is covered is here. These slides were also the supporting material the students had on their own laptops and often they had to do exercises in them.

The audience for these slides is beginners in Web development but technical otherwise — people who understand OOP, trees, data structures and have experience in at least one C-like programming language.

Some demos will not make sense as they were live coded, but I included notes (top right or bottom left corner) about what was explained in each part.

Use the arrow keys to navigate. It is also quite big, so do not open this on a phone or on a data plan.

If the “Open in new Tab” button opens a tab which then closes immediately, disable Adblock.

From some quick testing, they seem to work in Firefox and Safari, but in class we were using an updated version of Chrome (since we were talking about developer tools, we needed to all have the same UI), so that’s the browser I’d recommend since they were tested much more there.

I’m sharing them as-is in case someone else finds them useful. Please do not bug me if they don’t work in your setup, or if you do not find them useful or whatever**.** If they don’t tickle your fancy, move on. I cannot provide any support or fixes. If you want to help fix the issue, you can submit a pull request, but be warned: most of the code was written under extreme time pressure (I had to produce this 6 times as fast as I usually need to make talks), so is not my finest moment.

If you want to use them to teach other people that’s fine as long as it’s a non-profit event.

[gallery columns=“2” size=“medium” ids=“2756,2757,2755,2754”]


Different remote and local resource URLs, with Service Workers!

4 min read 0 comments Report broken page

I often run into this issue where I want a different URL remotely and a different one locally so I can test my local changes to a library. Sure, relative URLs work a lot of the time, but are often not an option. Developing Mavo is yet another example of this: since Mavo is in a separate repo from mavo.io (its website) as well as test.mavo.io (the testsuite), I can’t just have relative URLs to it that also work remotely. I’ve been encountering this problem way too frequently pretty much since I started in web development. In this post, will describe all solutions and workarounds I’ve used over time for this, including the one I’m currently using for Mavo: Service Workers!

The manual one

Probably the first solution everyone tries is doing it manually: every time you need to test, you just change the URL to a relative, local one and try to remember to change it back before committing. I still use this in some cases, since us developers are a lazy bunch. Usually I have both and use my editor’s (un)commenting shortcut for enabling one or the other:

<script src="https://get.mavo.io/mavo.js"></script>
<!--<script src="../mavo/dist/mavo.js"></script>-->

However, as you might imagine, this approach has several problems, the worst of which is that more than once I forgot and committed with the active script being the local one, which resulted in the remote website totally breaking. Also, it’s clunky, especially when it’s two resources whose URLs you need to change.

The JS one

This idea uses a bit of JS to load the remote URL when the local one fails to load.

<script src="http://localhost:8000/mavo/dist/mavo.js" onerror="this.src='https://get.mavo.io/mavo.js'"></script>

This works, and doesn’t introduce any cognitive overhead for the developer, but the obvious drawback is that it slows things down for the server since a request needs to be sent and fail before the real resource can be loaded. Slowing things down for the local case might be acceptable, even though undesirable, but slowing things down on the remote website for the sake of debugging is completely unacceptable. Furthermore, this exposes the debugging URLs in the HTML source, which gives me a bit of a knee jerk reaction.

A variation of this approach that doesn’t have the performance problem is:

<script>
{
 let host = location.hostname == "localhost"? 'http://localhost:8000/dist' : 'https://get.mavo.io';
 document.write(`<script src="${host}/mavo.js"></scr` + `ipt>`);
}
</script>

This works fine, but it’s very clunky, especially if you have to do this multiple times (e.g. on multiple testing files or demos).

The build tools one

The solution I was following up to a few months ago was to use gulp to copy over the files needed, and then link to my local copies via a relative URL. I would also have a gulp.watch() that monitors changes to the original files and copies them over again:

gulp.task("copy", function() {
	gulp.src(["../mavo/dist/**/*"])
		.pipe(gulp.dest("mavo"));
});

gulp.task("watch", function() { gulp.watch(["…/mavo/dist/*"], ["copy"]); });

This worked but I had to remember to run gulp watch every time I started working on each project. Often I forgot, which was a huge source of confusion as to why my changes had no effect. Also, it meant I had copies of Mavo lying around on every repo that uses it and had to manually update them by running gulp, which was suboptimal.

The Service Worker one

In April, after being fed up with having to deal with this problem for over a decade, I posted a tweet:

@MylesBorins replied (though his tweet seems to have disappeared) and suggested that perhaps Service Workers could help. In case you’ve been hiding under a rock for the past couple of years, Service Workers are a new(ish) API that allows you to intercept requests from your website to the network and do whatever you want with them. They are mostly promoted for creating good offline experiences, though they can do a lot more.

I was looking for an excuse to dabble in Service Workers for a while, and this was a great one. Furthermore, browser support doesn’t really matter in this case because the Service Worker is only used locally.

The code I ended up with looks like this in a small script called sitewide.js, which, as you may imagine, is used sitewide:

(function() {

if (location.hostname !== "localhost") { return; }

if (!self.document) { // We’re in a service worker! Oh man, we’re living in the future! ?? self.addEventListener("fetch", function(evt) { var url = evt.request.url;

if (url.indexOf("get.mavo.io/mavo.") > -1 || url.indexOf("dev.mavo.io/dist/mavo.") > -1) { var newURL = url.replace(/.+?(get|dev).mavo.io/(dist/)?/, "http://localhost:8000/dist/&quot;) + "?" + Date.now();

var response = fetch(new Request(newURL), evt.request) .then(r => r.status < 400? r : Promise.reject()) // if that fails, return original request .catch(err => fetch(evt.request));

evt.respondWith(response); } });

return; }

if ("serviceWorker" in navigator) { // Register this script as a service worker addEventListener("load", function() { navigator.serviceWorker.register("sitewide.js"); }); }

})();

So far, this has worked more nicely than any of the aforementioned solutions and allows me to just use the normal remote URLs in my HTML. However, it’s not without its own caveats:

  • Service Workers are only activated on a cached pageload, so the first one uses the remote URL. This is almost never a problem locally anyway though, so I’m not concerned about it much.
  • The same origin restriction that service workers have is fairly annoying. So, I have to copy the service worker script on every repo I want to use this on, I cannot just link to it.
  • It needs to be explained to new contributors since most aren’t familiar with Service Workers and how they work at all.

Currently the URLs for both local and remote are baked into the code, but it’s easy to imagine a mini-library that takes care of it as long as you include the local URL as a parameter (e.g. https://get.mavo.io/mavo.js?local=http://localhost:8000/dist/mavo.js).

Other solutions

Solutions I didn’t test (but you may want to) include:

  • .htaccess redirect based on domain, suggested by @codepo8. I don’t use Apache locally, so that’s of no use to me.
  • Symbolic links, suggested by @aleschmidx
  • User scripts (e.g. Greasemonkey), suggested by @WebManWlkg
  • Modifying the hosts file, suggested by @LukeBrowell (that works if you don’t need access to the remote URL at all)

Is there any other solution? What do you do?


Introducing Mavo: Create web apps entirely by writing HTML!

1 min read 0 comments Report broken page

Today I finally released the project I’ve been working on for the last two years at MIT CSAIL: An HTML-based language for creating (many kinds of) web applications without programming or a server backend. It’s named Mavo after my late mother (Maria Verou), and is Open Source of course (yes, getting paid to work on open source is exactly as fun as it sounds).

It was the scariest release of my life, and have been postponing it for months. I kept feeling Mavo was not quite there yet, maybe I should add this one feature first, oh and this other one, oh and we can’t release without this one, surely! Eventually I realized that what I was doing had more to do with postponing the anxiety and less to do with Mavo reaching a stage where it can be released. After all, “if you’re not at least a bit embarrassed by what you release, you waited too long”, right?

The infamous Ship It Squrrel

So, there it is, I hope you find it useful. Read the post on Smashing Magazine or just head straight to mavo.io, read the docs, and play with the demos!

And do let me know what you make with it, no matter how small and trivial you may think it is, I would love to see it!


HTML APIs: What they are and how to design a good one

1 min read 0 comments Report broken page

I’m a strong believer in lowering the barrier of what it takes to create rich, interactive experiences and improving the user experience of programming. I wrote an article over at Smashing Magazine aimed at JavaScript library developers that want their libraries to be usable via HTML (i.e. without writing any JavaScript). Sounds interesting? Read it here.