There have been a couple of viral tweets about this lately, one from Adam Argyle and one from Mathias Bynes. This is a nice change that makes CSS a bit more clear. Before, every single color function actually needs two functions, one for transparency and one without, this eliminates that need and brings the syntax more-inline with CSS grammar overall.
Lemme make some code block from Mathias’ tweet here.
If you need IE 11 support, you can preprocess it (or not use it). PostCSS’s preset-envdoes it as well as the very specific plugin postcss-color-rgb (weird it doesn’t do HSL also).
If you don’t like it, you literally never need to use it. No browser will ever pull support for such an important feature.
The reason to switch is muscle memory and consistent-looking codebases as new color functions (e.g, lab, lch, and color) will only support this new syntax.
There is a weird hybrid between old and new. You can pass an opacity value to rgb() and it still works like rgb(255, 0, 0, 0.5);.
If you need it in Sass (which is apparently a pain to support), there is a weird workaround. I would guess Sass will get around to supporting it. If they can’t, this is the kind of barb that drives people away from projects.
Prettier, which is in the business of cleaning up your code from the perspective of spacing and syntax, could intervene here and convert syntax, but it’s not going to (the Prettier stance is to not change the AST).
I imagine DevTools will start showing colors in this format, which will drive adoption.
In programming, functions are a named portion of code that performs a specific task. An example of this could be a function written in JavaScript called sayWoof():
function sayWoof() { console.log("Woof!"); }
We can use this function later in our code, after we have defined our desired behavior. For this example, any time you type sayWoof() in your website or web app’s JavaScript it will print “Woof!” into the browser’s console.
Functions can also use arguments, which are slots for things like numbers or bits of text that you can feed into the function’s logic to have it modify them. It works like this in JavaScript:
function countDogs(amount) { console.log("There are " + amount + " dogs!"); }
Here, we have a function called countDogs() that has an argument called amount. When a number is provided for amount, it will take that number and add it to a pre-specified sentence. This lets us create sentences that tell us how many dogs we’ve counted.
countDogs(3); // There are 3 dogs! countDogs(276); // There are 276 dogs! countDogs("many"); // There are many dogs!
Some programming languages come with baked-in functions to help prevent you from having to reinvent the wheel for every new project. Typically, these functions are built to help make working with the main strengths and features of the language easier.
Take libraries, for example. Libraries are collections of opinionated code made to help make development faster and easier, effectively just curated function collections — think FitVids.js for creating flexible video elements.
Like any other programming language, CSS has functions. They can be inserted where you’d place a value, or in some cases, accompanying another value declaration. Some CSS functions even let you nest other functions within them!
Basics of CSS Functions
Unlike other programming languages, we cannot create our own functions in CSS, per se. That kind of logic is reserved for CSS selectors, which allow you to create powerful conditional styling rules.
As opposed to other programming languages — where the output of a function typically invisibly affects other logic down the line — the output of CSS functions are visual in nature. This output is used to control both layout and presentation of content. For example:
The CSS filter function drop-shadow() uses the arguments we provide it to create an orange outer glow effect on whatever it is applied to.
In the following demo, I have a JavaScript function named toggleOrangeGlow that toggles the application of the class .has-orange-glow on the CSS-Tricks logo. Combining this with a CSS transition, we’re able to create a cool glowing effect:
You may be familiar with some CSS functions, but the language has a surprisingly expansive list!
Much like any other technology on the web, different CSS functions have different levels of browser support. Make sure you research and test to ensure your experience works for everyone, and use things like @supports to provide quality alternate experiences.
Common CSS Functions
url()
url() allows you to link to other resources to load them. This can include images, fonts, and even other stylesheets. For performance reasons, it’s good practice to limit the things you load via url(), as each declaration is an additional HTTP request.
attr()
This function allows us to reach into HTML, snag an attribute’s content, and feed it to the CSS content property. You’ll commonly see attr() used in print stylesheets, where it is used to show the URL of a link after its text. Another great application of this function is using it to show the alt description of an image if it fails to load.
calc()
If there’s one function you should spend some time experimenting with, it’s calc(). This function takes two arguments and calculates a result from the operator (+, -, *, /) you supply it, provided those arguments are numbers with or without an accompanying unit.
Unlike CSS preprocessors such as Sass, calc() can mix units, meaning you can do things like subtract 6rem from 100%. calc() is also updated on the fly, so if that 100% represents a width, it’ll still work if that width changes. calc() can also accept CSS Custom Properties as arguments, allowing you an incredible degree of flexibility.
lang()
Including a lang attribute in your HTML is a really important thing to do. When present in your HTML, you’re able to use the lang() function to target the presence of the attribute’s value and conditionally apply styling based on it.
One common use for this selector is to set language-specific quotes, which is great for things like internationalization.
Clever designers and developers might also use it as a hook for styling translated versions of their sites, where cultural and/or language considerations mean there’s different perceptions about things like negative space.
:not()
This pseudo class selector will select anything that isn’t what you specify. For example, you could target anything that isn’t an image with body:not(img). While this example is dangerously powerful, scoping :not() to more focused selectors such as BEM’s block class can give you a great deal of versatility.
Currently, :not() supports only one selector for its argument, but support for multiple comma-separated arguments (e.g. div:not(.this, .that)) is being worked on!
CSS Custom Properties
var() is used to reference a custom property declared earlier in the document. It is incredibly powerful when combined with calc().
An example of this is declaring a custom property called --ratio: 1.618; in the root of the document, then invoking it later in our CSS to control line height: line-height: var(--ratio);.
Here, var() is a set of instructions that tells the browser, “Go find the argument called --ratio declared earlier in the document, take its value, and apply it here.”
Remember! calc() lets us dynamically adjust things on the fly, including the argument you supply via var().
This allows us to create things like modular scale systems directly in CSS with just a few lines of code. If you change the value of --ratio, the whole modular scale system will update to match.
In the following CodePen demo, I’ve done exactly just that. Change the value of --scale in the Pen’s CSS to a different number to see what I mean:
It’s also worth mentioning that JavaScript’s setProperty method can update custom properties in real time. This allows us to quickly and efficiently make dynamic changes to things that previously might have required a lot of complicated code to achieve.
Color Functions
Another common place you see CSS functions is when working with color.
rgb() and rgba()
These functions allow you to use numbers to describe the red (r), green (g), blue (b), and alpha (a) levels of a color. For example, a red color with a hex value of #fb1010 could also be described as rgba(251, 16, 16, 1). The red value, 251, is far higher than the green and blue values (16 and 16), as the color is mostly comprised of red information.
The alpha value of 1 means that it is fully opaque, and won’t show anything behind what the color is applied to. If we change the alpha value to be 0.5, the color will be 50% transparent. If you use an rgb() function instead of rgba(), you don’t have to supply an alpha value. This creates terser code, but prevents you from using transparency.
hsl() and hsla()
Similar to rgb() and rgba(), hsl() and hsla() are functions that allow you to describe color. Instead of using red, green, and blue, they use hue (h), saturation (s), and lightness (l).
In the upcoming CSS Color Module Level 4 spec, we can ignore the a portion of rgba() and hsla(), as well as the commas. Now, spaces are used to separate the rgb and hsl arguments, with an optional / to indicate an alpha level.
These selectors use specialized argument notation that specifies patterns of what to select. This allows you to do things like select every other element, every fifth element, every third element after the seventh element, etc.
Pseudo class selectors are incredibly versatile, yet often overlooked and under-appreciated. Many times, a thoughtful application of a few of these selectors can do the work of one or more node packages.
:nth-child()
nth-child() allows you to target one or more of the elements present in a group of elements that are on the same level in the Document Object Model (DOM) tree.
This pseudo class selector targets elements in a group of one or more elements that are on the same level in the DOM. It starts counting from the last element in the group and works backwards through the list of available DOM nodes.
:nth-last-of-type()
This pseudo class selector can target an element in a group of elements of a similar type. Much like :nth-last-child(), it starts counting from the last element in the group. Unlike :nth-last-child, it will skip elements that don’t apply as it works backwards.
:nth-of-type()
:nth-of-type() matches a specified collection of elements of a given type. For example, a declaration of img:nth-of-type(5) would target the fifth image on a page.
Animation Functions
Animation is an important part of adding that certain je ne sais quoi to your website or web app. Just remember to put your users’ needs first and honor theiranimation preferences.
Creating animations also requires controlling the state of things over time, so functions are a natural fit for making that happen.
This function is paired with the offset-path property. It allows you to “draw” a SVG path that other elements can be animated to follow.
Both Michelle Barker and Dan Wilson have published excellent articles that go into more detail about this approach to animation.
steps()
This relatively new function allows you to set the easing timing across an animation, which allows for a greater degree of control over what part of the animation occurs when. Dan Wilson has another excellent writeup of how it fits into the existing animation easing landscape.
Sizing and Scaling Functions
One common thing we do with animation is stretch and squash stuff. The following functions allow you to do exactly that. There is a catch, however: These CSS functions are a special subset, in that they can only work with the transform property.
scaleX(), scaleY(), scaleZ(), scale3d(), and scale()
Scaling functions let you increase or decrease the size of something along one or more axes. If you use scale3d() you can even do this in three dimensions!
translateX(), translateY(), translateZ(), translate3d(), and translate()
Translate functions let you reposition an element along one or more axes. Much like scale functions, you can also extend this manipulation into three dimensions.
perspective()
This function lets you adjust the appearance of an object to make it look like it is projecting up and out from its background.
rotateX(), rotateY(), rotateZ(), rotate3d(), and rotate()
Rotate functions let you swivel an element along one or more axes, much like grasping a ball and turning it around in your hand.
skewX(), skewY(), and skew()
Skew functions are a little different from scaling and rotation functions in that they apply a distortion effect relative to a single point. The amount of distortion is proportionate to the angle and distance declared, meaning that the further the effect continues in a direction the more pronounced it will be.
Jorge Moreno also did us all a favor and made a great tool called CSS Transform Functions Visualizer. It allows you to adjust sizing and scaling in real time to better understand how all these functions work together:
As responsible web professionals, we should be mindful of our users and the fact that they may not be using new or powerful hardware to view our content. Large and complicated animations may slow down the experience, or even cause the browser to crash in extreme scenarios.
This function adjusts how, um, bright something appears. Setting it to a low level will make it appear as if it has had a shadow cast over it. Setting it to a high level will blow it out, like an over-exposed photo.
blur()
If you’re familiar with Photoshop’s Gaussian Blur filter, you know how blur() works. The more of this you apply, the more indistinct the thing you apply it to will look.
contrast()
contrast() will adjust the degree of difference between the lightest and darkest parts of what is applied to.
grayscale()
grayscale() removes the color information from what it is applied to. Remember that this isn’t an all-or-nothing affair! You can apply a partial grayscale effect to make something look weathered or washed out.
While invert() can be used to make something look like a photo negative, my favorite technique is to use it in a inverted colors media query to invert inverted images and video:
This ensures that image and video content looks the way it should, regardless of a user’s expressed browsing mode preferences.
opacity()
This function controls how much of the background is visible through the element (and child elements) the function is applied to.
An element that has 0% opacity will be completely transparent, although it will still be present in the DOM. If you need to remove an object completely, use other techniques such as the hidden attribute.
saturate()
Applying this filter can enhance, or decrease the intensity of the color of what it is applied to. Enhancing an image’s saturation is a common technique photographers use to fix underexposed photos.
sepia()
There are fancier ways to describe this, but realistically it’s a function that makes something look like it’s an old-timey photograph.
drop-shadow()
A drop shadow is a visual effect applied to an object that makes it appear like it is hovering off of the page. There’s a bit of a trick here, in that CSS also allows you to apply drop shadow effects to text and elements. It’s also distinct from the box-shadow property is that it applies drop shadows to the shape of an element rather than the actual box of an element.
Skilled designers and developers can take advantage of this to create complicated visual effects.
hue-rotate()
When a class with a declaration containing hue-rotate() is applied to an element, each pixel used to draw that element will have it’s hue valued shifted by the amount you specify. hue-rotate()‘s effect is applied to each and every pixel it is applied to, so all colors will update relative to their hue value’s starting point.
This can create a really psychedelic effect when applied to things that contain a lot of color information, such as photos.
This effect was created by skillful application of SVG filter effects.
Gradient Functions
Gradients are created when you transition one color to one or more other colors. They are workhorses of modern user interfaces — skilled designers and developers use them to lend an air of polish and sophistication to their work.
Gradient functions allow you to specify a whole range of properties, including:
Color values,
The position on the gradient area where that color comes in,
What angle the gradient is positioned at.
And yes, you guessed it: the colors we use in a gradient can be described using CSS color functions!
linear-gradient() and repeating-linear-gradient()
Linear gradients apply the color transformation in a straight line, from one point to another — this line can be set at an angle as well. In cases where there’s more area than gradient, using repeating-linear-gradient() will, er, repeat the gradient you described until all the available area has been filled.
radial-gradient() and repeating-radial-gradient()
Radial gradients are a lot like linear gradients, only instead of a straight line, color transformations radiate outward from a center point. They’re oftentimes used to create a semitransparent screen to help separate a modal from the background it is placed over.
An adjustable conic gradient donut chart made by Ana Tudor
Grid Functions
CSS Grid is a relatively new feature of the language. It allows us to efficiently create adaptive, robust layouts for multiple screen sizes.
It’s worth acknowledging our roots. Before Grid, layout in CSS was largely a series of codified hacks to work with a language originally designed to format academic documents. Grid’s introduction is further acknowledgement that the language’s intent has changed.
Modern CSS is an efficient, fault-tolerant language for controlling presentation and layout across a wide range of device form factors. Equipped with Grid and other properties like flexbox, we’re able to create layouts that would have been impossible to create in earlier iterations of CSS.
Grid introduces the following CSS functions to help you use it.
fit-content()
This function “clamps” the size of grid rows or columns, letting you specify a maximum size a grid track can expand to. fit-content() accepts a range of values, but most notable among them are min-content and max-content. These values allow you to tie your layout to the content it contains. Impressive stuff!
You can loop through patterns of grid column and rows using repeat(). This is great for two scenarios:
When you do know how many rows or columns you need, but typing them out would be laborious. A good example of this would be constructing the grid for a calendar.
When you don’t know how many rows or columns you need. Here, you can specify a template that the browser will honor as it propagates content into your layout.
Shape Functions
Like filter() and transform(), shape CSS functions only work with one property: clip-path. This property is used to mask portions of something, allowing you to create all sorts of cool effects.
circle()
This function creates a circular shape for your mask, allowing you to specify its radius and position.
ellipse()
Like circle(), ellipse() will draw a rounded shape, only instead of a perfect circle, ellipse() lets you construct an oblong mask.
inset()
This function will mask out a rectangle inside of the element you apply it to.
polygon()
With polygon(), you are able to specify an arbitrary number of points, allowing you to draw complicated shapes. polygon() also takes an optional fill-rule argument, which specifies which part of the shape is the inside part.
Miscellaneous Functions
These are the un-categorizable CSS functions, things that don’t fit neatly elsewhere.
element()
Ever pointed a camera at its own video feed? That’s sort of what element() does. It allows you to specify the ID of another element to create an “image” of what that element looks like. You can then apply other CSS to that image, including stuff like CSS filters!
It might take a bit to wrap your head around the concept — and it has some support concerns — but element() is a potentially very powerful in the right hands.
Here, she’s created a minimap for reading through a longform article:
image-set()
This function allows you to specify a list of images for the browser to select for a background image, based on what it knows about the capabilities of its display and its connection speed. It is analogous to what you would do with the srcset property.
::slotted()
This is a pseudo-element selector used to target elements that have been placed into a slot inside a HTML template. ::slotted() is intended to be used when working with Web Components, which are custom, developer-defined HTML elements.
Not Ready for Prime Time
Like any other living programming language, CSS includes features and functionality that are actively being worked on.
This function enables Alternate Annotation Forms, characters reserved for marking up things like notation and annotation. These characters typically will be outlined with a circle, square, or diamond shape.
Not many typefaces contain Alternate Annotation Forms, so it’s good to check to see if the typeface you’re using includes them before trying to get annotation() to work. Tools such as Wakamai Fondue can help with that.
When you create an ordered list in HTML, the browser will automatically generate numbers for you and place them before your list item content. These pieces of browser-generated list content are called counters.
By using a combination of the ::marker pseudo-element selector, the content property, and the counter() function, we can control the content and presentation of the counters on an ordered list. For browsers that don’t support counter() or counters() yet, you still get a decent experience due to the browser automatically falling back to its generated content:
For situations where you have nested ordered lists, the counters() function allows a child ordered list to access its parent. This allows us to control their content and presentation. If you want to learn more about the power of ::marker, counter(), and counters(), you can read “CSS Lists, Markers, And Counters” by Rachel Andrew.
cross-fade()
This function will allow you to blend one background image into one or more other background images. Its proposed syntax is similar to gradient functions, where you can specify the stops where images start and end.
dir()
This function allows you to flip the orientation of a language’s reading order. For English, that means a left-to-right (ltr) reading order gets turned into right-to-left (rtl). Only Firefox currently has support for dir(), but you can achieve the same effect in Chromium-based browsers by using an attribute selector such as [dir="rtl"].
That being said, device sniffing is a fallacious affair — you shouldn’t consider env() a way to cheat it. Instead, use it as intended: to make sure your design works for devices that impose unique hardware constraints on the viewport.
has()
has() is a relational pseudo-class that will target an element that contains another element, provided there is at least one match in the HTML source. An example of this is be a:has(> img), which tells the browser to target any link that contains an image.
Interestingly, has() is currently being proposed as CSS you can only write in JavaScript. If I were to wager a guess as to why this is, it is to scope the selector for performance reasons. With this approach has() is triggered only after the browser has been told to process conditional logic, and therefore query the state of things.
image()
This function will let you insert either a static image (referenced with url(), or draw one dynamically via gradients and element().
The square root of the sum of squares of its arguments: hypot()
Power: pow()
I’m especially excited to see what people who are more clever than I am will do with these functions, especially for things like animation!
clamp()
When providing minimum, maximum, and preferred values as arguments, clamp() will honor the preferred value so long as it does not exceed the minimum and maximum boundaries.
clamp() will allow us to author things like components whose size will scale along with the size of the viewport, but won’t shrink or grow past a specific size. This will be especially useful for creating CSS locks, where you can ensure a responsive type size will not get so small that it can’t be read.
:host() and :host-context()
To be honest, I’m a little hazy on the specifics of the jargon and mechanics that power the Shadow DOM. Here’s how the MDN describes host():
The :host()CSS pseudo-class function selects the shadow host of the shadow DOM containing the CSS it is used inside (so you can select a custom element from inside its shadow DOM) — but only if the selector given as the function’s parameter matches the shadow host.
The :host-context() CSS pseudo-class function selects the shadow host of the shadow DOM containing the CSS it is used inside (so you can select a custom element from inside its shadow DOM) — but only if the selector given as the function’s parameter matches the shadow host’s ancestor(s) in the place it sits inside the DOM hierarchy.
:is() and :where()
:is() has had a bit of an identity crisis. Previously referred to as both matches() and vendor prefixed as :-webkit-any/:-moz-any, it now enjoys a standardized, agreed-upon name. It is a pseudo class selector that accepts a range of selectors as its argument.
This allows an author to group and target a wide range of selectors in an efficient way. :where() is much like :is(), only it has a specificity of zero, while the specificity of :is() is set to the highest specificity in the provided selector list.
:is() and :where() will allow us a good deal of flexibility about how we select things to style, especially for situations where you may not have as much control over the web site or web app’s stylesheet (e.g. third-party integrations, ads, etc.).
These functions allow you to select either the maximum or minimum value from a range of values you provide. Much like clamp(), these functions allow us to make things responsive up until a certain point.
:nth-col() and :nth-last-col()
These pseudo-classes will allow you to select one or a specified series columns in a CSS grid to apply styling to them. A good mental model for how these functions will work is how CSS pseudo class selectors operate. Unlike pseudo class selectors, :nth-col() and :nth-last-col() should be able to target implicit grid columns.
symbols()
This function allows you to specify a list of different kinds of characters to use for list bullets. Much like annotation(), you’ll want to make sure the typeface you use contains a glyph you want to use as a symbol before trying to get symbols() to work.
Deprecated Functions
Sometimes things just don’t work out the way you think they will. While deprecated CSS functions may still render in the browser for legacy support reasons, it isn’t recommended you use them going forward.
target-counter(), target-counters(), and target-text()
These functions were intended to help work with fragment URLs for paged (printed) media. You can read more about them on the W3C’s CSS Generated Content for Paged Media Module documentation
Typography
The web is typography, so it makes sense to give your type the care and attention it deserves. While CSS provides some functions specifically designed to unlock the potential of your website or webapp’s chosen typefaces, it is advised to not use the following functions to access these advanced features.
Many typefaces made by professional foundries include alternate treatments for certain letters, or combinations of letters. One example use case is providing different variations of commonly-used letters for typefaces designed to look like handwriting, to help make it appear more natural-looking.
Utilizing these functions activates these special alternate characters, provided they are present in the font’s glyph set.
Unfortunately, it is not a standardized offering. Different typefaces will have different ranges of support, based on what the typographer chose to include. It would be wise to check to see if the font you’re using supports these special features before writing any code.
format()
When you are importing a font via the url() function, the format() function is an optional hint that lets you manually specify the font’s file format. If this hint is provided, the browser won’t download the font if it does not recognize the specified file format.
You know when you’re reading a menu at a restaurant and there’s a series of periods that help you figure out what price is attached to what menu item? Those are leaders.
local() allows you to specify a font installed locally, meaning it is present on the device. Local fonts either ship with the device, or can be manually installed.
Betting on someone installing a font so things look the way you want them to is very risky! Because of this, it is recommended you don’t specify a local font that needs to be manually installed. Your site won’t look the way it is intended to, even moreso if you don’t specify a fallback font.
@font-face { font-family: 'FeltTipPen'; src: local('Felt Tip Pen Web'), /* Full font name */ local('FeltTipPen-Regular'); /* Postscript name */ }
ornaments()
Special dingbat characters can be enabled using this function. Be careful, as not all dingbat characters are properly coded in a way that will work well if a user does something like change the font, or use a specialized browsing mode.
swash()
Swashes are alternate visual treatments for letters that give them an extra-fancy flourish. They’re commonly found in italic and cursive-style typefaces.
CSS is maligned as frequently as it is misunderstood. The guiding thought to understanding why all these functions are made available to us is knowing that CSS isn’t prescriptive — not every website has to look like a Microsoft Word document.
The technologies that power the web are designed in such a way that someone with enough interest can build whatever they want. It’s a powerful, revolutionary concept, a large part of why the web became so ubiquitous.
Here’s an example of a nice little use case for cloud functions. Glitch has this great package of friendly words. Say you wanted to randomly generate “happy-elephant” or “walking-tree”, and you need to do that on your website in JavaScript. Well, this package is pretty big (~200 KB), necessarily so, because it has big dictionaries of words in it. You wouldn’t want to ship that to your client-side JavaScript when you don’t have to.
Cloud functions are cool, and we can use them to give ourselves a little API for this package instead. That way the size doesn’t matter that much, it’s up on a server. Netlify makes this about as easy as it can be.
Here’s a repo that makes it all possible. It’s barely any code!
A functions folder with a Node file in it.
At the root of our project: /functions/random.js
This file will require the friendly-words package and export a single function. Essentially it grabs two random words, plops them together, and returns it.
Now I don’t have to ship that package in my client-side JavaScript — I can just hit this URL to get what I want.
CORS
If I was hitting this URL from my own website also at friendly-words.netlify.com I wouldn’t have to worry about CORS, but if I need to use it from any other website, I do. Notice the Access-Control-Allow-Origin stuff in the Node JavaScript above. That takes care of that.
Demo
To use our little API, we can fetch from it. That’s it!
While I was doing this I came across Paul Kinlan’s article that does pretty much exactly the same thing, but his has some extra functionality as part of the API you might wanna check out.
Serverless functions are fairly straightforward. Put a bit of back-end language code, like Node, in the cloud and communicate with it via URL. But what if that URL didn’t run a back-end language, it ran an actual browser? Richard Young:
We can now do full stack development using just Web APIs. Need to read/write network resources? Use the fetch API. Need to cache some data? Use localStorage. Need to blur an image? Use a CSS filter on an img tag. Need to manage sessions? Use cookies. Need multi-threading? Use Web Workers. Need native compiled speed (or a language other than JavaScript)? Use WebAssembly.
Clever. Browsers are getting so capable it makes sense to leverage the things they are good at.
I’ve said before that HSL is the best color format we have. Most of us aren’t like David DeSandro, who can read hex codes. HSL(a) is Hue, Saturation, Lightness, and alpha, if we need it.
hsl(120, 100%, 40%)
Hue isn’t intuitive, but it’s not that weird. You take a trip around the color wheel from 0 to 360. Saturation is more obvious where 0% has all the color sucked out, like grayscale, and 100% is fully rich color at that hue. Lightness is “normal” at 50% and adds white or black as you go toward 100% and 0%, respectively. I’m sure that’s not the correct scientific or technical way of explaining it, but that’s the brain logic.
There are still issues with using HSL, which Brian Kardell explains in depth. I’m far from a color expert, but I think I see what Brian (and Adam) are saying in that article. Say you have three different colors and they all have the exact same lightness in HSL. That doesn’t mean they are all actually the same lightness. That’s kinda weird, particularly when you’re using this color format as part of a system of colors.
The good news is that there are color features already specced as a CSS Level 4 module that help with this: Lab and LCH. Check out the example from Adam where the colors in Lab have values that reflect their actual lightness much more accurately to how we perceive it.
HSL vs LAB:: lightness 💡
Same colors from our tricky color poll, but this time I've shown LAB's version of the same color over top. Notice how much closer LAB's lightness value is to the results of our poll!
There are color spaces like Lab and LCH which deal with the full spectrum and have qualities like perceptual uniformness. Thus, if we want great color functions for use in real design systems everyone seems to agree that having support to do said math in the Lab/LCH color spaces is the ideal enabling feature.
Note that lab()/lch()/gray() can all be eagerly converted into our existing color infrastructure; they don’t introduce any fundamentally new concepts, they’re just a better way to specify colors, more closely associated with how our eyes actually function rather than being closely tied to how rgb pixels function.
The conversion functions to turn it into rgb are a little bit of code, but it’s just some exponentials and a bit of matrix multiplication, and it’s well-documented in the spec.
This should be a GoodFirstBug sort of thing, I think.
Indie and enterprise web developers alike are pushing toward a serverless architecture for modern applications. Serverless architectures typically scale well, avoid the need for server provisioning and most importantly are easy and cheap to set up! And that’s why I believe the next evolution for cloud is serverless because it enables developers to focus on writing applications.
With that in mind, let’s build a REST API (because will we ever stop making these?) using 100% serverless technology.
We’re going to do that with Firebase Cloud Functions and FaunaDB, a globally distributed serverless database with native GraphQL.
Those familiar with Firebase know that Google’s serverless app-building tools also provide multiple data storage options: Firebase Realtime Database and Cloud Firestore. Both are valid alternatives to FaunaDB and are effectively serverless.
But why choose FaunaDB when Firestore offers a similar promise and is available with Google’s toolkit? Since our application is quite simple, it does not matter that much. The main difference is that once my application grows and I add multiple collections, then FaunaDB still offers consistency over multiple collections whereas Firestore does not. In this case, I made my choice based on a few other nifty benefits of FaunaDB, which you will discover as you read along — and FaunaDB’s generous free tier doesn’t hurt, either. 😉
In this post, we’ll cover:
Installing Firebase CLI tools
Creating a Firebase project with Hosting and Cloud Function capabilities
Routing URLs to Cloud Functions
Building three REST API calls with Express
Establishing a FaunaDB Collection to track your (my) favorite video games
Creating FaunaDB Documents, accessing them with FaunaDB’s JavaScript client API, and performing basic and intermediate-level queries
And more, of course!
Set Up A Local Firebase Functions Project
For this step, you’ll need Node v8 or higher. Install firebase-tools globally on your machine:
$ npm i -g firebase-tools
Then log into Firebase with this command:
$ firebase login
Make a new directory for your project, e.g. mkdir serverless-rest-api and navigate inside.
Create a Firebase project in your new directory by executing firebase login.
Select Functions and Hosting when prompted.
Choose “functions” and “hosting” when the bubbles appear, create a brand new firebase project, select JavaScript as your language, and choose yes (y) for the remaining options.
Create a new project, then choose JavaScript as your Cloud Function language.
Once complete, enter the functions directory, this is where your code lives and where you’ll add a few NPM packages.
Your API requires Express, CORS, and FaunaDB. Install it all with the following:
$ npm i cors express faunadb
Set Up FaunaDB with NodeJS and Firebase Cloud Functions
Before you can use FaunaDB, you need to sign up for an account.
When you’re signed in, go to your FaunaDB console and create your first database, name it “Games.”
You’ll notice that you can create databases inside other databases . So you could make a database for development, one for production or even make one small database per unit test suite. For now we only need ‘Games’ though, so let’s continue.
Create a new database and name it “Games.”
Then tab over to Collections and create your first Collection named ‘games’. Collections will contain your documents (games in this case) and are the equivalent of a table in other databases— don’t worry about payment details, Fauna has a generous free-tier, the reads and writes you perform in this tutorial will definitely not go over that free-tier. At all times you can monitor your usage in the FaunaDB console.
For the purpose of this API, make sure to name your collection ‘games’ because we’re going to be tracking your (my) favorite video games with this nerdy little API.
Create a Collection in your Games Database and name it “Games.”
Tab over to Security, and create a new Key and name it “Personal Key.” There are 3 different types of keys, Admin/Server/Client. Admin key is meant to manage multiple databases, A Server key is typically what you use in a backend which allows you to manage one database. Finally a client key is meant for untrusted clients such as your browser. Since we’ll be using this key to access one FaunaDB database in a serverless backend environment, choose ‘Server key’.
Under the Security tab, create a new Key. Name it Personal Key.
Save the key somewhere, you’ll need it shortly.
Build an Express REST API with Firebase Functions
Firebase Functions can respond directly to external HTTPS requests, and the functions pass standard Node Request and Response objects to your code — sweet. This makes Google’s Cloud Function requests accessible to middleware such as Express.
Open index.js inside your functions directory, clear out the pre-filled code, and add the following to enable Firebase Functions:
This creates a cloud function named, “api” and passes all requests directly to your api express server.
Routing an API URL to a Firebase HTTPS Cloud Function
If you deployed right now, your function’s public URL would be something like this: https://project-name.firebaseapp.com/api. That’s a clunky name for an access point if I do say so myself (and I did because I wrote this… who came up with this useless phrase?)
To remedy this predicament, you will use Firebase’s Hosting options to re-route URL globs to your new function.
Open firebase.json and add the following section immediately below the “ignore” array:
This setting assigns all /api/v1/... requests to your brand new function, making it reachable from a domain that humans won’t mind typing into their text editors.
With that, you’re ready to test your API. Your API that does… nothing!
Respond to API Requests with Express and Firebase Functions
Before you run your function locally, let’s give your API something to do.
Add this simple route to your index.js file right above your export statement:
Save your index.js fil, open up your command line, and change into the functions directory.
If you installed Firebase globally, you can run your project by entering the following: firebase serve.
This command runs both the hosting and function environments from your machine.
If Firebase is installed locally in your project directory instead, open package.json and remove the --only functions parameter from your serve command, then run npm run serve from your command line.
Visit localhost:5000/api/v1/ in your browser. If everything was set up just right, you will be greeted by a gif from one of my favorite movies.
And if it’s not one of your favorite movies too, I won’t take it personally but I will say there are other tutorials you could be reading, Bethany.
Now you can leave the hosting and functions emulator running. They will automatically update as you edit your index.js file. Neat, huh?
FaunaDB Indexing
To query data in your games collection, FaunaDB requires an Index.
Indexes generally optimize query performance across all kinds of databases, but in FaunaDB, they are mandatory and you must create them ahead of time.
As a developer just starting out with FaunaDB, this requirement felt like a digital roadblock.
“Why can’t I just query data?” I grimaced as the right side of my mouth tried to meet my eyebrow.
I had to read the documentation and become familiar with how Indexes and the Fauna Query Language (FQL) actually work; whereas Cloud Firestore creates Indexes automatically and gives me stupid-simple ways to access my data. What gives?
Typical databases just let you do what you want and if you do not stop and think: : “is this performant?” or “how much reads will this cost me?” you might have a problem in the long run. Fauna prevents this by requiring an index whenever you query. As I created complex queries with FQL, I began to appreciate the level of understanding I had when I executed them. Whereas Firestore just gives you free candy and hopes you never ask where it came from as it abstracts away all concerns (such as performance, and more importantly: costs).
Basically, FaunaDB has the flexibility of a NoSQL database coupled with the performance attenuation one expects from a relational SQL database.
We’ll see more examples of how and why in a moment.
As with other NoSQL databases, the documents are JSON-style text blocks with the exception of a few Fauna-specific objects (such as Date used in the “release_date” field).
Now switch to the Shell area and clear your query. Paste the following:
And click the “Run Query” button. You should see a list of three items: references to the documents you created a moment ago.
In the Shell, clear out the query field, paste the query provided, and click “Run Query.”
It’s a little long in the tooth, but here’s what the query is doing.
Index("all_games") creates a reference to the all_games index which Fauna generated automatically for you when you established your collection.These default indexes are organized by reference and return references as values. So in this case we use the Match function on the index to return a Set of references. Since we do not filter anywhere, we will receive every document in the ‘games’ collection.
The set that was returned from Match is then passed to Paginate. This function as you would expect adds pagination functionality (forward, backward, skip ahead). Lastly, you pass the result of Paginate to Map, which much like its software counterpart lets you perform an operation on each element in a Set and return an array, in this case it is simply returning ref (the reference id).
As we mentioned before, the default index only returns references. The Lambda operation that we fed to Map, pulls this ref field from each entry in the paginated set. The result is an array of references.
Now that you have a list of references, you can retrieve the data behind the reference by using another function: Get.
Wrap Var("ref") with a Get call and re-run your query, which should look like this:
You perform a Create, pass in your collection, and include data fields that come straight from the body of the request.
client.query returns a Promise, the success-state of which provides a reference to the newly-created document.
And to make sure it’s working, you return the reference to the caller. Let’s see it in action.
Test Firebase Functions Locally with Postman and cURL
Use Postman or cURL to make the following request against localhost:5000/api/v1/ to add Halo: Combat Evolved to your list of games (or whichever Halo is your favorite but absolutely not 4, 5, Reach, Wars, Wars 2, Spartan…)
If everything went right, you should see a reference coming back with your request and a new document show up in your FaunaDB console.
Now that you have some data in your games collection, let’s learn how to retrieve it.
Retrieve FaunaDB Records Using a REST API Request
Earlier, I mentioned that every FaunaDB query requires an Index and that Fauna prevents you from doing inefficient queries. Since our next query will return games filtered by a game console, we can’t simply use a traditional `where` clause since that might be inefficient without an index. In Fauna, we first need to define an index that allows us to filter.
To filter, we need to specify which terms we want to filter on. And by terms, I mean the fields of document you expect to search on.
Navigate to Indexes in your FaunaDB Console and create a new one.
Name it games_by_console, set data.consoles as the only term since we will filter on the consoles. Then set data.title and ref as values. Values are indexed by range, but they are also just the values that will be returned by the query. Indexes are in that sense a bit like views, you can create an index that returns a different combination of fields and each index can have different security.
To minimize request overhead, we’ve limited the response data (e.g. values) to titles and the reference.
Your screen should resemble this one:
Under indexes, create a new index named games_by_console using the parameters above.
Click “Save” when you’re ready.
With your Index prepared, you can draft up your next API call.
I chose to represent consoles as a directory path where the console identifier is the sole parameter, e.g. /api/v1/console/playstation_3, not necessarily best practice, but not the worst either — come on now.
This query looks similar to the one you used in your SHELL to retrieve all games, but with a slight modification.This query looks similar to the one you used in your SHELL to retrieve all games, but with a slight modification. Note how your Match function now has a second parameter (req.params.name.toLowerCase()) which is the console identifier that was passed in through the URL.
The Index you made a moment ago, games_by_console, had one Term in it (the consoles array), this corresponds to the parameter we have provided to the match parameter. Basically, the Match function searches for the string you pass as its second argument in the index. The next interesting bit is the Lambda function. Your first encounter with Lamba featured a single string as Lambda’s first argument, “ref.”
However, the games_by_console Index returns two fields per result, the two values you specified earlier when you created the Index (data.title and ref). So basically we receive a paginated set containing tuples of titles and references, but we only need titles. In case your set contains multiple values, the parameter of your lambda will be an array. The array parameter above (`[‘title’, ‘ref’]`) says that the first value is bound to the text variable title and the second is bound to the variable ref. text parameter. These variables can then be retrieved again further in the query by using Var(‘title’). In this case, both “title” and “ref,” were returned by the index and your Map with Lambda function maps over this list of results and simply returns only the list of titles for each game.
In fauna, the composition of queries happens before they are executed. When you write var q = q.Match(q.Index('games_by_console'))), the variable just contains a query but no query was executed yet. Only when you pass the query to client.query(q) to be executed, it will execute. You can even pass javascript variables in other Fauna FQL functions to start composing queries. his is a big benefit of querying in Fauna vs the chained asynchronous queries required of Firestore. If you ever have tried to generate very complex queries in SQL dynamically, then you will also appreciate the composition and less declarative nature of FQL.
Neat, huh? But Match only returns documents whose fields are exact matches, which doesn’t help the user looking for a game whose title they can barely recall.
Although Fauna does not offer fuzzy searching via indexes (yet), we can provide similar functionality by making an index on all words in the string. Or if we want really flexible fuzzy searching we can use the filter syntax. Note that its is not necessarily a good idea from a performance or cost point of view… but hey, we’ll do it because we can and because it is a great example of how flexible FQL is!
Filtering FaunaDB Documents by Search String
The last API call we are going to construct will let users find titles by name. Head back into your FaunaDB Console, select INDEXES and click NEW INDEX. Name the new Index, games_by_title and leave the Terms empty, you won’t be needing them.
Rather than rely on Match to compare the title to the search string, you will iterate over every game in your collection to find titles that contain the search query.
Remember how we mentioned that indexes are a bit like views. In order to filter on title , we need to include `data.title` as a value returned by the Index. Since we are using Filter on the results of Match, we have to make sure that Match returns the title so we can work with it.
Add data.title and ref as Values, compare your screen to mine:
Create another index called games_by_title using the parameters above.
Click “Save” when you’re ready.
Back in index.js, add your fourth and final API call:
Big breath because I know there are many brackets (Lisp programmers will love this) , but once you understand the components, the full query is quite easy to understand since it’s basically just like coding.
Beginning with the first new function you spot, Filter. Filter is again very similar to the filter you encounter in programming languages. It reduces an Array or Set to a subset based on the result of a Lambda function.
In this Filter, you exclude any game titles that do not contain the user’s search query.
You do that by comparing the result of FindStr (a string finding function similar to JavaScript’s indexOf) to -1, a non-negative value here means FindStr discovered the user’s query in a lowercase-version of the game’s title.
And the result of this Filter is passed to Map, where each document is retrieved and placed in the final result output.
Now you may have thought the obvious: performing a string comparison across four entries is cheap, 2 million…? Not so much.
This is an inefficient way to perform a text search, but it will get the job done for the purpose of this example. (Maybe we should have used ElasticSearch or Solr for this?) Well in that case, FaunaDB is quite perfect as central system to keep your data safe and feed this data into a search engine thanks to the temporal aspect which allows you to ask Fauna: “Hey, give me the last changes since timestamp X?”. So you could setup ElasticSearch next to it and use FaunaDB (soon they have push messages) to update it whenever there are changes. Whoever did this once knows how hard it is to keep such an external search up to date and correct, FaunaDB makes it quite easy.
Don’t You Dare Forget This One Firebase Optimization
A lot of Firebase Cloud Functions code snippets make one terribly wrong assumption: that each function invocation is independent of another.
In reality, Firebase Function instances can remain “hot” for a short period of time, prepared to execute subsequent requests.
This means you should lazy-load your variables and cache the results to help reduce computation time (and money!) during peak activity, here’s how:
let functions, admin, faunadb, q, client, express, cors, api if (typeof api === 'undefined') { ... // dump the existing code here } exports.api = functions.https.onRequest(api)
Deploy Your REST API with Firebase Functions
Finally, deploy both your functions and hosting configuration to Firebase by running firebase deploy from your shell.
Without a custom domain name, refer to your Firebase subdomain when making API requests, e.g. https://{project-name}.firebaseapp.com/api/v1/.
What Next?
FaunaDB has made me a conscientious developer.
When using other schemaless databases, I start off with great intentions by treating documents as if I instantiated them with a DDL (strict types, version numbers, the whole shebang).
While that keeps me organized for a short while, soon after standards fall in favor of speed and my documents splinter: leaving outdated formatting and zombie data behind.
By forcing me to think about how I query my data, which Indexes I need, and how to best manipulate that data before it returns to my server, I remain conscious of my documents.
To aid me in remaining forever organized, my catalog (in FaunaDB Console) of Indexes helps me keep track of everything my documents offer.
And by incorporating this wide range of arithmetic and linguistic functions right into the query language, FaunaDB encourages me to maximize efficiency and keep a close eye on my data-storage policies. Considering the affordable pricing model, I’d sooner run 10k+ data manipulations on FaunaDB’s servers than on a single Cloud Function.
Building and maintaining your own website is a great idea. Not only do you own your platform, but you get to experiment with web technologies along the way. Recently, I dug into a concept called serverless functions, starting with my own website. I’d like to share the results and what I learned along the way, so you can get your hands dirty, too!
But first, a 1-minute intro to serverless functions
A serverless function (sometimes called a lambda function or cloud function) is a piece of code that you can write, host, and run independently of your website, app, or any other code. Despite the name, serverless functions do, indeed, run on a server; but it’s a server you don’t have to build or maintain. Serverless functions are exciting because they take a lot of the legwork out of making powerful, scalable, apps.
I started my journey with a challenge: I wanted to have an email list sign-up form on my site. The rules are as follows:
It should work without JavaScript. I’d like to see how much I can get by with just CSS and HTML. Progressive enhancements are OK.
It shouldn’t require external dependencies. This is a learning project, so I want to write 100% of the code if possible.
It should use serverless functions. Instead of sending data to my email list service client-side, let’s do it server(less)-side!
Meet the team: 11ty, Netlify, and Buttondown
My website is built using a static site framework called 11ty. 11ty allows me to write templates and components in HTML, so that’s how we’ll build our email form. (Chris recently wrote a great article about his experience with 11ty if you’re interested in learning more.)
The website is then deployed using a service called Netlify) and it is the key player on our team here: the point guard, the quarterback, the captain. That’s because Netlify has three features that work together to produce serverless excellence:
Netlify can deploy automatically from a GitHub repo. This means I can write my code, create a pull request, and instantly see if my code works. While there are tools to test serverless functions locally, Netlify makes it super easy to test live.
Netlify Forms handles any form submissions my site gets. This is one part of the serverless equation: instead of writing code to collect submissions, I’ll configure the HTML with a few simple attributes and let Netlify handle the rest.
Netlify Functions allows me to take action with the data from my forms. I’ll write some code to send emails off to my email list provider, and tell Netlify when to run that code.
Finally, I’ll manage my email list with a service called Buttondown. It’s a no-frills email newsletter provider, with an easy-to-use API.
Bonus: for personal sites like mine, 11ty, Netlify, and Buttondown are free. You can’t beat that.
The form
The HTML for my email subscription form is very minimal, with a few extras for Netlify Forms to work.
<form class="email-form" name="newsletter" method="POST" data-netlify="true" netlify-honeypot="bot-field"> <div hidden aria-hidden="true"> <label> Don’t fill this out if you're human: <input name="bot-field" /> </label> </div> <label for="email">Your email address</label> <div> <input type="email" name="email" placeholder="Email" id="email" required /> <button type="submit">Subscribe</button> </div> </form>
First, I set the data-netlify attribute to true to tell Netlify to handle this form.
The first input in the form is named bot-field. This tricks robots into revealing themselves: I tell Netlify to watch for any suspicious submissions by setting the netlify-honeypot attribute to bot-field. I then hide the field from humans using the html hidden and aria-hidden values — users with and without assistive technology won’t be able to fill out the fake input.
If the form gets submitted with anything in the bot-field input, Netlify knows it’s coming from a robot, and ignores the input. In addition to this layer of protection, Netlify automatically filters suspicious submissions with Askimethttps://www.netlify.com/blog/2019/02/12/improved-netlify-forms-spam-filtering-using-akismet/). I don’t have to worry about spam!
The next input in the form is named email. This is where the email address goes! I’ve specified the input-type as email, and indicated that is required; this means that the browser will do all my validation for me, and won’t let users submit anything other than a valid email address.
Progressive enhancement with JavaScript
One neat feature of Netlify Forms is the ability to automatically redirect users to a “thank you” page when they submit a form. But ideally, I’d like to keep my users on the page. I wrote a short function to submit the form without a redirect.
const processForm = form => { const data = new FormData(form) data.append('form-name', 'newsletter'); fetch('/', { method: 'POST', body: data, }) .then(() => { form.innerHTML = `<div class="form--success">Almost there! Check your inbox for a confirmation e-mail.</div>`; }) .catch(error => { form.innerHTML = `<div class="form--error">Error: $ {error}</div>`; }) }
When I provide the content of my email form to this function via the form value, it submits the form using JavaScript’s built-in Fetch API. If the function was successful, it shows a pleasant message to the user. If the function hits a snag, it’ll tell my users that something went wrong.
This function is called whenever a user clicks the “submit” button on the form:
const emailForm = document.querySelector('.email-form') if (emailForm) { emailForm.addEventListener('submit', e => { e.preventDefault(); processForm(emailForm); }) }
This listener progressively enhances the default behavior of the form. This means that if the user has JavaScript disabled, the form still works!
The serverless function
Now that we have a working email submission form, it’s time to do some automation with a serverless function.
The way Netlify functions work is as follows:
Write the function in a JavaScript file in your project.
Tell Netlify where to look for your function via the netlify.toml file in your project.
Add any environment variables you’ll need via Netlify’s admin interface. An environment variable is something like an API key that you need to keep secret.
That’s it! The next time you deploy your site, the function will be ready to go.
The function for my site is going to be in the functions folder, so I have the following in my netlify.toml file:
[build] base = "." functions = "./functions"
Then, I’ll add a file in the functions folder called submission-created.js. It’s important to name the file submission-created so that Netlify knows to run it every time a new form submission occurs. A full list of events you can script against can be found in Netlify’s documentation. If you’ve correctly named and configured your function, you should see it on Netlify’s Functions dashboard:
Netlify’s Functions dashboard shows I’ve correctly configured my submission-created function
The content in submission-created.js looks like this:
Line 1 includes a library called dotenv. This will help me use environment variables. Environment variables are useful to hold information that I don’t want to make public, like an API key. If I’m running my project locally, I set my environment variables with a .env file in the repo, and make sure it’s listed my .gitignore file. In order to deploy on Netlify, I also set up environment variables in Netlify’s web interface.
On line 2, I add a small library called node-fetch. This allows me to use Javascript’s Fetch API in node, which is how we’ll send data to Buttondown. Netlify automatically includes this dependency, as long as it’s listed in my project’s package.json file.
On line 3, I import my API key from the environment variables object, process.env.
Line 4 is where the function is defined. The exports.handler value is where Netlify expects to find our function, so we define it there. The only input we’ll need is the event value, which will contain all of the data from the form submission.
After retrieving the email address from the event value using JSON.parse, I’m ready to send it off to Buttondown. Here’s where I use the node-fetch library I imported earlier: I send a POST request to https://api.buttondown.email/v1/subscribers, including my API key in the header. Buttondown’s API doesn’t have many features, so it doesn’t take long to read through the documentation if you’d like to learn more.
The body of my POST request consists of the email address we retrieved.
Then (using the neat .then() syntax), I collect the response from Buttondown’s server. I do this so I can diagnose any issues that are happening with the process — Netlify makes it easy to check your function’s logs, so use console.log often!
Deploying the function
Now that I’ve written my function, configured my netlify.toml file, and added my environment variables, everything is ready to go. Deploying is painless: just set up Netlify’s GitHub integration, and your function will be deployed when your project is pushed.
Netlify projects can also be tested locally using Netlify Dev. Depending on the complexity of your code, it can be faster to develop locally: just run npm i netlify -g, then netlify dev. Netlify Dev will use the netlify.toml file to configure and run the project locally, including any serverless functions. Neat, right? One caveat: Netlify Dev currently can’t trigger serverless functions on form submissions, so you’ll have to test that using preview builds.
An idea for the future
Buttondown’s API has a few possible responses when I submit a new email. For instance, if a user submits an email that’s already subscribed to the list, I’d love to be able to tell them as soon as they submit the form.
Conclusion
All in all, I only had to write about 50 lines of code to have a functional email newsletter sign-up form available on my website. I wrote it all in HTML, CSS, and JavaScript, without having to fret with the server side of the equation. The form handles spam, and my readers get a nice experience whether they have JavaScript enabled or not.
This post covers how I built a typography grid for a design system using Vue render functions. Here’s the demo and the code. I used render functions because they allow you to create HTML with a greater level of control than regular Vue templates, yet surprisingly I couldn’t find very much when I web searched around for real-life, non-tutorial applications of them. I’m hoping this post will fill that void and provide a helpful and practical use case on using Vue render functions.
I’ve always found render functions to be a little out-of-character for Vue. While the rest of the framework emphasizes simplicity and separation of concerns, render functions are a strange and often difficult-to-read mix of HTML and JavaScript.
I suspect that this syntax turns some people off, since ease-of-use is a key reason to reach for Vue in the first place. This is a shame because render functions and functional components are capable of some pretty cool, powerful stuff. In the spirit of demonstrating their value, here’s how they solved an actual business problem for me.
Quick disclaimer: It will be super helpful to have the demo open in another tab to reference throughout this post.
Defining criteria for a design system
My team wanted to include a page in our VuePress-powered design system showcasing different typography options. This is part of a mockup that I got from our designer.
And here’s a sample of some of the corresponding CSS:
Headings are targeted with tag names. Other items use class names, and there are separate classes for weight and size.
Before writing any code, I created some ground rules:
Since this is really a data visualization, the data should be stored in a separate file.
Headings should use semantic heading tags (e.g. <h1>, <h2>, etc.) instead of having to rely on a class.
Body content should use paragraph (<p>) tags with the class name (e.g. <p class="body-text--lg">).
Content types that have variations should be grouped together by wrapping them in the root paragraph tag, or corresponding root element, without a styling class. Children should be wrapped with <span> and the class name.
Class names should only need to be written once for each cell that’s demonstrating styling.
Why render functions make sense
I considered a few options before starting:
Hardcoding
I like hardcoding when appropriate, but writing my HTML by hand would have meant typing out different combinations of the markup, which seemed unpleasant and repetitive. It also meant that data couldn’t be kept in a separate file, so I ruled out this approach.
Here’s what I mean:
<div class="row"> <h1>Heading 1</h1> <p class="body-text body-text--md body-text--semibold">h1</p> <p class="body-text body-text--md body-text--semibold">Balboa Light, 30px</p> <p class="group body-text body-text--md body-text--semibold"> <span>Product title (once on a page)</span> <span>Illustration headline</span> </p> </div>
Using a traditional Vue template
This would normally be the go-to option. However, consider the following:
– An <h1>> tag rendered as-is. – A <p> tag that groups some <span> children with text, each with a class (but no special class on the <p> tag). – A <p> tag with a class and no children.
The result would have meant many instances of v-if and v-if-else, which I knew would get confusing fast. I also disliked all of that conditional logic inside the markup.
Because of these reasons, I chose render functions. Render functions use JavaScript to conditionally create child nodes based on all of the criteria that’s been defined, which seemed perfect for this situation.
Data model
As I mentioned earlier, I’d like to keep typography data in a separate JSON file so I can easily make changes later without touching markup. Here’s the raw data.
Each object in the file represents a different row.
{ "text": "Heading 1", "element": "h1", // Root wrapping element. "properties": "Balboa Light, 30px", // Third column text. "usage": ["Product title (once on a page)", "Illustration headline"] // Fourth column text. Each item is a child node. }
The object above renders the following HTML:
<div class="row"> <h1>Heading 1</h1> <p class="body-text body-text--md body-text--semibold">h1</p> <p class="body-text body-text--md body-text--semibold">Balboa Light, 30px</p> <p class="group body-text body-text--md body-text--semibold"> <span>Product title (once on a page)</span> <span>Illustration headline</span> </p> </div>
Let’s look at a more involved example. Arrays represent groups of children. A classes object can store classes. The base property contains classes that are common to every node in the cell grouping. Each class in variants is applied to a different item in the grouping.
{ "text": "Body Text - Large", "element": "p", "classes": { "base": "body-text body-text--lg", // Applied to every child node "variants": ["body-text--bold", "body-text--regular"] // Looped through, one class applied to each example. Each item in the array is its own node. }, "properties": "Proxima Nova Bold and Regular, 20px", "usage": ["Large button title", "Form label", "Large modal text"] }
We have a parent component, TypographyTable.vue, which contains the markup for the wrapper table element, and a child component, TypographyRow.vue, which creates a row and contains our render function.
I loop through the row component, passing the row data as props.
<template> <section> <!-- Headers hardcoded for simplicity --> <div class="row"> <p class="body-text body-text--lg-bold heading">Hierarchy</p> <p class="body-text body-text--lg-bold heading">Element/Class</p> <p class="body-text body-text--lg-bold heading">Properties</p> <p class="body-text body-text--lg-bold heading">Usage</p> </div> <!-- Loop and pass our data as props to each row --> <typography-row v-for="(rowData, index) in $ options.typographyData" :key="index" :row-data="rowData" /> </section> </template> <script> import TypographyData from "@/data/typography.json"; import TypographyRow from "./TypographyRow"; export default { // Our data is static so we don't need to make it reactive typographyData: TypographyData, name: "TypographyTable", components: { TypographyRow } }; </script>
One neat thing to point out: the typography data can be a property on the Vue instance and be accessed using $ options.typographyData since it doesn’t change and doesn’t need to be reactive. (Hat tip to Anton Kosykh.)
Making a functional component
The TypographyRow component that passes data is a functional component. Functional components are stateless and instanceless, which means that they have no this and don’t have access to any Vue lifecycle methods.
The empty starting component looks like this:
// No <template> <script> export default { name: "TypographyRow", functional: true, // This property makes the component functional props: { rowData: { // A prop with row data type: Object } }, render(createElement, { props }) { // Markup gets rendered here } } </script>
The render method takes a context argument, which has a props property that’s de-structured and used as the second argument.
The first argument is createElement, which is a function that tells Vue what nodes to create. For brevity and convention, I’ll be abbreviating createElement as h. You can read about why I do that in Sarah’s post.
h takes three arguments:
An HTML tag (e.g. div)
A data object with template attributes (e.g. { class: 'something'})
Text strings (if we’re just adding text) or child nodes built using h
render(h, { props }) { return h("div", { class: "example-class }, "Here's my example text") }
OK, so to recap where we are at this point, we’ve covered creating:
a file with the data that’s going to be used in my visualization;
a regular Vue component where I’m importing the full data file; and
the beginning of a functional component that will display each row.
To create each row, the data from the JSON file needs to be passed into arguments for h. This could be done all at once, but that involves a lot of conditional logic and is confusing.
Instead, I decided to do it in two parts:
Transform the data into a predictable format.
Render the transformed data.
Transforming the common data
I wanted my data in a format that would match the arguments for h, but before doing this, I wrote out how I wanted things structured:
// One cell { tag: "", // HTML tag of current level cellClass: "", // Class of current level, null if no class exists for that level text: "", // Text to be displayed children: [] // Children each follow this data model, empty array if no child nodes }
Each object represents one cell, with four cells making up each row (an array).
function createRow(data) { // Pass in the full row data and construct each cell let { text, element, classes = null, properties, usage } = data; let row = []; row[0] = createCellData(data) // Transform our data using some shared function row[1] = createCellData(data) row[2] = createCellData(data) row[3] = createCellData(data) return row; }
Let’s take another look at our mockup.
The first column has styling variations, but the rest seem to follow the same pattern, so let’s start with those.
This gives us a tree-like structure for each cell since some cells have groups of children. Let’s use two functions to create the cells.
createNode takes each of our desired properties as arguments.
createCell wraps around createNode so that we can check if the text that we’re passing in is an array. If it is, we build up an array of child nodes.
// Model for each cell function createCellData(tag, text) { let children; // Base classes that get applied to every root cell tag const nodeClass = "body-text body-text--md body-text--semibold"; // If the text that we're passing in as an array, create child elements that are wrapped in spans. if (Array.isArray(text)) { children = text.map(child => createNode("span", null, child, children)); } return createNode(tag, nodeClass, text, children); } // Model for each node function createNode(tag, nodeClass, text, children = []) { return { tag: tag, cellClass: nodeClass, text: children.length ? null : text, children: children }; }
Now, we can do something like:
function createRow(data) { let { text, element, classes = null, properties, usage } = data; let row = []; row[0] = "" row[1] = createCellData("p", ?????) // Need to pass in class names as text row[2] = createCellData("p", properties) // Third column row[3] = createCellData("p", usage) // Fourth column return row; }
We pass properties and usage to the third and fourth columns as text arguments. However, the second column is a little different; there, we’re displaying the class names, which are stored in the data file like:
Additionally, remember that headings don’t have classes, so we want to show the heading tag names for those rows (e.g. h1, h2, etc.).
Let’s create some helper functions to parse this data into a format that we can use for our text argument.
// Pass in the base tag and class names as arguments function displayClasses(element, classes) { // If there are no classes, return the base tag (appropriate for headings) return getClasses(classes) ? getClasses(classes) : element; } // Return the node class as a string (if there's one class), an array (if there are multiple classes), or null (if there are none.) // Ex. "body-text body-text--sm" or ["body-text body-text--sm body-text--bold", "body-text body-text--sm body-text--italic"] function getClasses(classes) { if (classes) { const { base, variants = null } = classes; if (variants) { // Concatenate each variant with the base classes return variants.map(variant => base.concat(`$ {variant}`)); } return base; } return classes; }
Now we can do this:
function createRow(data) { let { text, element, classes = null, properties, usage } = data; let row = []; row[0] = "" row[1] = createCellData("p", displayClasses(element, classes)) // Second column row[2] = createCellData("p", properties) // Third column row[3] = createCellData("p", usage) // Fourth column return row; }
Transforming the demo data
This leaves the first column that demonstrates the styles. This column is different because we’re applying new tags and classes to each cell instead of using the class combination used by the rest of the columns:
Rather than try to do this in createCellData or createNodeData, let’s make another function to sit on top of these base transformation functions and handle some of the new logic.
function createDemoCellData(data) { let children; const classes = getClasses(data.classes); // In cases where we're showing off multiple classes, we need to create children and apply each class to each child. if (Array.isArray(classes)) { children = classes.map(child => // We can use "data.text" since each node in a cell grouping has the same text createNode("span", child, data.text, children) ); } // Handle cases where we only have one class if (typeof classes === "string") { return createNode("p", classes, data.text, children); } // Handle cases where we have no classes (ie. headings) return createNode(data.element, null, data.text, children); }
Now we have the row data in a normalized format that we can pass to our render function:
function createRow(data) { let { text, element, classes = null, properties, usage } = data let row = [] row[0] = createDemoCellData(data) row[1] = createCellData("p", displayClasses(element, classes)) row[2] = createCellData("p", properties) row[3] = createCellData("p", usage) return row }
Rendering the data
Here’s how we actually render the data to display:
// Access our data in the "props" object const rowData = props.rowData; // Pass it into our entry transformation function const row = createRow(rowData); // Create a root "div" node and handle each cell return h("div", { class: "row" }, row.map(cell => renderCells(cell))); // Traverse cell values function renderCells(data) { // Handle cells with multiple child nodes if (data.children.length) { return renderCell( data.tag, // Use the base cell tag { // Attributes in here class: { group: true, // Add a class of "group" since there are multiple nodes [data.cellClass]: data.cellClass // If the cell class isn't null, apply it to the node } }, // The node content data.children.map(child => { return renderCell( child.tag, { class: child.cellClass }, child.text ); }) ); } // If there are no children, render the base cell return renderCell(data.tag, { class: data.cellClass }, data.text); } // A wrapper function around "h" to improve readability function renderCell(tag, classArgs, text) { return h(tag, classArgs, text); }
It’s worth pointing out that this approach represents an experimental way of addressing a relatively trivial problem. I’m sure many people will argue that this solution is needlessly complicated and over-engineered. I’d probably agree.
Despite the up-front cost, however, the data is now fully separated from the presentation. Now, if my design team adds or removes rows, I don’t have to dig into messy HTML — I just update a couple of properties in the JSON file.
Is it worth it? Like everything else in programming, I guess it depends. I will say that this comic strip was in the back of my mind as I worked on this:
Maybe that’s an answer. I’d love to hear all of your (constructive) thoughts and suggestions, or if you’ve tried other ways of going about a similar task.
Let’s say you’re rocking a JAMstack-style site (no server-side languages in use), but you want to do something rather dynamic like send an email. Not a problem! That’s the whole point of JAMstack. It’s not just static hosting. It’s that plus doing anything else you wanna do through JavaScript and APIs.
Here’s the setup: You need a service to help you send the email. Let’s just pick Sparkpost out of a hat. There are a number of them, and I’ll leave comparing their features and pricing to you, as we’re doing something extremely basic and low-volume here. To send an email with Sparkpost, you hit their API with your API key, provide information about the email you want to send, and Sparkpost sends it.
So, you’ll need to run a little server-side code to protect your API key during the API request. Where can you run that code? A Lambda is perfect for that (aka a serverless function or cloud function). There are lots of services to help you run these, but none are easier than Netlify, where you might be hosting your site anyway.
Get Sparkpost ready
I signed up for Sparkpost and made sure my account was all set up and verified. The dashboard there will give you an API key:
Toss that API Key into Netlify
Part of protecting our API key is making sure it’s only used in server-side code, but also that we keep it out of our Git repository. Netlify has environment variables that expose it to functions as needed, so we’ll plop it there:
Let’s spin up Netlify Dev, as that’ll make this easy to work with
Netlify Dev is a magical little tool that does stuff like run our static site generator for us. For the site I’m working on, I use Eleventy and Netlify Dev auto-detects and auto-runs it, which is super neat. But more importantly, for us, it gives us a local URL that runs our functions for testing.
Once it’s all installed, running it should look like this:
In the terminal screenshot above, it shows the website itself being spun up at localhost:8080, but it also says:
◈ Lambda server is listening on 59629
That’ll be very useful in a moment when we’re writing and testing our new function — which, by the way, we can scaffold out if we’d like. For example:
netlify functions:create --name hello-world
From there, it will ask some questions and then make a function. Pretty useful to get started quickly. We’ll cover writing that function in a moment, but first, let’s use this…
Sparkpost has their own Node lib
Sparkpost has an API, of course, for sending these emails. We could look at those docs and learn how to hit their URL endpoints with the correct data.
But things get even easier with their Node.js bindings. Let’s get this set up by creating all the folders and files we’ll need:
/project ... your entire website or whatever ... /functions/ /send-email/ package.json send-email.js
All we need the package.json</code file for is to yank in the Sparkpost library, so <code>npm install sparkpost --save-dev will do the trick there.
Then the send-email.js imports that lib and uses it:
You’ll want to look at their docs for error handling and whatnot. Again, we’ve just chosen Sparkpost out of a hat here. Any email sending service will have an API and helper code for popular languages.
Notice line 2! That’s where we need the API key, and we don’t need to hard-code it because Netlify Dev is so darn fancy that it will connect to Netlify and let us use the environment variable from there.
Test the function
When Netlify Dev is running, our Lamba functions have that special port they are running. We’ll be able to have a URL like this to run the function:
This function is set up to run when it’s hit, so we could simply visit that in a browser to run it.
Testing
Maybe you’ll POST to this URL. Maybe you’ll send the body of the email. Maybe you’ll send the recipient’s email address. It would be nice to have a testing environment for all of this.
Well, we can console.log() stuff and see it in the terminal, so that’s always handy. Plus we can write our functions to return whatever, and we could look at those responses in some kind of API testing tool, like Postman or Insomnia.
During the past few months, I’ve been actively teaching myself how to draw and animate SVG shapes. I’ve been using CSS transitions, as well as tools like D3.js, react-motion and GSAP, to create my animations.
One thing about animations in general and the documentation these and other animation tools recommend is using easing functions. I’ve been working with them in some capacity over the years, but to be honest, I would never know which function to choose for which kind of animation. Moreover, I did not know about the magic that goes into each of these functions, the notable differences between them, and how to use them effectively. But I was fine with that because I knew that easing functions somehow “smoothed” things out and mostly made my work look realistic.
Here, I present to you what I learned about easing functions in the form of a primer that I hope gives you a good understanding as you dig into animations.
How I got into easing functions
I tried to re-create a pattern called rotating snakes, an optical illusion that tricks the brains into thinking that circles rotate and “dance” when they are not.
I quickly found a gap in my knowledge when trying to build this out. It’s hard! But in the process, I discovered that easing functions play a big role in it.
I turned to JavaScript to draw a bunch of concentric circles in SVG using a library:
for (i = 1; i <= 10; i++) { drawCircle({radius: i * 10}); }
This was the result:
But that clearly does not look anything like the picture.
As I thought things through, I realized that I was looking for a certain property. I wanted the change in radius of the concentric circles to be small at the beginning and then become larger as the radius increases.
This means that the linear increase in radius using i++ won’t do the trick. We need a better formula to derive the radius. So, my next attempt looked something like this:
let i = 1; let radiusList = []; let radius = 0; while (i <= 10) { drawCircle({radius: i * 10}); if(i < 4) { i = i + 0.5 } else { i = i + 1 } }
…which got me this:
Hmm, still not what I wanted. In fact, this deviates even further from the pattern. Plus, this code is hardly customizable unwieldy to maintain.
So, I turned to math for one last attempt.
What we need is a function that changes the radius organically and exponentially. I had an “Aha!” moment and maybe you already see it, too. Easing functions will do this!
The radius of each circle should increase slowly at first, then quickly as the circles go outward. With easing, we can make move things along a curve that can slow and speed up at certain points.
A quick Google search landed me at this gist which is a well-documents list of easing functions and really saved my day. Each function takes one input value, runs formulae. and provides an output value. The input value has to be between 0 and 1. (We will dig into this reasoning later.)
A quadratic easing function looked promising because all it does is square the value it receives:
The difference between this pattern and my first two attempts was night and day. Yay for easing functions!
This little experience got me really interested in what else easing functions could do. I scoured the internet for cool information. I found old articles, mostly related to Flash and ActionScript which had demos showing different line graphs.
That’s all pretty outdated, so here’s my little primer on easing functions.
What are easing functions?
They’re a type of function that takes a numeric input between 0 and 1. That number runs through the specified function and returns another number between 0 and 1. A value between 0-1 multiplied by another value between 0-1 always results in a value between 0-1. This special property helps us make any computation we want while remaining within specific bounds.
The purpose of an easing function is to get non-linear values from linear value inputs.
This is the crux of what we need to know about easing functions. The explanations and demos here on out are all geared towards driving home this concept.
Easing functions are a manifestation of the interpolation concept in mathematics. Interpolation is the process of finding the set of points that lie on a curve. Easing functions are essentially drawing a curve from point 0 to point 1 by interpolating (computing) different sets of points along the way.
Robert Penner was the first to define easing functions and create formulae for different ones in his book.
The five types of easing functions
There are five types of easing functions. They can be mixed, inverted and even mashed together to form additional, more complex functions. Let’s dig into each one.
Linear easing functions
This is the most basic form of easing. If the interval between the points we interpolate between 0 and 1 are constant, then we then form a linear easing function.
Going back to the concentric circles example earlier, increasing the radius of the initial circle by a constant amount (10px in that example) makes a linear function.
It should come as no surprise that linear is the default easing function. They’re extremely simple because there is no curve to the animation and the object moves in a straight, consistent direction. That said, linear functions have their drawbacks. For example, linear animations tend to feel unnatural or even robotic because real-life objects rarely move with such perfect, straight motion.
Quadratic easing functions
A quadratic easing function is created by multiplying a value between 0 and 1 by itself (e.g. 0.5*0.5). As we learned earlier, we see that this results in a value that is also between 0 and 1 (e.g. 0.5*0.5 = 0.25).
To demonstrate, let’s make 10 values between 0 and 1 with a quadratic function.
const quad_easing = (t) => t*t; let easing_vals = []; for(let i = 0; i < 1; i +=0.1) { easing_vals.push(quad_easing(i)); }
Here’s a table of all the values we get:
Input Value (x-axis)
Quadratic Eased Value (y-axis)
0
0
0.1
0.01
0.2
0.04
0.3
0.09
0.4
0.16
0.5
0.25
0.6
0.36
0.7
0.49
0.8
0.64
0.9
0.81
1
1
If we were to plot this value on a graph with x-axis as the original value and y-axis as the eased value, we would get something like this:
Notice something? The curve is practically the same as the ease-in functions we commonly find, even in CSS!
Cubic, Quartic and Quintic easing functions
The final three types of easing functions behave the same, but work with a different value.
A cubic easing function is creating by multiplying a value between 0 and 1 by itself three times. In other words, it’s some value (e.g. t), cubed (e.g. t3).
Quartic functions do the same, but to the power of 4. So, if t is our value, we’re looking at t4
And, as you have already guessed, a quintic function runs to the power of 5.
The following demo will give you a way to play around with the five types of functions for a good visual of how they differ from one another.
“An ease-in-out is a delicious half-and-half combination, like a vanilla-chocolate swirl ice cream cone.” — Robert Penner
Ease in and ease out might be the most familiar easing animations. They often smooth out a typical linear line by slowing down at the start or end (or both!) of an animation.
Ease-in and ease-out animations can be created using any of the non-linear functions we’ve already looked at, though cubic functions are most commonly used. In fact, the CSS animation property comes with ease-in and ease-out values right out of the box, via the animation-timing-function sub-property.
ease-in: This function starts slow but ends faster.
ease-out: This function starts fast and ends slower.
ease-in-out: This function acts as a combination of the others, starting fast, slowing down in the middle, then ending faster.
These curves can be created in JavaScript as well. I personally like and use the bezier-easing library for it. Easing.js is another good one, as is D3’s library (with a nice example from Mike Bostock). And, if jQuery is more your thing, check out this plugin or even this one.
See, it’s pretty “ease”-y!
I hope this little primer helps illustrate easing functions and interpolation in general. There are so many ways these functions can make animations more natural and life-like. Have a look at Easing.css for a UI that allows you to create custom curves and comes with a slew of preset options.
I hope the next time you use an easing function, it won’t be a blackbox to you. You now have a baseline understanding to put easing functions to use and open up a ton of possibilities when working with animations.
More on easing
We’ve only scratched the surface of easing functions here, but there are other good resources right here on CSS-Tricks worth checking out to level up even further.