Tag: System

How I Made an Icon System Out of CSS Custom Properties

SVG is the best format for icons on a website, there is no doubt about that. It allows you to have sharp icons no matter the screen pixel density, you can change the styles of the SVG on hover and you can even animate the icons with CSS or JavaScript.

There are many ways to include an SVG on a page and each technique has its own advantages and disadvantages. For the last couple of years, I have been using a Sass function to import directly my icons in my CSS and avoid having to mess up my HTML markup.

I have a Sass list with all the source codes of my icons. Each icon is then encoded into a data URI with a Sass function and stored in a custom property on the root of the page.

TL;DR

What I have for you here is a Sass function that creates a SVG icon library directly in your CSS.

The SVG source code is compiled with the Sass function that encodes them in data URI and then stores the icons in CSS custom properties. You can then use any icon anywhere in your CSS like as if it was an external image.

This is an example pulled straight from the code of my personal site:

.c-filters__summary h2:after {   content: var(--svg-down-arrow);   position: relative;   top: 2px;   margin-left: auto;   animation: closeSummary .25s ease-out; }

Demo

Sass structure

/* All the icons source codes */ $  svg-icons: (   burger: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0...' );  /* Sass function to encode the icons */ @function svg($  name) {   @return url('data:image/svg+xml, #{$  encodedSVG} '); }  /* Store each icon into a custom property */ :root {   @each $  name, $  code in $  svg-icons {     --svg-#{$  name}: #{svg($  name)};   } }  /* Append a burger icon in my button */ .menu::after {   content: var(--svg-burger); }		

This technique has both pros and cons, so please take them into account before implementing this solution on your project:

Pros

  • There are no HTTP requests for the SVG files.
  • All of the icons are stored in one place.
  • If you need to update an icon, you don’t have to go over each HTML templates file.
  • The icons are cached along with your CSS.
  • You can manually edit the source code of the icons.
  • It does not pollute your HTML by adding extra markup.
  • You can still change the color or some aspect of the icon with CSS.

Cons

  • You cannot animate or update a specific part of the SVG with CSS.
  • The more icons you have, the heavier your CSS compiled file will be.

I mostly use this technique for icons rather than logos or illustrations. An encoded SVG is always going to be heavier than its original file, so I still load my complex SVG with an external file either with an <img> tag or in my CSS with url(path/to/file.svg).

Encoding SVG into data URI

Encoding your SVG as data URIs is not new. In fact Chris Coyier wrote a post about it over 10 years ago to explain how to use this technique and why you should (or should not) use it.

There are two ways to use an SVG in your CSS with data URI:

  • As an external image (using background-image,border-image,list-style-image,…)
  • As the content of a pseudo element (e.g. ::before or ::after)

Here is a basic example showing how you how to use those two methods:

The main issue with this particular implementation is that you have to convert the SVG manually every time you need a new icon and it is not really pleasant to have this long string of unreadable code in your CSS.

This is where Sass comes to the rescue!

Using a Sass function

By using Sass, we can make our life simpler by copying the source code of our SVG directly in our codebase, letting Sass encode them properly to avoid any browser error.

This solution is mostly inspired by an existing function developed by Threespot Media and available in their repository.

Here are the four steps of this technique:

  • Create a variable with all your SVG icons listed.
  • List all the characters that needs to be skipped for a data URI.
  • Implement a function to encode the SVGs to a data URI format.
  • Use your function in your code.

1. Icons list

/** * Add all the icons of your project in this Sass list */ $  svg-icons: (   burger: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24.8 18.92" width="24.8" height="18.92"><path d="M23.8,9.46H1m22.8,8.46H1M23.8,1H1" fill="none" stroke="#000" stroke-linecap="round" stroke-width="2"/></svg>' );

2. List of escaped characters

/** * Characters to escape from SVGs * This list allows you to have inline CSS in your SVG code as well */ $  fs-escape-chars: (   ' ': '%20',   '\'': '%22',   '"': '%27',   '#': '%23',   '/': '%2F',   ':': '%3A',   '(': '%28',   ')': '%29',   '%': '%25',   '<': '%3C',   '>': '%3E',   '\': '%5C',   '^': '%5E',   '{': '%7B',   '|': '%7C',   '}': '%7D', );

3. Encode function

/** * You can call this function by using `svg(nameOfTheSVG)` */ @function svg($  name) {   // Check if icon exists   @if not map-has-key($  svg-icons, $  name) {     @error 'icon “#{$  name}” does not exists in $  svg-icons map';     @return false;   }    // Get icon data   $  icon-map: map-get($  svg-icons, $  name);    $  escaped-string: '';   $  unquote-icon: unquote($  icon-map);   // Loop through each character in string   @for $  i from 1 through str-length($  unquote-icon) {     $  char: str-slice($  unquote-icon, $  i, $  i);      // Check if character is in symbol map     $  char-lookup: map-get($  fs-escape-chars, $  char);      // If it is, use escaped version     @if $  char-lookup != null {         $  char: $  char-lookup;     }      // Append character to escaped string     $  escaped-string: $  escaped-string + $  char;   }    // Return inline SVG data   @return url('data:image/svg+xml, #{$  escaped-string} '); }		

4. Add an SVG in your page

button {   &::after {     /* Import inline SVG */     content: svg(burger);   } }

If you have followed those steps, Sass should compile your code properly and output the following:

button::after {   content: url("data:image/svg+xml, %3Csvg%20xmlns=%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox=%270%200%2024.8%2018.92%27%20width=%2724.8%27%20height=%2718.92%27%3E%3Cpath%20d=%27M23.8,9.46H1m22.8,8.46H1M23.8,1H1%27%20fill=%27none%27%20stroke=%27%23000%27%20stroke-linecap=%27round%27%20stroke-width=%272%27%2F%3E%3C%2Fsvg%3E "); }		

Custom properties

The now-implemented Sass svg() function works great. But its biggest flaw is that an icon that is needed in multiple places in your code will be duplicated and could increase your compiled CSS file weight by a lot!

To avoid this, we can store all our icons into CSS variables and use a reference to the variable instead of outputting the encoded URI every time.

We will keep the same code we had before, but this time we will first output all the icons from the Sass list into the root of our webpage:

/**   * Convert all icons into custom properties   * They will be available to any HTML tag since they are attached to the :root   */  :root {   @each $  name, $  code in $  svg-icons {     --svg-#{$  name}: #{svg($  name)};   } }

Now, instead of calling the svg() function every time we need an icon, we have to use the variable that was created with the --svg prefix.

button::after {   /* Import inline SVG */   content: var(--svg-burger); }

Optimizing your SVGs

This technique does not provide any optimization on the source code of the SVG you are using. Make sure that you don’t leave unnecessary code; otherwise they will be encoded as well and will increase your CSS file size.

You can check this great list of tools and information on how to optimize properly your SVG. My favorite tool is Jake Archibald’s SVGOMG — simply drag your file in there and copy the outputted code.

Bonus: Updating the icon on hover

With this technique, we cannot select with CSS specific parts of the SVG. For example, there is no way to change the fill color of the icon when the user hovers the button. But there are a few tricks we can use with CSS to still be able to modify the look of our icon.

For example, if you have a black icon and you want to have it white on hover, you can use the invert() CSS filter. We can also play with the hue-rotate() filter.

That’s it!

I hope you find this little helper function handy in your own projects. Let me know what you think of the approach — I’d be interested to know how you’d make this better or tackle it differently!


How I Made an Icon System Out of CSS Custom Properties originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

CSS-Tricks

, , , ,

Avoiding the Pitfalls of Nested Components in a Design System

When creating a component-based, front-end infrastructure, one of the biggest pain points I’ve personally encountered is making components that are both reusable and responsive when there are nested components within components.

Take the following “call to action” (<CTA />) component, for example:

On smaller devices we want it to look like this:

This is simple enough with basic media queries. If we’re using flexbox, a media query can change the flex direction and makes the button go the full width. But we run into a problem when we start nesting other components in there. For example, say we’re using a component for the button and it already has a prop that makes it full-width. We are actually duplicating the button’s styling when applying a media query to the parent component. The nested button is already capable of handling it!

This is a small example and it wouldn’t be that bad of a problem, but for other scenarios it could cause a lot of duplicated code to replicate the styling. What if in the future we wanted to change something about how full-width buttons are styled? We’d need to go through and change it in all these different places. We should be able to change it in the button component and have that update everywhere.

Wouldn’t it be nice if we could move away from media queries and have more control of the styling? We should be using a component’s existing props and be able to pass different values based on the screen width.

Well, I have a way to do that and will show you how I did it.

I am aware that container queries can solve a lot of these issues, but it’s still in early days and doesn’t solve the issue with passing a variety of props based on screen width.

Tracking the window width

First, we need to track the current width of the page and set a breakpoint. This can be done with any front-end framework, but I’m going using a Vue composable here as to demonstrate the idea:

// composables/useBreakpoints.js  import { readonly, ref } from "vue";  const bps = ref({ xs: 0, sm: 1, md: 2, lg: 3, xl: 4 }) const currentBreakpoint = ref(bps.xl);  export default () => {   const updateBreakpoint = () => {        const windowWidth = window.innerWidth;          if(windowWidth >= 1200) {       currentBreakpoint.value = bps.xl     } else if(windowWidth >= 992) {       currentBreakpoint.value = bps.lg     } else if(windowWidth >= 768) {       currentBreakpoint.value = bps.md     } else if(windowWidth >= 576) {       currentBreakpoint.value = bps.sm     } else {       currentBreakpoint.value = bps.xs     }   }    return {     currentBreakpoint: readonly(currentBreakpoint),     bps: readonly(bps),     updateBreakpoint,   }; };

The reason we are using numbers for the currentBreakpoint object will become clear later.

Now we can listen for window resize events and update the current breakpoint using the composable in the main App.vue file:

// App.vue  <script> import useBreakpoints from "@/composables/useBreakpoints"; import { onMounted, onUnmounted } from 'vue'  export default {   name: 'App',      setup() {     const { updateBreakpoint } = useBreakpoints()      onMounted(() => {       updateBreakpoint();       window.addEventListener('resize', updateBreakpoint)     })      onUnmounted(() => {       window.removeEventListener('resize', updateBreakpoint)     })   } } </script>

We probably want this to be debounced, but I’m keeping things simple for brevity.

Styling components

We can update the <CTA /> component to accept a new prop for how it should be styled:

// CTA.vue props: {   displayMode: {     type: String,     default: "default"   } }

The naming here is totally arbitrary. You can use whatever names you’d like for each of the component modes.

We can then use this prop to change the mode based on the current breakpoint:

<CTA :display-mode="currentBreakpoint > bps.md ? 'default' : 'compact'" />

You can see now why we’re using a number to represent the current breakpoint — it’s so the correct mode can be applied to all breakpoints below or above a certain number.

We can then use this in the CTA component to style according to the mode passed through:

// components/CTA.vue  <template>   <div class="cta" :class="displayMode">          <div class="cta-content">       <h5>title</h5>       <p>description</p>     </div>          <Btn :block="displayMode === 'compact'">Continue</Btn>        </div> </template>  <script> import Btn from "@/components/ui/Btn"; export default {   name: "CTA",   components: { Btn },   props: {     displayMode: {       type: String,       default: "default"     },   } } </script>  <style scoped lang="scss"> .cta {   display: flex;   align-items: center;      .cta-content {     margin-right: 2rem;   }    &.compact {     flex-direction: column;     .cta-content {       margin-right: 0;       margin-bottom: 2rem;     }   } } </style>

Already, we have removed the need for media queries! You can see this in action on a demo page I created.

Admittedly, this may seem like a lengthy process for something so simple. But when applied to multiple components, this approach can massively improve the consistency and stability of the UI while reducing the total amount of code we need to write. This way of using JavaScript and CSS classes to control the responsive styling also has another benefit…

Extensible functionality for nested components

There have been scenarios where I’ve needed to revert back to a previous breakpoint for a component. For example, if it takes up 50% of the screen, I want it displayed in the small mode. But at a certain screen size, it becomes full-width. In other words, the mode should change one way or the other when there’s a resize event.

Showing three versions of a call-to-action components with nested components within it.

I’ve also been in situations where the same component is used in different modes on different pages. This isn’t something that frameworks like Bootstrap and Tailwind can do, and using media queries to pull it off would be a nightmare. (You can still use those frameworks using this technique, just without the need for the responsive classes they provide.)

We could use a media query that only applies to middle sized screens, but this doesn’t solve the issue with varying props based on screen width. Thankfully, the approach we’re covering can solve that. We can modify the previous code to allow for a custom mode per breakpoint by passing it through an array, with the first item in the array being the smallest screen size.

<CTA :custom-mode="['compact', 'default', 'compact']" />

First, let’s update the props that the <CTA /> component can accept:

props: {   displayMode: {     type: String,     default: "default"   },   customMode: {     type: [Boolean, Array],     default: false   }, }

We can then add the following to generate to correct mode:

import { computed } from "vue"; import useBreakpoints from "@/composables/useBreakpoints";  // ...  setup(props) {    const { currentBreakpoint } = useBreakpoints()    const mode = computed(() => {     if(props.customMode) {       return props.customMode[currentBreakpoint.value] ?? props.displayMode     }     return props.displayMode   })    return { mode } },

This is taking the mode from the array based on the current breakpoint, and defaults to the displayMode if one isn’t found. Then we can use mode instead to style the component.

Extraction for reusability

Many of these methods can be extracted into additional composables and mixins that can be reuseD with other components.

Extracting computed mode

The logic for returning the correct mode can be extracted into a composable:

// composables/useResponsive.js  import { computed } from "vue"; import useBreakpoints from "@/composables/useBreakpoints";  export const useResponsive = (props) => {    const { currentBreakpoint } = useBreakpoints()    const mode = computed(() => {     if(props.customMode) {       return props.customMode[currentBreakpoint.value] ?? props.displayMode     }     return props.displayMode   })    return { mode } }

Extracting props

In Vue 2, we could repeat props was by using mixins, but there are noticeable drawbacks. Vue 3 allows us to merge these with other props using the same composable. There’s a small caveat with this, as IDEs seem unable to recognize props for autocompletion using this method. If this is too annoying, you can use a mixin instead.

Optionally, we can also pass custom validation to make sure we’re using the modes only available to each component, where the first value passed through to the validator is the default.

// composables/useResponsive.js  // ...  export const withResponsiveProps = (validation, props) => {   return {     displayMode: {       type: String,       default: validation[0],       validator: function (value) {         return validation.indexOf(value) !== -1       }     },     customMode: {       type: [Boolean, Array],       default: false,       validator: function (value) {         return value ? value.every(mode => validation.includes(mode)) : true       }     },     ...props   } }

Now let’s move the logic out and import these instead:

// components/CTA.vue  import Btn from "@/components/ui/Btn"; import { useResponsive, withResponsiveProps } from "@/composables/useResponsive";  export default {   name: "CTA",   components: { Btn },   props: withResponsiveProps(['default 'compact'], {     extraPropExample: {       type: String,     },   }),      setup(props) {     const { mode } = useResponsive(props)     return { mode }   } }

Conclusion

Creating a design system of reusable and responsive components is challenging and prone to inconsistencies. Plus, we saw how easy it is to wind up with a load of duplicated code. There’s a fine balance when it comes to creating components that not only work in many contexts, but play well with other components when they’re combined.

I’m sure you’ve come across this sort of situation in your own work. Using these methods can reduce the problem and hopefully make the UI more stable, reusable, maintainable, and easy to use.


Avoiding the Pitfalls of Nested Components in a Design System originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , , , ,
[Top]

Getting Started With the File System Access API

The File System Access API is a web API that allows read and write access to a user’s local files. It unlocks new capabilities to build powerful web applications, such as text editors or IDEs, image editing tools, improved import/export, all in the frontend. Let’s look into how to get started using this API.

Screenshot of an alert popup asking the user if they want the site to be able to view their files via the File System Access API.

Reading files with the File System Access API

Before diving into the code required to read a file from the user’s system, an important detail to keep in mind is that calling the File System Access API needs to be done by a user gesture, in a secure context. In the following example, we’ll use a click event.

Reading from a single file

Reading data from a file can be done in less than 10 lines of code. Here’s an example code sample:

let fileHandle;   document.querySelector(".pick-file").onclick = async () => {  [fileHandle] = await window.showOpenFilePicker();    const file = await fileHandle.getFile();  const content = await file.text();    return content; };

Let’s imagine we have a button in our HTML with the class .pick-file. When clicking on this button, we launch the file picker by calling window.showOpenFilePicker(), and we store the result from this query in a variable called fileHandle

What we get back from calling showOpenFilePicker() is an array of FileSystemFileHandle objects representing each file we selected. As this example is for a single file, we destructure the result. I’ll show how to select multiple files a bit later.

These objects contain a kind and name property. If you were to use console.log(fileHandle), you would see the following object:

FileSystemFileHandle {kind: 'file', name: 'data.txt'}

The kind can either be file or directory.

On fileHandle, we can then call the getFile() method to get details about our file. Calling this method returns an object with a few properties, including a timestamp of when the file was last modified, the name of the file, its size, and type.

Finally, we can call text() on the file to get its content.

Reading from multiple files

To read from multiple files, we need to pass an options object to showOpenFilePicker().

For example:

let fileHandles; const options = {  multiple: true, };   document.querySelector(".pick-file").onclick = async () => {  fileHandles = await window.showOpenFilePicker(options);    // The rest of the code will be shown below };

By default, the multiple property is set to false. Other options can be used to indicate the types of files that can be selected.

For example, if we only wanted to accept .jpeg files, the options object would include the following:

const options = {  types: [    {      description: "Images",      accept: {        "image/jpeg": ".jpeg",      },    },  ],  excludeAcceptAllOption: true, };

In this example, fileHandles is an array containing multiple files, so getting their content would be done in the following way:

let fileHandles; const options = {  multiple: true, };   document.querySelector(".pick-file").onclick = async () => {  fileHandles = await window.showOpenFilePicker(options);    const allContent = await Promise.all(    fileHandles.map(async (fileHandle) => {      const file = await fileHandle.getFile();      const content = await file.text();      return content;    })  );    console.log(allContent); };

Writing to a file with the File System Access API

The File System Access API also allows you to write content to files. First, let’s look into how to save a new file.

Writing to a new file

Writing to a new file can also be done in a very short amount of code!

document.querySelector(".save-file").onclick = async () => {  const options = {    types: [      {        description: "Test files",        accept: {          "text/plain": [".txt"],        },      },    ],  };    const handle = await window.showSaveFilePicker(options);  const writable = await handle.createWritable();    await writable.write("Hello World");  await writable.close();    return handle; };

If we imagine a second button with the class save-file, on click, we open the file picker with the method showSaveFilePicker() and we pass in an option object containing the type of file to be saved, here a .txt file.

Calling this method will also return a FileSystemFileHandle object like in the first section. On this object, we can call the createWritable() method that will return a FileSystemWritableFileStream object. We can then write some content to this stream with the write() method in which we need to pass the content.

Finally, we need to call the close() method to close the file and finish writing the content to disk.

If you wanted to write some HTML code to a file for example, you would only need to change what’s in the options object to accept "text/html": [".html"]  and pass some HTML content to the write() method.

Editing an existing file

If you’d like to import a file and edit it with the File System Access API,  an example code sample would look like:

let fileHandle;   document.querySelector(".pick-file").onclick = async () => {  [fileHandle] = await window.showOpenFilePicker();    const file = await fileHandle.getFile();  const writable = await fileHandle.createWritable();    await writable.write("This is a new line");  await writable.close(); };

If you’ve been following the rest of this post, you might recognize that we start with the showOpenFilePicker() and getFile() methods to read a file and we then use createWritable(), write() and close() to write to that same file.

If the file you’re importing already has content, this code sample will replace the current content with the new one passed into the write() method.

Additional File System Access API features

Without going into too much detail, the File System Access API also lets you list files in directories and delete files or directories.

Read directories

Reading directories can be done with a tiny bit of code:

document.querySelector(".read-dir").onclick = async () => {  const directoryHandle = await window.showDirectoryPicker();    for await (const entry of directoryHandle.values()) {    console.log(entry.kind, entry.name);  } };

If we add a new button with the class .read-dir, on click, calling the showDirectoryPicker() method will open the file picker and, when selecting a directory on your computer, this code will list the files found in that directory.

Delete files

Deleting a file in a directory can be done with the following code sample:

document.querySelector(".pick-file").onclick = async () => {  const [fileHandle] = await window.showOpenFilePicker();  await fileHandle.remove(); };

If you want to delete a folder, you only need to make a small change to the code sample above:

document.querySelector(".read-dir").onclick = async () => {  const directoryHandle = await window.showDirectoryPicker();  await directoryHandle.remove(); };

Finally, if you want to remove a specific file when selecting a folder, you could write it like this:

// Delete a single file named data.txt in the selected folder document.querySelector(".pick-folder").onclick = async () => {    const directoryHandle = await window.showDirectoryPicker();    await directoryHandle.removeEntry("data.txt"); };

And if you want to remove an entire folder, you would need the following lines:

// Recursively delete the folder named "data" document.querySelector(".pick-folder").onclick = async () => {    const directoryHandle = await window.showDirectoryPicker();    await directoryHandle.removeEntry('data', { recursive: true }); };

File System Access API browser support

At the moment, IE and Firefox don’t seem to be supporting the File System Access API. However, there exists a ponyfill called browser-fs-access.

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

Chrome Firefox IE Edge Safari
101 No No 98 TP

Mobile / Tablet

Android Chrome Android Firefox Android iOS Safari
No No No 15.4

Wrapping up

If you’d like to try the File System Access API, check out this live demo text editor built by Google engineers. Otherwise, if you’d like to learn more about this API and all its features, here are some resources:


Getting Started With the File System Access API originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , , ,
[Top]

How Do You Handle Component Spacing in a Design System?

Say you’ve got a <Card /> component. It’s highly likely it shouldn’t be butted right up against any other components with no spacing around it. That’s true for… pretty much every component. So, how do you handle component spacing in a design system?

Do you apply spacing using margin directly on the <Card />? Perhaps margin-block-end: 1rem; margin-inline-end: 1rem; so it pushes away from the two sides where more content natural flows? That’s a little presumptuous. Perhaps the cards are children inside a <Grid /> component and the grid applies a gap: 1rem. That’s awkward, as now the <Card /> component spacing is going to conflict with the <Grid /> component spacing, which is very likely not what you want, not to mention the amount of space is hard coded.

Example of a component spacing where a card component is to the left of an accordion component and above an article, with 50 pixels of spacing between all three elements. Lorem i-sum text throughout in a mono font. The card has a Calvin and Hobbes comic image.
Adding space to the inline start and block end of a card component.

Different perspectives on component spacing

Eric Bailey got into this recently and looked at some options:

  • You could bake spacing into every component and try to be as clever as you can about it. (But that’s pretty limiting.)
  • You could pass in component spacing, like <Card space="xxl" />. (That can be a good approach, likely needs more than one prop, maybe even one for each direction, which is quite verbose.)
  • You could use no component spacing and create something like a <Spacer /> or <Layout /> component specifically for spacing between components. (It breaks up the job of components nicely, but can also be verbose and add unnecessary DOM weight.)

This conversation has a wide spectrum of viewpoints, some as extreme as Max Stoiber saying just never use margin ever at all. That’s a little dogmatic for me, but I like that it’s trying to rethink things. I do like the idea of taking the job of spacing and layout away from components themselves — like, for example, those content components should completely not care where they are used and let layout happen a level up from them.

Adam Argyle predicted a few years back that the use of margin in CSS would decline as the use of gap rises. He’s probably going to end up right about this, especially now that flexbox has gap and that developers have an appetite these dats to use CSS Flexbox and Grid on nearly everything at both a macro and micro level.


How Do You Handle Component Spacing in a Design System? originally published on CSS-Tricks. You should get the newsletter and become a supporter.

CSS-Tricks

, , , ,
[Top]

Open Props (and Custom Properties as a System)

Perhaps the most basic and obvious use of CSS custom properties is design tokens. Colors, fonts, spacings, timings, and other atomic bits of design that you can pull from as you design a site. If you pretty much only pull values from design tokens, you’ll be headed toward clean design and that consistent professional look that is typically the goal in web design. In fact, I’ve written that I think it’s exactly this that contributes to the popularity of utility class frameworks:

I’d argue some of that popularity is driven by the fact that if you choose from these pre-configured classes, that the design ends up fairly nice. You can’t go off the rails. You’re choosing from a limited selection of values that have been designed to look good.

I’m saying this (with a stylesheet that defines these classes as one-styling-job tokens):

<h1 class="color-primary size-large">Header<h1>

…is a similar value proposition as this:

html {   --color-primary: green;   --size-large: 3rem;   /* ... and a whole set of tokens */ }  h1 {   color: var(--color-primary);   font-size: var(--size-large); }

There are zero-build versions of both. For example, Tachyons is an it-is-what-it is stylesheet with a slew of utility classes you just use, while Windi is a whole fancy thing with a just-in-time compiler and such. Pollen is an it-is-what-it is library of custom properties you just use, while the brand new Open Props has a just-in-time compiler to only deliver the custom properties that are used.

Right, so, Open Props!

The entire thing is literally just a whole pile of CSS custom properties you can use to design stuff. It’s like a massive starting point for your styles. It’s saying custom property all the things, but in the way that we’re already used to with design tokens where they are a limited pre-determined number of choices.

The analogies are clear to people:

My guess is what will draw people to this is the beautiful defaults.

What it doesn’t do is prevent you from having to name things, which is something I know utility-class lovers really enjoy. Here, you’ll need to continue to use regular ol’ CSS selectors (like with named classes) to select things and style them as you “normally” would. But rather than hand-crafting your own values, you’re plucking values from these custom properties.

The whole base thing (you can view the source here) rolls in at 4.4kb across the wire (that’s what my DevTools showed, anyway). That doesn’t include the CSS you write to use the custom properties, but it’s a pretty tiny amount of overhead. There are additional PropPacks that increase the size (but thye are also super tiny), and if you’re worried about size, that’s what the whole just-in-time thing is about. You can play with that on StackBlitz.

Seems pretty sweet to me! I’d use it. I like that it’s ultimately just regular CSS, so there is nothing you can’t do. You’ll stay in good shape as CSS evolves.

CSS-Tricks

, , , ,
[Top]

A Handy Little System for Animated Entrances in CSS

I love little touches that make a website feel like more than just a static document. What if web content wouldn’t just “appear” when a page loaded, but instead popped, slid, faded, or spun into place? It might be a stretch to say that movements like this are always useful, though in some cases they can draw attention to certain elements, reinforce which elements are distinct from one another, or even indicate a changed state. So, they’re not totally useless, either.

So, I put together a set of CSS utilities for animating elements as they enter into view. And, yes, this pure CSS. It not only has a nice variety of animations and variations, but supports staggering those animations as well, almost like a way of creating scenes.

You know, stuff like this:

Which is really just a fancier version of this:

We’ll go over the foundation I used to create the animations first, then get into the little flourishes I added, how to stagger animations, then how to apply them to HTML elements before we also take a look at how to do all of this while respecting a user’s reduced motion preferences.

The basics

The core idea involves adding a simple CSS @keyframes animation that’s applied to anything we want to animate on page load. Let’s make it so that an element fades in, going from opacity: 0 to opacity: 1 in a half second:

.animate {   animation-duration: 0.5s;   animation-name: animate-fade;   animation-delay: 0.5s;   animation-fill-mode: backwards; }  @keyframes animate-fade {   0% { opacity: 0; }   100% { opacity: 1; } }

Notice, too, that there’s an animation-delay of a half second in there, allowing the rest of the site a little time to load first. The animation-fill-mode: backwards is there to make sure that our initial animation state is active on page load. Without this, our animated element pops into view before we want it to.

If we’re lazy, we can call it a day and just go with this. But, CSS-Tricks readers aren’t lazy, of course, so let’s look at how we can make this sort of thing even better with a system.

Fancier animations

It’s much more fun to have a variety of animations to work with than just one or two. We don’t even need to create a bunch of new @keyframes to make more animations. It’s simple enough to create new classes where all we change is which frames the animation uses while keeping all the timing the same.

There’s nearly an infinite number of CSS animations out there. (See animate.style for a huge collection.) CSS filters, like blur(), brightness() and saturate() and of course CSS transforms can also be used to create even more variations.

But for now, let’s start with a new animation class that uses a CSS transform to make an element “pop” into place.

.animate.pop {   animation-duration: 0.5s;   animation-name: animate-pop;   animation-timing-function: cubic-bezier(.26, .53, .74, 1.48); }  @keyframes animate-pop {   0% {     opacity: 0;     transform: scale(0.5, 0.5);   }    100% {     opacity: 1;     transform: scale(1, 1);   } }

I threw in a little cubic-bezier() timing curve, courtesy of Lea Verou’s indispensable cubic-bezier.com for a springy bounce.

Adding delays

We can do better! For example, we can animate elements so that they enter at different times. This creates a stagger that makes for complex-looking motion without a complex amount of code.

This animation on three page elements using a CSS filter, CSS transform, and staggered by about a tenth of a second each, feels really nice:

All we did there was create a new class for each element that spaces when the elements start animating, using animation-delay values that are just a tenth of a second apart.

.delay-1 { animation-delay: 0.6s; }   .delay-2 { animation-delay: 0.7s; } .delay-3 { animation-delay: 0.8s; }

Everything else is exactly the same. And remember that our base delay is 0.5s, so these helper classes count up from there.

Respecting accessibility preferences

Let’s be good web citizens and remove our animations for users who have enabled their reduced motion preference setting:

@media screen and (prefers-reduced-motion: reduce) {   .animate { animation: none !important; } }

This way, the animation never loads and elements enter into view like normal. It’s here, though, that is worth a reminder that “reduced” motion doesn’t always mean “remove” motion.

Applying animations to HTML elements

So far, we’ve looked at a base animation as well as a slightly fancier one that we were able to make even fancier with staggered animation delays that are contained in new classes. We also saw how we can respect user motion preferences at the same time.

Even though there are live demos that show off the concepts, we haven’t actually walked though how to apply our work to HTML. And what’s cool is that we can use this on just about any element, whether its a div, span, article, header, section, table, form… you get the idea.

Here’s what we’re going to do. We want to use our animation system on three HTML elements where each element gets three classes. We could hard-code all the animation code to the element itself, but splitting it up gives us a little animation system we can reuse.

  • .animate: This is the base class that contains our core animation declaration and timing.
  • The animation type: We’ll use our “pop” animation from before, but we could use the one that fades in as well. This class is technically optional but is a good way to apply distinct movements.
  • .delay-<number>: As we saw earlier, we can create distinct classes that are used to stagger when the animation starts on each element, making for a neat effect. This class is also optional.

So our animated elements might now look like:

<h2 class="animate pop">One!</h2> <h2 class="animate pop delay-1">Two!</h2> <h2 class="animate pop delay-2">Three!</h2>

Let’s count them in!

Conclusion

Check that out: we went from a seemingly basic set of @keyframes and turned it into a full-fledged system for applying interesting animations for elements entering into view.

This is ridiculously fun, of course. But the big takeaway for me is how the examples we looked at form a complete system that can be used to create a baseline, different types of animations, staggered delays, and an approach for respecting user motion preferences. These, to me, are all the ingredients for a flexible system that easy to use, while giving us a lot with a little and without a bunch of extra cruft.

What we covered could indeed be a full animation library. But, of course, I did’t stop there and have my entire CSS file of animations in all its glory for you. There are several more types of animations in there, including 15 classes of different delays that can be used for staggering things. I’ve been using these on my own projects, but it’s still an early draft and I love feedback on it—so please enjoy and let me know what you think in the comments!

/* ========================================================================== Animation System by Neale Van Fleet from Rogue Amoeba ========================================================================== */ .animate {   animation-duration: 0.75s;   animation-delay: 0.5s;   animation-name: animate-fade;   animation-timing-function: cubic-bezier(.26, .53, .74, 1.48);   animation-fill-mode: backwards; }  /* Fade In */ .animate.fade {   animation-name: animate-fade;   animation-timing-function: ease; }  @keyframes animate-fade {   0% { opacity: 0; }   100% { opacity: 1; } }  /* Pop In */ .animate.pop { animation-name: animate-pop; }  @keyframes animate-pop {   0% {     opacity: 0;     transform: scale(0.5, 0.5);   }   100% {     opacity: 1;     transform: scale(1, 1);   } }  /* Blur In */ .animate.blur {   animation-name: animate-blur;   animation-timing-function: ease; }  @keyframes animate-blur {   0% {     opacity: 0;     filter: blur(15px);   }   100% {     opacity: 1;     filter: blur(0px);   } }  /* Glow In */ .animate.glow {   animation-name: animate-glow;   animation-timing-function: ease; }  @keyframes animate-glow {   0% {     opacity: 0;     filter: brightness(3) saturate(3);     transform: scale(0.8, 0.8);   }   100% {     opacity: 1;     filter: brightness(1) saturate(1);     transform: scale(1, 1);   } }  /* Grow In */ .animate.grow { animation-name: animate-grow; }  @keyframes animate-grow {   0% {     opacity: 0;     transform: scale(1, 0);     visibility: hidden;   }   100% {     opacity: 1;     transform: scale(1, 1);   } }  /* Splat In */ .animate.splat { animation-name: animate-splat; }  @keyframes animate-splat {   0% {     opacity: 0;     transform: scale(0, 0) rotate(20deg) translate(0, -30px);     }   70% {     opacity: 1;     transform: scale(1.1, 1.1) rotate(15deg));   }   85% {     opacity: 1;     transform: scale(1.1, 1.1) rotate(15deg) translate(0, -10px);   }    100% {     opacity: 1;     transform: scale(1, 1) rotate(0) translate(0, 0);   } }  /* Roll In */ .animate.roll { animation-name: animate-roll; }  @keyframes animate-roll {   0% {     opacity: 0;     transform: scale(0, 0) rotate(360deg);   }   100% {     opacity: 1;     transform: scale(1, 1) rotate(0deg);   } }  /* Flip In */ .animate.flip {   animation-name: animate-flip;   transform-style: preserve-3d;   perspective: 1000px; }  @keyframes animate-flip {   0% {     opacity: 0;     transform: rotateX(-120deg) scale(0.9, 0.9);   }   100% {     opacity: 1;     transform: rotateX(0deg) scale(1, 1);   } }  /* Spin In */ .animate.spin {   animation-name: animate-spin;   transform-style: preserve-3d;   perspective: 1000px; }  @keyframes animate-spin {   0% {     opacity: 0;     transform: rotateY(-120deg) scale(0.9, .9);   }   100% {     opacity: 1;     transform: rotateY(0deg) scale(1, 1);   } }  /* Slide In */ .animate.slide { animation-name: animate-slide; }  @keyframes animate-slide {   0% {     opacity: 0;     transform: translate(0, 20px);   }   100% {     opacity: 1;     transform: translate(0, 0);   } }  /* Drop In */ .animate.drop {    animation-name: animate-drop;    animation-timing-function: cubic-bezier(.77, .14, .91, 1.25); }  @keyframes animate-drop { 0% {   opacity: 0;   transform: translate(0,-300px) scale(0.9, 1.1); } 95% {   opacity: 1;   transform: translate(0, 0) scale(0.9, 1.1); } 96% {   opacity: 1;   transform: translate(10px, 0) scale(1.2, 0.9); } 97% {   opacity: 1;   transform: translate(-10px, 0) scale(1.2, 0.9); } 98% {   opacity: 1;   transform: translate(5px, 0) scale(1.1, 0.9); } 99% {   opacity: 1;   transform: translate(-5px, 0) scale(1.1, 0.9); } 100% {   opacity: 1;   transform: translate(0, 0) scale(1, 1);   } }  /* Animation Delays */ .delay-1 {   animation-delay: 0.6s; } .delay-2 {   animation-delay: 0.7s; } .delay-3 {   animation-delay: 0.8s; } .delay-4 {   animation-delay: 0.9s; } .delay-5 {   animation-delay: 1s; } .delay-6 {   animation-delay: 1.1s; } .delay-7 {   animation-delay: 1.2s; } .delay-8 {   animation-delay: 1.3s; } .delay-9 {   animation-delay: 1.4s; } .delay-10 {   animation-delay: 1.5s; } .delay-11 {   animation-delay: 1.6s; } .delay-12 {   animation-delay: 1.7s; } .delay-13 {   animation-delay: 1.8s; } .delay-14 {   animation-delay: 1.9s; } .delay-15 {   animation-delay: 2s; }  @media screen and (prefers-reduced-motion: reduce) {   .animate {     animation: none !important;   } }

The post A Handy Little System for Animated Entrances in CSS appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

System *Things

I think we’re all largely aware of named colors in CSS:

color: OldLace; background: rebeccapurple;

I guess you’d just call those “named colors” in CSS.

Those aren’t the only kind of named colors there are though. Some of them are a bit more fluid. Jim Nielsen was blowin’ minds the other day when he blogged about System Colors.

What I need is a way to say “hey browser, for my dropdown, use the same black (or white if in light mode) that you’re using for the background color of the document”. I need access to a variable of sorts that references the exact “black” the browser is using.

Then, via Thomas Steiner, I discovered there are literally CSS system colors. These aren’t colors that are (or at least attempt to be) the same across all browsers, but they are allowed to be set by “choices made by the user, the browser, or the OS.” So for example, Canvas is the “background of application content or documents.” Case in point: the background-color for dark mode is #1e1e1e in Safari and #121212 in Chrome. If you like that, meaning you’re leaning into what the browser thinks is a good color for things, then you can now access it through that Canvas keyword.

System colors! There are a bunch of them.

  • Canvas
  • CanvasText
  • LinkText
  • VisitedText
  • ActiveText
  • ButtonFace
  • ButtonText
  • ButtonBorder
  • Field
  • FieldText
  • Highlight
  • HighlightText
  • Mark
  • MarkText
  • GrayText

Not only do they change across browsers, they change when toggling between dark and light mode as long as you have CSS in place to support them…

html {   color-scheme: light dark; }

You’ll see them change when modes change. And you don’t have to use them for what they were designed for, I suppose:

So those are the system colors, but you can see right in that Pen that I’ve also used a system font: system-ui. Same vibe! It’s purposely fluid. It’s not going to be the same typeface across browsers and operating systems. Jim also covered this a while back. We used to replicate the idea with a big long stack of named fonts, but now CSS helps with it (in supporting browsers).

There are a bunch of them specced:

  • serif
  • sans-serif
  • monospace
  • system-ui
  • cursive
  • fantasy
  • emoji
  • math
  • fangsong
  • ui-serif
  • ui-sans-serif
  • ui-monospace
  • ui-rounded

Support seems scattered. For example, I could set this:

p {   font-family: ui-monospace, system-ui, fantasy; }

On my Mac, in Safari, I’d get SF Mono (ui-monospace). But in Chrome, ui-monospace doesn’t work so it would fall back to SF Pro (system-ui). In Firefox neither ui-monospace or system-ui work and I’d get Papyrus (fantasy). So font stacks are still important. It’s just funny to think about because these new system font keywords are almost like font stacks in and of themselves.

So there are system colors and system fonts — doesn’t that beg the question of what other system things there are?

Well, there are named font weights — like how font-weight: bold; is the same as 700, and bolder is just a bit more bold than the parent. But that doesn’t feel like a system-level thing where the system would want to take hold of that and do different things. But hey, maybe.

There are also named font sizes, like font-size: xx-small;. I could see systems wanting to get their hands on those values and adjust them to sizes that make sense contextually, but in a quick glance (comparing Chrome and iOS Safari), they compute to the same sizes.

Those named font size values don’t travel, either. I can’t do margin: large;. Well, I can, but it doesn’t do anything. So no real universal system sizes.

What about system icons? We do kinda have those in the form of emoji! We use the emoji knowing that different systems will render it differently and are generally fine with that as we know it will look consistent with that user’s platform.

The “Blue Book” emoji (via Emojipedia)

We could sort of think of inputs as “system inputs.” We know different browsers and platforms render input controls in very different ways, and that is how the spec intends it. To each their own.


The post System *Things appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

,
[Top]

Using Your Own Design System with KendoReact Components

Maybe you’ve already heard of (or even worked with!) KendoReact. It’s popped up in some of my day-to-day conversations, especially those about working with design systems and React. You could think of it as a component library like Bootstrap or Material Design, except the components in KendoReact are far more robust. These are interactive, state-driven components ready to start building full-blown UI’s right out of the gate (not to mention, if you want to use Bootstrap as the theme, you absolutely can).

Whenever you’re thinking about using a UI library, you need to think about the styling capabilities. Are you able to really express your brand with these? Were they meant to be styled? What is the styling experience going to be like?

Fortunately, KendoReact really makes styling a citizen of the entire UI library.

KendoReact is a collection of UI components for building sites. It’s a pretty massive one. Over 80 by my count, and that doesn’t include the child components of heavy lifters like the <Grid /> family.

Here’s one, the <DropDownList />, and just using the default theme (even that is optional):

If I want to style this, I don’t need any special proprietary skills, I can just use CSS. Here’s me forcing a whole new look onto it with different colors and fonts, with just some simple CSS:

But hey, maybe you want to do something a bit more systematized than cowboying some random override CSS. I don’t blame you. Good news: KendoReact themes are Sass-powered. So you can control a lot of the colorization and styling just by changing a few Sass variables.

They have a whole theme builder you can use right on their site that spits out exactly what you need. Say you want to start from their base theme and go from there, select the Default theme:

Then you can play with all the colors in the UI to your liking. Here’s me poking at a theme with some CSS-Tricks colors.

I can download that from the site which will give me the variables as a SCSS file that I can apply before the default theme in my build (there is a great tutorial covering how to do that over on the Telerik blog). Plus, it gives me the whole dang CSS file of the theme if I want to use it that way, which is simple and quick. Here’s me using their conversational chat widget with that theme:

Again, I can start with Bootstrap, I can start with Material, I can start with their default theme, or I can start from scratch. Styling is totally up to me. Each theme has its perks and, as you might expect, are super flexible as far as configuring colors, fonts, and other design elements.

If you really get into this, of course you’ll be consulting their docs and finding your way around there (it’s nice to know they have really comprehensive docs). It’s all pretty straightforward though, you’ll do great! If you need to get going building out a state-driven interactive interface quickly without sacrificing any customizability or power, you’ll find KendoReact is your friend.


The post Using Your Own Design System with KendoReact Components appeared first on CSS-Tricks.

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

CSS-Tricks

, , , ,
[Top]

Setting up and Customizing the Ant Design System in a Nuxt App

I don’t typically work with UI libraries because they can be cumbersome and hard to override, which can contribute to a bloated. However, Ant Design has recently gained some some of my affection because it’s easy to use, has extensible defaults, and features a delicate design.

Nuxt and Ant Design work well together, in part because of Nuxt’s code-splitting and tree-shaking abilities, not to mention Nuxt’s new static target deployment option. I can serve an app using Ant Design with great performance scores.

Combining the two was a little tricky and there isn’t a lot in the way of documentation for how to do it, so what follows are the steps you need to set it up. Let’s get started!

Install Ant.design 

The first step is installing the ant-design-vue package, along with Less.js and less-loader, which we will need to create our Less variables:

yarn add ant-design-vue less less-loader # or npm i ant-design-vue less less-loader

Now lets tell Nuxt to use it globally via a plugin. We’ll create a file called antd-ui.js:

import Vue from 'vue' import Antd from 'ant-design-vue/lib'  Vue.use(Antd)

You may notice that unlike the process outlined in the Ant Design getting started guide, we are not importing the global CSS file they mention. That’s because we’re going to manually import the base variable Less file instead so that we can override it. 

We have a few things to do in our nuxt.config.js file. First, let’s register the plugin we just made:

plugins: ["@/plugins/antd-ui"],

Next, we’re going to let webpack know we’d like to build Less:

build: {    loaders: {      less: {        lessOptions: {          javascriptEnabled: true,        },     },   }, }

Finally, we need to create a global stylesheet for our variables that imports Ant Design’s defaults as well as our overrides:

css: [   "~/assets/variables.less" ],

We can see that this file exists in a /assets folder, so let’s make it. We’ll create a file in there called variables.less, and import Ant Design’s Less variables:

@import '~ant-design-vue/dist/antd.less';

Below this line, there are myriad variables you can override. This is just a sampling. The rest of the variables are here, and you’ll need to include them by their @ and can change it to whatever you wish:

@primary-color: #1890ff; // primary color for all components @link-color: #1890ff; // link color @success-color: #52c41a; // success state color @warning-color: #faad14; // warning state color @error-color: #f5222d; // error state color @font-size-base: 14px; // major text font size @heading-color: rgba(0, 0, 0, 0.85); // heading text color @text-color: rgba(0, 0, 0, 0.65); // major text color @text-color-secondary: rgba(0, 0, 0, 0.45); // secondary text color @disabled-color: rgba(0, 0, 0, 0.25); // disable state color @border-radius-base: 4px; // major border radius @border-color-base: #d9d9d9; // major border color @box-shadow-base: 0 2px 8px rgba(0, 0, 0, 0.15); // major shadow for layers

We’re good to go! There’s no need to import what we need into every component because Nuxt will now take care of that. If you’d like to override very specific styles not included in the variables, you can find the associative classes and override them in your layouts/default.vue file as well.

Ant.design and Nuxt allow you a great framework for building apps very quickly and with ease. Enjoy!


The post Setting up and Customizing the Ant Design System in a Nuxt App appeared first on CSS-Tricks.

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

CSS-Tricks

, , , ,
[Top]

Designing a JavaScript Plugin System

WordPress has plugins. jQuery has plugins. Gatsby, Eleventy, and Vue do, too.

Plugins are a common feature of libraries and frameworks, and for a good reason: they allow developers to add functionality, in a safe, scalable way. This makes the core project more valuable, and it builds a community — all without creating an additional maintenance burden. What a great deal!

So how do you go about building a plugin system? Let’s answer that question by building one of our own, in JavaScript.

I’m using the word “plugin” but these things are sometimes called other names, like “extensions,” “add-ons,” or “modules.” Whatever you call them, the concept (and benefit) is the same.

Let’s build a plugin system

Let’s start with an example project called BetaCalc. The goal for BetaCalc is to be a minimalist JavaScript calculator that other developers can add “buttons” to. Here’s some basic code to get us started:

// The Calculator const betaCalc = {   currentValue: 0,      setValue(newValue) {     this.currentValue = newValue;     console.log(this.currentValue);   },      plus(addend) {     this.setValue(this.currentValue + addend);   },      minus(subtrahend) {     this.setValue(this.currentValue - subtrahend);   } }; 
 // Using the calculator betaCalc.setValue(3); // => 3 betaCalc.plus(3);     // => 6 betaCalc.minus(2);    // => 4

We’re defining our calculator as an object-literal to keep things simple. The calculator works by printing its result via console.log.

Functionality is really limited right now. We have a setValue method, which takes a number and displays it on the “screen.” We also have plus and minus methods, which will perform an operation on the currently displayed value.

It’s time to add more functionality. Let’s start by creating a plugin system.

The world’s smallest plugin system

We’ll start by creating a register method that other developers can use to register a plugin with BetaCalc. The job of this method is simple: take the external plugin, grab its exec function, and attach it to our calculator as a new method:

// The Calculator const betaCalc = {   // ...other calculator code up here 
   register(plugin) {     const { name, exec } = plugin;     this[name] = exec;   } };

And here’s an example plugin, which gives our calculator a “squared” button:

// Define the plugin const squaredPlugin = {   name: 'squared',   exec: function() {     this.setValue(this.currentValue * this.currentValue)   } }; 
 // Register the plugin betaCalc.register(squaredPlugin);

In many plugin systems, it’s common for plugins to have two parts:

  1. Code to be executed
  2. Metadata (like a name, description, version number, dependencies, etc.)

In our plugin, the exec function contains our code, and the name is our metadata. When the plugin is registered, the exec function is attached directly to our betaCalc object as a method, giving it access to BetaCalc’s this.

So now, BetaCalc has a new “squared” button, which can be called directly:

betaCalc.setValue(3); // => 3 betaCalc.plus(2);     // => 5 betaCalc.squared();   // => 25 betaCalc.squared();   // => 625

There’s a lot to like about this system. The plugin is a simple object-literal that can be passed into our function. This means that plugins can be downloaded via npm and imported as ES6 modules. Easy distribution is super important!

But our system has a few flaws.

By giving plugins access to BetaCalc’s this, they get read/write access to all of BetaCalc’s code. While this is useful for getting and setting the currentValue, it’s also dangerous. If a plugin was to redefine an internal function (like setValue), it could produce unexpected results for BetaCalc and other plugins. This violates the open-closed principle, which states that a software entity should be open for extension but closed for modification.

Also, the “squared” function works by producing side effects. That’s not uncommon in JavaScript, but it doesn’t feel great — especially when other plugins could be in there messing with the same internal state. A more functional approach would go a long way toward making our system safer and more predictable.

A better plugin architecture

Let’s take another pass at a better plugin architecture. This next example changes both the calculator and its plugin API:

// The Calculator const betaCalc = {   currentValue: 0,      setValue(value) {     this.currentValue = value;     console.log(this.currentValue);   },     core: {     'plus': (currentVal, addend) => currentVal + addend,     'minus': (currentVal, subtrahend) => currentVal - subtrahend   }, 
   plugins: {},     
   press(buttonName, newVal) {     const func = this.core[buttonName] || this.plugins[buttonName];     this.setValue(func(this.currentValue, newVal));   }, 
   register(plugin) {     const { name, exec } = plugin;     this.plugins[name] = exec;   } };    // Our Plugin const squaredPlugin = {    name: 'squared',   exec: function(currentValue) {     return currentValue * currentValue;   } }; 
 betaCalc.register(squaredPlugin); 
 // Using the calculator betaCalc.setValue(3);      // => 3 betaCalc.press('plus', 2); // => 5 betaCalc.press('squared'); // => 25 betaCalc.press('squared'); // => 625

We’ve got a few notable changes here.

First, we’ve separated the plugins from “core” calculator methods (like plus and minus), by putting them in their own plugins object. Storing our plugins in a plugin object makes our system safer. Now plugins accessing this can’t see the BetaCalc properties — they can only see properties of betaCalc.plugins.

Second, we’ve implemented a press method, which looks up the button’s function by name and then calls it. Now when we call a plugin’s exec function, we pass it the current calculator value (currentValue), and we expect it to return the new calculator value.

Essentially, this new press method converts all of our calculator buttons into pure functions. They take a value, perform an operation, and return the result. This has a lot of benefits:

  • It simplifies the API.
  • It makes testing easier (for both BetaCalc and the plugins themselves).
  • It reduces the dependencies of our system, making it more loosely coupled.

This new architecture is more limited than the first example, but in a good way. We’ve essentially put up guardrails for plugin authors, restricting them to only the kind of changes that we want them to make.

In fact, it might be too restrictive! Now our calculator plugins can only do operations on the currentValue. If a plugin author wanted to add advanced functionality like a “memory” button or a way to track history, they wouldn’t be able to.

Maybe that’s ok. The amount of power you give plugin authors is a delicate balance. Giving them too much power could impact the stability of your project. But giving them too little power makes it hard for them to solve their problems — in that case you might as well not have plugins.

What more could we do?

There’s a lot more we could do to improve our system.

We could add error handling to notify plugin authors if they forget to define a name or return a value. It’s good to think like a QA dev and imagine how our system could break so we can proactively handle those cases.

We could expand the scope of what a plugin can do. Currently, a BetaCalc plugin can add a button. But what if it could also register callbacks for certain lifecycle events — like when the calculator is about to display a value? Or what if there was a dedicated place for it to store a piece of state across multiple interactions? Would that open up some new use cases?

We could also expand plugin registration. What if a plugin could be registered with some initial settings? Could that make the plugins more flexible? What if a plugin author wanted to register a whole suite of buttons instead of a single one — like a “BetaCalc Statistics Pack”? What changes would be needed to support that?

Your plugin system

Both BetaCalc and its plugin system are deliberately simple. If your project is larger, then you’ll want to explore some other plugin architectures.

One good place to start is to look at existing projects for examples of successful plugin systems. For JavaScript, that could mean jQuery, Gatsby, D3, CKEditor, or others.

You may also want to be familiar with various JavaScript design patterns. (Addy Osmani has a book on the subject.)  Each pattern provides a different interface and degree of coupling, which gives you a lot of good plugin architecture options to choose from. Being aware of these options helps you better balance the needs of everyone who uses your project.

Besides the patterns themselves, there’s a lot of good software development principles you can draw on to make these kinds of decisions. I’ve mentioned a few along the way (like the open-closed principle and loose coupling), but some other relevant ones include the Law of Demeter and dependency injection.

I know it sounds like a lot, but you’ve gotta do your research. Nothing is more painful than making everyone rewrite their plugins because you needed to change the plugin architecture. It’s a quick way to lose trust and discourage people from contributing in the future.

Conclusion

Writing a good plugin architecture from scratch is difficult! You have to balance a lot of considerations to build a system that meets everyone’s needs. Is it simple enough? Powerful enough? Will it work long term?

It’s worth the effort though. Having a good plugin system helps everyone. Developers get the freedom to solve their problems. End users get a large number of opt-in features to choose from. And you get to grow an ecosystem and community around your project. It’s a win-win-win situation.


The post Designing a JavaScript Plugin System appeared first on CSS-Tricks.

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

CSS-Tricks

, , ,
[Top]