SVG Coding Examples: Useful Recipes For Writing Vectors By Hand

Even though I am the kind of front-end engineer who manually cleans up SVG files when they are a mess, I never expected to become one of those people. You know, those crazy people that draw with code.

But here we are.

I dove deep into SVG specs last winter when I created a project to , and even though I knew the basic structures and rules of SVG, it was only then that I fully tried to figure out and understand what all of those numbers meant and how they interacted with each other.

And, once you get the hang of it, it is actually very interesting and quite fun to code SVG by hand.

No <path> ahead

We won’t go into more complex SVG shapes like in this article, this is more about practical information for simple SVGs. When it comes to drawing curves, I still recommend using a tool like Illustrator or Affinity. However, if you are super into compounding your lines, a path is useful. Maybe we’ll do that in Part 2.

Also, this guide focuses mostly on practical examples that illustrate some of the math involved when drawing SVGs. There is a wonderful article here that goes a bit deeper into the specs, which I recommend reading if you’re more interested in that: “.”

Drawing With Math. Remember Coordinate Systems?

Illustrator, Affinity, and all other vector programs are basically just helping you draw on a coordinate system, and then those paths and shapes are stored in SVG files.

If you open up these files in an editor, you’ll see that they are just a bunch of paths that contain lots of numbers, which are coordinates in that coordinate system that make up the lines.

But, there is a difference between the all-powerful <path> and the other, more semantic elements like <rect>, <circle>, <line>, <ellipse>, <polygon>, and <polyline>.

These elements are not that hard to read and write by hand, and they open up a lot of possibilities to add animation and other fun stuff. So, while most people might only think of SVGs as never-pixelated, infinitely scaling images, they can also be quite comprehensive pieces of code.

How Does SVG Work? unit != unit

Before we get started on how SVG elements are drawn, let’s talk about the ways units work in SVG because they might be a bit confusing when you first get started.

The beauty of SVG is that it’s a vector format, which means that the units are somewhat detached from the browser and are instead just relative to the coordinate system you’re working in.

That means you would not use a unit within SVG but rather just use numbers and then define the size of the document you’re working with.

So, your width and height might be using CSS rem units, but in your viewBox, units become just a concept that helps you in establishing sizing relationships.

What Is The viewBox?

The viewBox works a little bit like the CSS aspect-ratio property. It helps you establish a relationship between the width and the height of your coordinate system and sets up the box you’re working in. I tend to think of the viewBox as my “document” size.

Any element that is placed within the SVG with bigger dimensions than the viewBox will not be visible. So, the viewBox is the cutout of the coordinate system we’re looking through. The width and height attributes are unnecessary if there is a viewBox attribute.

So, in short, having an SVG with a viewBox makes it behave a lot like a regular image. And just like with images, it’s usually easiest to just set either a width or a height and let the other dimension be automatically sized based on the intrinsic aspect ratio dimensions.

So, if we were to create a function that draws an SVG, we might store three separate variables and fill them in like this:

`<svg 
  width="${svgWidth}" 
  viewBox="0 0 ${documentWidth} ${documentHeight}" 
  xmlns="http://www.w3.org/2000/svg"
>`;

SVG Things Of Note

There is a lot to know about SVG: When you want to reuse an image a lot, you may want to turn it into a symbol that can then be referenced with a use tag, you can create sprites, and there are some best practices when using them for icons, and so on.

Unfortunately, this is a bit out of the scope of this article. Here, we’re mainly focusing on designing SVG files and not on how we can optimize and use them.

However, one thing of note that is easier to implement from the start is accessibility.

SVGs can be used in an <img> tag, where alt tags are available, but then you lose the ability to interact with your SVG code, so inlining might be your preference.

When inlining, it’s easiest to declare role="img" and then add a <title> tag with your image title.

Note: You can check out .

<svg
  role="img"
  [...attr]
>
  <title>An accessible title</title>
  <!-- design code -->
</svg>

Drawing SVG With JavaScript

There is usually some mathematics involved when drawing SVGs. It’s usually fairly simple arithmetic (except, you know, in case you draw calligraphy grids and then have to dig out trigonometry…), but I think even for simple math, most people don’t write their SVGs in pure HTML and thus would like to use algebra.

At least for me, I find it much easier to understand SVG Code when giving meaning to numbers, so I always stick to JavaScript, and by giving my coordinates names, I like them immeasurable times more.

So, for the upcoming examples, we’ll look at the list of variables with the simple math and then JSX-style templates for interpolation, as that gives more legible syntax highlighting than string interpolations, and then each example will be available as a CodePen.

To keep this Guide framework-agnostic, I wanted to quickly go over drawing SVG elements with just good old vanilla JavaScript.

We’ll create a container element in HTML that we can put our SVG into and grab that element with JavaScript.

<div data-svg-container></div>
<script src="template.js"></script>

To make it simple, we’ll draw a rectangle <rect> that covers the entire viewBox and uses a fill.

Note: You can add all valid CSS values as fills, so a fixed color, or something like currentColor to access the site’s text color or a CSS variable would work here if you’re inlining your SVG and want it to interact with the page it’s placed in.

Let’s first start with our variable setup.

// vars
const container = document.querySelector("[data-svg-container]");
const svgWidth = "30rem"; // use any value with units here
const documentWidth = 100;
const documentHeight = 100;
const rectWidth = documentWidth;
const rectHeight = documentHeight;
const rectFill = "currentColor"; // use any color value here
const title = "A simple square box";

Method 1: Create Element and Set Attributes

This method is easier to keep type-safe (if using TypeScript) — uses proper SVG elements and attributes, and so on — but it is less performant and may take a long time if you have many elements.

const svg = document.createElementNS(", "svg");
const titleElement = document.createElementNS(", "title");
const rect = document.createElementNS(", "rect");

svg.setAttribute("width", svgWidth);
svg.setAttribute("viewBox", 0 0 ${documentWidth} ${documentHeight});
svg.setAttribute("xmlns", ";
svg.setAttribute("role", "img");

titleElement.textContent = title;

rect.setAttribute("width", rectWidth);
rect.setAttribute("height", rectHeight);
rect.setAttribute("fill", rectFill);

svg.appendChild(titleElement);
svg.appendChild(rect);

container.appendChild(svg);

Here, you can see that with the same coordinates, a polyline won’t draw the line between the blue and the red dot, while a polygon will. However, when applying a fill, they take the exact same information as if the shape was closed, which is the right side of the graphic, where the polyline makes it look like a piece of a circle is missing.

This is the second time where we have dealt with quite a bit of repetition, and we can have a look at how we could leverage the power of JavaScript logic to render our template faster.

But first, we need a basic implementation like we’ve done before. We’re creating objects for the circles, and then we’re chaining the cx and cy values together to create the points attribute. We’re also storing our transforms in variables.

const polyDocWidth = 200;
const polyDocHeight = 200;
const circleOne = { cx: 25, cy: 80, r: 10, fill: "red" };
const circleTwo = { cx: 40, cy: 20, r: 5, fill: "lime" };
const circleThree = { cx: 70, cy: 60, r: 8, fill: "cyan" };
const points = ${circleOne.cx},${circleOne.cy} ${circleTwo.cx},${circleTwo.cy} ${circleThree.cx},${circleThree.cy};
const moveToTopRight = translate(${polyDocWidth / 2}, 0);
const moveToBottomRight = translate(${polyDocWidth / 2}, ${polyDocHeight / 2});
const moveToBottomLeft = translate(0, ${polyDocHeight / 2});

And then, we apply the variables to the template, using either a polyline or polygon element and a fill attribute that is either set to none or a color value.


<svg
  width={svgWidth}
  viewBox={`0 0 ${polyDocWidth} ${polyDocHeight}`}
  xmlns="http://www.w3.org/2000/svg"
  role="img"
>
  <title>Composite shape comparison</title>
  <g>
    <circle
      cx={circleOne.cx}
      cy={circleOne.cy}
      r={circleOne.r}
      fill={circleOne.fill}
    />
    <circle
      cx={circleTwo.cx}
      cy={circleTwo.cy}
      r={circleTwo.r}
      fill={circleTwo.fill}
    />
    <circle
      cx={circleThree.cx}
      cy={circleThree.cy}
      r={circleThree.r}
      fill={circleThree.fill}
    />
    <polyline
      points={points}
      fill="none"
      stroke="black"
    />
  </g>
  <g transform={moveToTopRight}>
    <circle
      cx={circleOne.cx}
      cy={circleOne.cy}
      r={circleOne.r}
      fill={circleOne.fill}
    />
    <circle
      cx={circleTwo.cx}
      cy={circleTwo.cy}
      r={circleTwo.r}
      fill={circleTwo.fill}
    />
    <circle
      cx={circleThree.cx}
      cy={circleThree.cy}
      r={circleThree.r}
      fill={circleThree.fill}
    />
    <polyline
      points={points}
      fill="white"
      stroke="black"
    />
  </g>
  <g transform={moveToBottomLeft}>
    <circle
      cx={circleOne.cx}
      cy={circleOne.cy}
      r={circleOne.r}
      fill={circleOne.fill}
    />
    <circle
      cx={circleTwo.cx}
      cy={circleTwo.cy}
      r={circleTwo.r}
      fill={circleTwo.fill}
    />
    <circle
      cx={circleThree.cx}
      cy={circleThree.cy}
      r={circleThree.r}
      fill={circleThree.fill}
    />
    <polygon
      points={points}
      fill="none"
      stroke="black"
    />
  </g>
  <g transform={moveToBottomRight}>
    <circle
      cx={circleOne.cx}
      cy={circleOne.cy}
      r={circleOne.r}
      fill={circleOne.fill}
    />
    <circle
      cx={circleTwo.cx}
      cy={circleTwo.cy}
      r={circleTwo.r}
      fill={circleTwo.fill}
    />
    <circle
      cx={circleThree.cx}
      cy={circleThree.cy}
      r={circleThree.r}
      fill={circleThree.fill}
    />
    <polygon
      points={points}
      fill="white"
      stroke="black"
    />
  </g>
</svg>

And here’s a version of it to play with:

See the Pen by .

Dealing With Repetition

When it comes to drawing SVGs, you may find that you’ll be repeating a lot of the same code over and over again. This is where JavaScript can come in handy, so let’s look at the composite example again and see how we could optimize it so that there is less repetition.

Observations:

  • We have three circle elements, all following the same pattern.
  • We create one repetition to change the fill style for the element.
  • We repeat those two elements one more time, with either a polyline or a polygon.
  • We have four different transforms (technically, no transform is a transform in this case).

This tells us that we can create nested loops.

Let’s go back to just a vanilla implementation for this since the way loops are done is quite different across frameworks.

You could make this more generic and write separate generator functions for each type of element, but this is just to give you an idea of what you could do in terms of logic. There are certainly still ways to optimize this.

I’ve opted to have arrays for each type of variation that we have and wrote a helper function that goes through the data and builds out an array of objects with all the necessary information for each group. In such a short array, it would certainly be a viable option to just have the data stored in one element, where the values are repeated, but we’re taking the DRY thing seriously in this one.

The group array can then be looped over to build our SVG HTML.

const container = document.querySelector("[data-svg-container]");
const svgWidth = 200;
const documentWidth = 200;
const documentHeight = 200;
const halfWidth = documentWidth / 2;
const halfHeight = documentHeight / 2;
const circles = [
  { cx: 25, cy: 80, r: 10, fill: "red" },
  { cx: 40, cy: 20, r: 5, fill: "lime" },
  { cx: 70, cy: 60, r: 8, fill: "cyan" },
];
const points = circles.map(({ cx, cy }) => ${cx},${cy}).join(" ");
const elements = ["polyline", "polygon"];
const fillOptions = ["none", "white"];
const transforms = [
  undefined,
  translate(${halfWidth}, 0),
  translate(0, ${halfHeight}),
  translate(${halfWidth}, ${halfHeight}),
];
const makeGroupsDataObject = () => {
  let counter = 0;
  const g = [];
  elements.forEach((element) => {
    fillOptions.forEach((fill) => {
      const transform = transforms[counter++];
      g.push({ element, fill, transform });
    });
  });
  return g;
};
const groups = makeGroupsDataObject();
// result:
// [
//   {
//     element: "polyline",
//     fill: "none",
//   },
//   {
//     element: "polyline",
//     fill: "white",
//     transform: "translate(100, 0)",
//   },
//   {
//     element: "polygon",
//     fill: "none",
//     transform: "translate(0, 100)",
//   },
//   {
//     element: "polygon",
//     fill: "white",
//     transform: "translate(100, 100)",
//   }
// ]

const svg = document.createElementNS(", "svg");
svg.setAttribute("width", svgWidth);
svg.setAttribute("viewBox", 0 0 ${documentWidth} ${documentHeight});
svg.setAttribute("xmlns", ";
svg.setAttribute("role", "img");
svg.innerHTML = "<title>Composite shape comparison</title>";
groups.forEach((groupData) => {
  const circlesHTML = circles
    .map((circle) => {
      return &lt;circle 
          cx="${circle.cx}" 
          cy="${circle.cy}" 
          r="${circle.r}" 
          fill="${circle.fill}"
        /&gt;;
    })
    .join("");
  const polyElementHTML = &lt;${groupData.element} 
      points="${points}" 
      fill="${groupData.fill}" 
      stroke="black" 
    /&gt;;
  const group = &lt;g ${groupData.transform ?transform="${groupData.transform}": ""}&gt;
        ${circlesHTML}
        ${polyElementHTML}
      &lt;/g&gt;;
  svg.innerHTML += group;
});
container.appendChild(svg);

And here’s the Codepen of that:

See the Pen by .

More Fun Stuff

Now, that’s all the basics I wanted to cover, but there is so much more you can do with SVG. There is more you can do with transform; you can use a mask, you can use a marker, and so on.

We don’t have time to dive into all of them today, but since this started for me when making Calligraphy Grids, I wanted to show you the two most satisfying ones, which I, unfortunately, can’t use in the generator since I wanted to be able to open my generated SVGs in Affinity and it doesn’t support pattern.

Okay, so pattern is part of the defs section within the SVG, which is where you can define reusable elements that you can then reference in your SVG.

Graph Grid with pattern

If you think about it, a graph is just a bunch of horizontal and vertical lines that repeat across the x- and y-axis.

So, pattern can help us with that. We can create a <rect> and then reference a pattern in the fill attribute of the rect. The pattern then has its own width, height, and viewBox, which defines how the pattern is repeated.

So, let’s say we want to perfectly center our graph grid in any given width or height, and we want to be able to define the size of our resulting squares (cells).

Once again, let’s start with the JavaScipt variables:

const graphDocWidth = 226;
const graphDocHeight = 101;
const cellSize = 5;
const strokeWidth = 0.3;
const strokeColor = "currentColor";
const patternHeight = (cellSize / graphDocHeight) * 100;
const patternWidth = (cellSize / graphDocWidth) * 100;
const gridYStart = (graphDocHeight % cellSize) / 2;
const gridXStart = (graphDocWidth % cellSize) / 2;

Now, we can apply them to the SVG element:

<svg
  width={svgWidth}
  viewBox={`0 0 ${graphDocWidth} ${graphDocHeight}`}
  xmlns="http://www.w3.org/2000/svg"
  role="img"
>
  <defs>
    <pattern
      id="horizontal"
      viewBox={`0 0 ${graphDocWidth} ${strokeWidth}`}
      width="100%"
      height={`${patternHeight}%`}
    >
      <line
        x1="0"
        x2={graphDocWidth}
        y1={gridYStart}
        y2={gridYStart}
        stroke={strokeColor}
        stroke-width={strokeWidth}
      />
    </pattern>
    <pattern
      id="vertical"
      viewBox={`0 0 ${strokeWidth} ${graphDocHeight}`}
      width={`${patternWidth}%`}
      height="100%"
    >
      <line
        y1={0}
        y2={graphDocHeight}
        x1={gridXStart}
        x2={gridXStart}
        stroke={strokeColor}
        stroke-width={strokeWidth}
      />
    </pattern>
  </defs>
  <title>A graph grid</title>
  <rect
    width={graphDocWidth}
    height={graphDocHeight}
    fill="url(#horizontal)"
  />
  <rect
    width={graphDocWidth}
    height={graphDocHeight}
    fill="url(#vertical)"
  />
</svg>

And this is what that then looks like:

See the Pen by .

Dot Grid With pattern

If we wanted to draw a dot grid instead, we could simply repeat a circle. Or, we could alternatively use a line with a stroke-dasharray and stroke-dashoffset to create a dashed line. And we’d only need one line in this case.

Starting with our JavaScript variables:

const dotDocWidth = 219;
const dotDocHeight = 100;
const cellSize = 4;
const strokeColor = "black";
const gridYStart = (dotDocHeight % cellSize) / 2;
const gridXStart = (dotDocWidth % cellSize) / 2;
const dotSize = 0.5;
const patternHeight = (cellSize / dotDocHeight) * 100;

And then adding them to the SVG element:

<svg
  width={svgWidth}
  viewBox={`0 0 ${dotDocWidth} ${dotDocHeight}`}
  xmlns="http://www.w3.org/2000/svg"
  role="img"
>
  <defs>
    <pattern
      id="horizontal-dotted-line"
      viewBox={`0 0 ${dotDocWidth} ${dotSize}`}
      width="100%"
      height={`${patternHeight}%`}
    >
      <line
        x1={gridXStart}
        y1={gridYStart}
        x2={dotDocWidth}
        y2={gridYStart}
        stroke={strokeColor}
        stroke-width={dotSize}
        stroke-dasharray={`0,${cellSize}`}
        stroke-linecap="round"
      ></line>
    </pattern>
  </defs>
  <title>A Dot Grid</title>
  <rect
    x="0"
    y="0"
    width={dotDocWidth}
    height={dotDocHeight}
    fill="url(#horizontal-dotted-line)"
  ></rect>
</svg>

And this is what that looks like:

See the Pen by .

Conclusion

This brings us to the end of our little introductory journey into SVG. As you can see, coding SVG by hand is not as scary as it seems. If you break it down into the basic elements, it becomes quite like any other coding task:

  • We analyze the problem,
  • Break it down into smaller parts,
  • Examine each coordinate and its mathematical breakdown,
  • And then put it all together.

I hope that this article has given you a starting point into the wonderful world of coded images and that it gives you the motivation to delve deeper into the specs and try drawing some yourself.

Creating Custom Lottie Animations With SVGator

This article is a sponsored by

SVGator has gone through a series of updates since our last article, which was published in 2021, when it was already considered to be the most advanced web-based tool for vector animation. The first step toward more versatile software came with the mobile export feature that made it possible to implement the animations in iOS and Android applications.

The animation tool continued its upgrade with a series of new export options: video formats including MP4, AVI, MKV, MOV, and WebM, as well as image formats such as GIF, Animated PNG, WebP, and image sequence. By covering a larger area of users’ needs, the app now enables anyone to create animated stickers, social media, and newsletter animations, video assets, and many more types of visual content on demand.

The goal of becoming a “one tool for all” still lacked the last piece of the puzzle, namely full support for Lottie files. Lottie, just like SVG, is a vector-based format, but it has even better comprehensive multi-platform support, a fact that makes it super popular among developers and design professionals. It is built for use across various platforms, enabling smooth integration into both web and mobile applications. Its file size is minimal, it is infinitely scalable, and developers find it straightforward to implement once they get familiar with the format. Lottie can incorporate raster graphics and also supports interactivity.

SVGator’s latest version has everything you need for your various applications without the need for any third-party apps or plug-ins.

Note: You can test all of SVGator’s functionalities free of charge before committing to the Pro plan. However, you can export up to three watermarked files, with videos and GIFs limited to basic quality.

In this article, we will follow a creation process made of these steps:

  • Importing an existent Lottie JSON and making some minor adjustments;
  • Importing new animated assets created with SVGator (using the library);
  • Creating and animating new elements from scratch;
  • Exporting the Lottie animation.

Getting Started With SVGator

The sign-up process is simple, fast, and straightforward, and no credit card is required. Sign up either with Google or Facebook or, alternatively, by providing your name, email address, and password. Start a project either with a Lottie animation or a static SVG. If you don’t have an existing file, you can design and animate everything starting from a blank canvas.

Now that you’ve created your account, let’s dive right into the fun part. Here’s a preview of how your animation is going to look by the time you’re done following this guide. Neat, right?

()

Create A New Project

After logging in and clicking on the New Project option, you will be taken to the New Project Panel, where you can choose between starting from a blank project or uploading a file. Let’s start this project with an existing Lottie JSON.

  1. Click on the Upload file button and navigate to the directory where you have saved your Lottie file.

  2. Select the “Fast response.json” file and click Open.

    Hit play in the editor, and the animation should look like this:

    ()

Note: Make sure to hit Save after each step to make sure you don’t lose any of your progress while working on this project alongside our guide.

Import An Animated Asset

In this step, you will learn how to use the Library to import new assets to your project. You can easily choose from a variety of ready-made SVGs stored in different categories, load new files from your computer (Lottie, static SVG, and images), or save animations from other SVGator projects and reuse them.

In this case, let’s use an animated message bubble previously created and saved to the Uploads section of the Library.

Learn how to create and save animated assets with this .

  1. Navigate to the left sidebar of the app and switch to the Library tab, then click the “+” icon to upload the message bubble asset that you downloaded earlier.
  2. After it is loaded in the uploads section, simply click on it to add it to your project.

    All the animated properties of the asset are now present in the timeline, and you can edit them if you want.

    Note: Make sure the playhead is at the second “0” before adding the animated asset. When adding an animated asset, it will always start animating from the point where the playhead is placed.

  3. Freely adjust its position and size as you wish.
  4. With the playhead at the second 0, click on the Animate button, then choose Position.

At this point, you should have the first Position keyframe automatically added at the second 0, and you are ready to start animating.

Animate The Message Bubble

  1. Start by dragging the playhead on the timeline at 0.2 seconds:

  2. Then, drag the message bubble up a few pixels. The second keyframe will appear in the timeline, marking the element’s new position, thus creating the 2 milliseconds animation.

    Note: You can hit Play at any moment to check how everything looks!

    Next, you can use the Scale animator to make the bubble disappear after the dots representing the typing are done animating by scaling it down to 0 for both the X and Y axes:

  3. With the message bubble still selected, drag the playhead at 2.2 seconds, click on Animate, and select Scale (or just press Shift + S on the keyboard) to set the first Scale keyframe, then drag the playhead at 2.5 seconds.
  4. Set the scale properties to 0 for both the X and Y axes (in the right side panel). The bubble won’t be visible anymore at this point.

    Note: To maintain the ratio while changing the scale values, make sure you have the Maintain proportions on (the link icon next to the scale inputs).

    To add an extra touch of interest to this scaling motion, add an easing function preset:

  5. First, jump back to the first Scale keyframe (you can also double-click the keyframe to jump the playhead right at it).
  6. Open the Easing Panel next to the time indicator and scroll down through the presets list, then select Ease in Back. Due to its bezier going out of the graph, this easing function will create a bounce-back effect for the scale animation.

    Note: You can adjust the bezier of a selected easing preset and create a new custom function, which will appear at the top of the list.

    Keep in mind that you need at least one keyframe selected if you intend to apply an easing. The easing function will apply from the selected keyframe toward the next keyframe at its right. Of course, you can apply a certain easing for multiple keyframes at once.

    To get a smoother transition when the message bubble disappears, add an Opacity animation of one millisecond at the end of the scaling:

  7. Choose Opacity from the animators’ list and set the first keyframe at 2.4 seconds, then drag the playhead at 2.5 seconds to match the ending keyframe from the scale animation above.
  8. From the Appearance panel, drag the Opacity slider all the way to the left, at 0%.

Create An Email Icon

For the concept behind this animation to be complete, let’s create (and later animate) a “new email” notification as a response to the character sending that message.

Once again, SVGator’s asset library comes in handy for this step:

  1. Go to the search bar from the Library and type in “mail,” then click on the mail asset from the results.
  2. Place it somewhere above the laptop. Edit the mail icon to better fit the style of the animation:

  3. Open the email group and select the rectangle from the back.
  4. Change its fill color to a dark purple.
  5. Round up the corners using the Radius slider.

  6. Make the element’s design minimal by deleting these two lines from the lower part of the envelope.

  7. Select the envelope seal flap, which is the Polyline element in the group, above the rectangle.
  8. Add a lighter purple for the fill, set the stroke to 2 px width, and also make it white.

    To make the animation even more interesting, create a notification alert in the top-right corner of the envelope:

  9. Use the Ellipse tool (O) from the toolbar on top and draw a circle in the top-right corner of the envelope.
  10. Choose a nice red color for the fill, and set the stroke to white with a 2 px width.

  11. Click on the “T” icon to select the Text tool.
  12. Click on the circle and type “1”.
  13. Set the color to white and click on the “B” icon to make it bold.

  14. Select both the red circle and the number, and group them: right-click, and hit Group.

    You can also hit Command or Ctrl + G on your keyboard. Double-click on the newly created group to rename it to “Notification.”

  15. Select both the notification group and email group below and create a new group, which you can name “new email.”

Animate The New Email Group

Let’s animate the new email popping out of the laptop right after the character has finished texting his message:

  1. With the “New email” group selected, click twice on the Move down icon from the header to place the group last.
    You can also press Command or Ctrl + arrow down on your keyboard.
  2. Drag the group behind the laptop (on the canvas) to hide it entirely, and also scale it down a little.
  3. With the playhead at 3 seconds, add the animators Scale and Position.
    You can also do that by pressing Shift + S and Shift + P on your keyboard.

  4. Drag the playhead at the second 3.3 on the timeline.
  5. Move the New Email group above the laptop and scale it up a bit.
  6. You can also bend the motion path line to create a curved trajectory for the position animation.

  7. Select the first keyframes at the second 3.
  8. Open the easing panel.
  9. And click on the Ease Out Cubic preset to add it to both keyframes.

Animate The Notification

Let’s animate the notification dot separately. We’ll make it pop in while the email group shows up.

  1. Select the Notification group.
  2. Create a scale-up animation for it with 0 for both the X and Y axes at 3.2 and 1 at 3.5 seconds.
  3. Select the first keyframe and, from the easing panel, choose Ease Out Back. This easing function will ensure the popping effect.

Add Expressiveness To The Character

Make the character smile while looking at the email that just popped out. For this, you need to animate the stroke offset of the mouth:

  1. Select the mouth path. You can use the Node tool to select it directly with one click.
  2. Drag the playhead at 3.5 seconds, which is the moment from where the smile will start.
  3. Select the last keyframe of the Stroke offset animator from the timeline and duplicate it at second 3.5, or you can also use Ctrl or Cmd + D for duplication.

  4. Drag the playhead at second 3.9.
  5. Go to the properties panel and set the Offset to 0. The stroke will now fill the path all the way, creating a stroke offset animation of 4 milliseconds.

Final Edits

You can still make all kinds of adjustments to your animation before exporting it. In this case, let’s change the color of the initial Lottie animation we used to start this project:

  1. Use the Node tool to select all the green paths that form the character’s arms and torso.
  2. Change the color as you desire.

Export Lottie

Once you’re done editing, you can export the animation by clicking on the top right Export button and selecting the Lottie format. Alternatively, you can press Command or Ctrl + E on your keyboard to jump directly to the export panel, from where you can still select the animation you want to export.

  1. Make sure the Lottie format is selected from the dropdown. In the export panel, you can set a name for the file you are about to export, choose the frame rate and animation speed, or set a background color.
  2. You can preview the Lottie animation with a Lottie player.
    Note: This step is recommended to make sure all animations are supported in the Lottie format by previewing it on a webpage using the Lottie player. The preview in the export panel isn’t an actual Lottie animation.
  3. Get back to the export panel and simply click Export to download the Lottie JSON.

Final Thoughts

Now that you’re done with your animation don’t forget that you have plenty of export options available besides Lottie. You can post the same project on social media in video format, export it as an SVG animation for the web, or turn it into a GIF sticker or any other type of visual you can think of. GIF animations can also be used in Figma presentations and prototypes as a high-fidelity preview of the production-ready Lottie file.

We hope you enjoyed this article and that it will inspire you to create amazing Lottie animations in your next project.

Below, you can find a few useful resources to continue your journey with SVG and SVGator:

  • Check out a series of short video tutorials to help you get started with SVGator.
  • It answers the most common questions about SVGator, its features, and membership plans.

How To Build Custom Data Visualizations Using Luzmo Flex

This article is a sponsored by

In this article, I’ll introduce you to , a new feature from the Luzmo team who have been working hard making developer tooling to flatten the on-ramp for analytics reporting and data visualization.

With Luzmo Flex, you can hook up a dataset and create beautifully crafted, fully customizable interactive charts that meet your reporting needs. They easily integrate and interact with other components of your web app, allowing you to move away from a traditional “dashboard” interface and build more bespoke data products.

While many charting libraries offer similar features, I often found it challenging to get the data into the right shape that the library needed. In this article, I’ll show you how you can build beautiful data visualizations using the Google Analytics API, and you won’t have to spend any time “massaging” the data!

What Is Luzmo Flex?

Well, it’s two things, really. First of all, Luzmo is a low-code platform for embedded analytics. You can create datasets from just about anything, connect them to APIs like Google Analytics or your PostgreSQL database, or even upload static data in a .csv file and start creating data visualizations with drag and drop.

Secondly, Luzmo Flex is their new React component that can be configured to create custom data visualizations. Everything from the way you query your data to the way you display it can be achieved through code using the .

What makes Luzmo Flex unique is that you can reuse the core functionalities of Luzmo’s low-code embedded analytics platform in your custom-coded components.

That means, besides creating ready-to-use datasets, you can set up functions like the following out-of-the-box:

  • Multi-tenant analytics: Showing different data or visualizations to different users of your app.
  • Localization: Displaying charts in multiple languages, currencies, and timezones without much custom development.
  • Interactivity: Set up event listeners to create complex interactivity between Luzmo’s viz items and any non-Luzmo components in your app.

What Can You Build With Luzmo Flex?

By combining these off-the-shelf functions with flexibility through code, Luzmo Flex makes a great solution for building bespoke data products that go beyond the limits of a traditional dashboard interface. Below are a few examples of what that could look like.

Report Builder

A custom report builder that lets users search and filter a dataset and render it out using a number of different charts.

Filter Panel

Enable powerful filtering using HTML Select inputs, which will update each chart shown on the page.

Wearables Dashboard

Or how about a sleep tracker hooked up to your phone to track all those important snoozes?

When to Consider Luzmo Flex vs Chart Libraries

When building data-intensive applications, using something like , a well-known React charting library, you’ll likely need to reformat the data to fit the required shape. For instance, if I request the top 3 page views from the last seven days for my site, , I would have to use the Google Analytics API using the following query.

import dotenv from 'dotenv';
import { BetaAnalyticsDataClient } from '@google-analytics/data';
dotenv.config();

const credentials = JSON.parse(
  Buffer.from(process.env.GOOGLE_APPLICATION_CREDENTIALS_BASE64, 'base64').toString('utf-8')
);

const analyticsDataClient = new BetaAnalyticsDataClient({
  credentials,
});

const [{ rows }] = await analyticsDataClient.runReport({
  property: properties/${process.env.GA4&#95;PROPERTY&#95;ID},
  dateRanges: [
    {
      startDate: '7daysAgo',
      endDate: 'today',
    },
  ],
  dimensions: [
    {
      name: 'fullPageUrl',
    },
    {
      name: 'pageTitle',
    },
  ],
  metrics: [
    {
      name: 'totalUsers',
    },
  ],
  limit: 3,
  metricAggregations: ['MAXIMUM'],
});

The response would look something like this:

[
  {
    "dimensionValues": [
      {
        "value": ",
        "oneValue": "value"
      },
      {
        "value": "Paul Scanlon | Home",
        "oneValue": "value"
      }
    ],
    "metricValues": [
      {
        "value": "61",
        "oneValue": "value"
      }
    ]
  },
  {
    "dimensionValues": [
      {
        "value": ",
        "oneValue": "value"
      },
      {
        "value": "Paul Scanlon | A set of: "Sign In With Google" Buttons Made With Tailwind",
        "oneValue": "value"
      }
    ],
    "metricValues": [
      {
        "value": "41",
        "oneValue": "value"
      }
    ]
  },
  {
    "dimensionValues": [
      {
        "value": ",
        "oneValue": "value"
      },
      {
        "value": "Paul Scanlon | What Is a Proxy Redirect?",
        "oneValue": "value"
      }
    ],
    "metricValues": [
      {
        "value": "23",
        "oneValue": "value"
      }
    ]
  }
]

To make that data work with Recharts, I’d need to reformat it so it conforms to the following data shape.

[
  {
    "name": "Paul Scanlon | Home",
    "value": 61
  },
  {
    "name": "Paul Scanlon | A set of: "Sign In With Google" Buttons Made With Tailwind",
    "value": 41
  },
  {
    "name": "Paul Scanlon | What Is a Proxy Redirect?",
    "value": 23
  }
]

To accomplish this, I’d need to use an to iterate over each item, destructure the relevant data and return a key-value pair for the name and value for each.

const data = response.rows.map((row) => {
  const { dimensionValues, metricValues } = row;

  const pageTitle = dimensionValues[1].value;
  const totalUsers = parseInt(metricValues[0].value);

  return {
    name: pageTitle,
    value: totalUsers,
  };
});

And naturally, if you’re reformatting data this way in your application, you’d also want to write unit tests to ensure the data is always formatted correctly to avoid breaking your application… and all of this before you even get on to creating your charts!

With Luzmo Flex, all of this goes away, leaving you more time to focus on which data to display and how best to display it.

The First Steps to Building Bespoke Data Products

Typically, when building user interfaces that display data insights, your first job will be to figure out how to query the data source. This can take many forms, from RESTful API requests to direct database queries or sometimes reading from static files. Your next job will be figuring out when and how often these requests need to occur.

  • For data that rarely changes: Perhaps a query in the build step will work.
  • For data that changes regularly: A server-side request on page load.
  • For ever-changing data: A client-side request that polls an API on an interval.

Each will likely inform your application’s architecture, and there’s no single solution to this. Your last job, as mentioned, will be wrangling the responses, reformatting the data, and displaying it in the UI.

Below, I’ll show you how to do this using Luzmo Flex by using a simple example product.

What We’re Building: Custom Data Visualizations As Code

Here’s a screenshot of a simple data product I’ve built that displays three different charts for different reporting dimensions exposed by the Google Analytics API for page views for my site, , from the last seven days.

You can find all the code used in this article on the following link:

Getting Started With Luzmo

Before we get going, hop over to Luzmo and . You might also like to have a read of one of the getting started guides listed below. In this article, I’ll be using the Next.js starter.

Creating a Google Analytics Dataset

To create data visualization, you’ll first need data! To achieve this using Luzmo, head over to the dashboard, select Datasets from the navigation, and select GA4 Google Analytics. Follow the steps shown in the UI to connect Luzmo with your Google Analytics account.

With the setup complete, you can now select which reporting dimensions to add to your dataset. To follow along with this article, select Custom selection.

Lastly, select the following using the search input. Device Category, Page Title, Date, and Total users, then click Import when you’re ready.

You now have all the data required to build the Google Analytics dashboard. You can access the dataset ID from the URL address bar in your browser. You’ll need this in a later step.

If you’ve followed along from either of the first two getting started guides, you’ll have your API Key, API Token, App server, and API host environment variables set up and saved in a .env file.

Install Dependencies

If you’ve cloned one of the starter repositories, run the following to install the required dependencies.

npm install

Next, install the Luzmo React Embed dependency which exports the LuzmoVizItemComponent.

npm install  @luzmo/react-embed@latest

Now, find page.tsx located in the src/app directory, and add your dataset id as shown below.

Add the access object from the destructured response and pass access.datasets[0].id onto the LuzmoClientComponent component using a prop named datasetId.

// src/app/page.tsx


+ import dynamic from 'next/dynamic';

import Luzmo from '@luzmo/nodejs-sdk';
- import LuzmoClientComponent from './components/luzmo-client-component';
+ const LuzmoClientComponent = dynamic(() => import('./components/luzmo-client-component'), {
  ssr: false,
});


const client = new Luzmo({
  api_key: process.env.LUZMO_API_KEY!,
  api_token: process.env.LUZMO_API_TOKEN!,
  host: process.env.NEXT_PUBLIC_LUZMO_API_HOST!,
});

export default async function Home() {
  const response = await client.create('authorization', {
    type: 'embed',
    username: 'user id',
    name: 'first name last name',
    email: ',
    access: {
      datasets: [
        {
-          id: '<dataset_id>',
+          id: '42b43db3-24b2-45e7-98c5-3fcdef20b1a3',
          rights: 'use',
        },
      ],
    },
  });

-  const { id, token } = response;
+  const { id, token, access } = response;

-  return <LuzmoClientComponent authKey={id} authToken={token} />;
+  return <LuzmoClientComponent authKey={id} authToken={token} datasetId={access.datasets[0].id} />;
}

And lastly, find luzmo-client-component.tsx located in src/app/components. This is where you’ll be creating your charts.

Building a Donut Chart

The first chart you’ll create is a Donut chart that shows the various devices used by visitors to your site.

Add the following code to luzmo-client-component.tsx component.

// src/app/component/luzmo-client-component.tsx

'use client';

+ import { LuzmoVizItemComponent } from '@luzmo/react-embed';

interface Props {
  authKey: string;
  authToken: string;
+  datasetId: string;
}

- export default function LuzmoClientComponent({ authKey, authToken}: Props) {
+ export default function LuzmoClientComponent({ authKey, authToken, datasetId }: Props) {

+  const date = new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(); // creates a date 7 days ago

  console.log({ authKey, authToken });

  return (
    <section>
+    <div className='w-1/2 h-80'>
+      <LuzmoVizItemComponent
+        appServer={process.env.NEXT_PUBLIC_LUZMO_APP_SERVER}
+        apiHost={process.env.NEXT_PUBLIC_LUZMO_API_HOST}
+        authKey={authKey}
+        authToken={authToken}
+        type='donut-chart'
+        options={{
+          title: {
+            en: Devices from last 7 days,
+          },
+          display: {
+            title: true,
+          },
+          mode: 'donut',
+          legend: {
+            position: 'bottom',
+          },
+        }}
+        slots={[
+          {
+            name: 'measure',
+            content: [
+              {
+                label: {
+                  en: 'Total users',
+                },
+                column: '<column id>', // Total users
+                set: datasetId,
+                type: 'numeric',
+                format: '.4f',
+              },
+            ],
+          },
+          {
+            name: 'category',
+            content: [
+              {
+                label: {
+                  en: 'Device category',
+                },
+                column: '<column id>', // Device category
+                set: datasetId,
+                type: 'hierarchy',
+              },
+            ],
+          },
+        ]}
+        filters={[
+          {
+            condition: 'or',
+            filters: [
+              {
+                expression: '? >= ?',
+                parameters: [
+                  {
+                    column_id: '<column id>', // Date
+                    dataset_id: datasetId,
+                  },
+                  date,
+                ],
+              },
+            ],
+          },
+        ]}
+      />
+    <div/>
    </section>
  );
}

There’s quite a lot going on in the above code snippet, and I will explain it all in due course, but first, I’ll need to cover a particularly tricky part of the configuration.

Column IDs

You’ll notice the filters parameters, measure, and category content all require a column id.

In the filters parameters, the key is named column_id, and in the measure and category, the key is named column. Both of these are actually the column IDs from the dataset. And here’s how you can find them.

Back in the Luzmo dashboard, click into your dataset and look for the “more dots” next to each column heading. From the menu, select Copy column id. Add each column ID to the keys in the configuration objects.

In my example, I’m using the Total users for the measure, the Device category for the category, and the Date for the filter.

If you’ve added the column IDs correctly, you should be able to see a rendered chart on your screen!

… and as promised, here’s a breakdown of the configuration.

Initial Props Donut chart

The first part is fairly straightforward. appServer and authKey are the environment variables you saved to your .env file, and authKey and authToken are destructured from the authorization request and passed into this component via props.

The type prop determines which type of chart to render. In my example, I’m using donut-chart, but you could choose from one of the many options available, area-chart, bar-chart, bubble-chart, box-plot, and many more. You can see all the available options in the under .

<LuzmoVizItemComponent
  appServer={process.env.NEXT_PUBLIC_LUZMO_APP_SERVER}
  apiHost={process.env.NEXT_PUBLIC_LUZMO_API_HOST}
  authKey={authKey}
  authToken={authToken}
  type='donut-chart'

The one thing I should point out is my use of Tailwind classes: w-1/2 (width: 50%) and h-80 (height: 20rem). The LuzmoVizItemComponent ships with height 100%, so you’ll need to wrap the component with an element that has an actual height, or you won’t be able to see the chart on the page as it could be 100% of the height of an element with no height.

Donut Chart Options

The options object is where you can customize the appearance of your chart. It accepts many configuration options, among which:

  • A title for the chart that accepts a with corresponding text to display.
  • A display title value to determine if the title is shown or not.
  • A mode to determine if the chart is to be of type donut or pie chart.
  • A legend option to determine where the legend can be positioned.

All the available configuration options can be seen in the .

options={{
  title: {
    en: `Devices from last 7 days`,
  },
  display: {
    title: true,
  },
  mode: 'donut',
  legend: {
    position: 'bottom',
  },
}}

Donut Chart Slots

Slots are where you can configure which column from your dataset to use for the category and measure.

Slots can contain multiple measures, useful for displaying two columns of data per chart, but if more than two are used, one will become the measure.

Each measure contains a content array. The content array, among many other configurations, can include the following:

  • A label and locale,
  • The column id from the dataset,
  • The datasetId,
  • The type of data you’re displaying,
  • A format for the data.

The format used here is Python syntax for ; it’s similar to JavaScript’s .toFixed() method, e.g number.toFixed(4).

The hierarchy type is ​​the Luzmo standard data type. Any text column is considered as an data type.

You can read more in the about available configuration options for .

slots={[
  {
    name: 'measure',
    content: [
      {
        label: {
          en: 'Total users',
        },
        column: '<column id>', // Total users
        set: datasetId,
        type: 'numeric',
        format: '.4f',
      },
    ],
  },
  {
    name: 'category',
    content: [
      {
        label: {
          en: 'Device category',
        },
        column: '<column id>', // Device category
        set: datasetId,
        type: 'hierarchy',
      },
    ],
  },
]}

Donut Chart Filters

The filters object is where you can apply conditions that will determine which data will be shown. In my example, I only want to show data from the last seven days. To accomplish this, I first create the date variable:

const date = new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();

This would produce an ISO date string, e.g., 2024-08-21T14:25:40.088Z, which I can use with the filter. The filter uses Luzmo’s , to determine if the date for each row of the data is greater than or equal to the date variable. You can read more about Filter Expressions in Luzmo’s .

filters={[
  {
    condition: 'or',
    filters: [
      {
        expression: '? >= ?',
        parameters: [
          {
            column_id: '<column id>', // Date
            dataset_id: datasetId,
          },
          date,
        ],
      },
    ],
  },
]}

Building a Line Chart

The second chart you’ll be creating is a Line chart that displays the number of page views on each date from the last seven days from folks who visit your site.

Initial Props Line Chart

As with the Donut chart, the initial props are pretty much the same, but the type has been changed to line-chart.

<LuzmoVizItemComponent
  appServer={process.env.NEXT_PUBLIC_LUZMO_APP_SERVER}
  apiHost={process.env.NEXT_PUBLIC_LUZMO_API_HOST}
  authKey={authKey}
  authToken={authToken}
  type='line-chart'

Line Chart Options

The options for the Line chart are as follows, and the mode has been changed to line-chart.

options={{
  title: {
    en: `Site visits from last 7 days`,
  },
  display: {
    title: true,
  },
  mode: 'grouped',
}}

Line Chart Slots

The slots object is almost the same as before with the Donut chart, but for the Line chart, I’m using the date column from the dataset instead of the device category, and instead of category, I’m using the x-axis slot type. To ensure I’m formatting the data correctly (by day), I’ve used level 5. You can read .

slots={[
  {
    name: 'measure',
    content: [
      {
        label: {
          en: 'Total users',
        },
        column: '<column id>', // Total users
        set: datasetId,
        type: 'numeric',
        format: '.4f',
      },
    ],
  },
  {
    name: 'x-axis',
    content: [
      {
        label: {
          en: 'Date',
        },
        column: '<column id>', // Date
        set: datasetId,
        type: 'datetime',
        level: 5,
      },
    ],
  },
]}

Line Chart Filters

I’ve used the same filters as I used in the Donut chart.

Building a Bar Chart

The last chart you’ll be creating is a Bar chart that displays the number of page views for the top ten most viewed pages on your site.

Initial Props Bar Chart

As with the Donut and Line chart, the initial props are pretty much the same, but the type has been changed to bar-chart.

<LuzmoVizItemComponent
  className='w-full h-80'
  appServer={process.env.NEXT_PUBLIC_LUZMO_APP_SERVER}
  apiHost={process.env.NEXT_PUBLIC_LUZMO_API_HOST}
  authKey={authKey}
  authToken={authToken}
  type='bar-chart'

Bar Chart Options

The options for the Bar chart are a little more involved. I’ve included some styling options for the border-radii of the bars, limited the number of results to 10, and sorted the data by the highest page view count first using the sort by measure and direction options.

options={{
  title: {
    en: `Page views from last 7 days`,
  },
  display: {
    title: true,
  },
  mode: 'grouped',
  bars: {
    roundedCorners: 5,
  },
  limit: {
    number: 10,
  },
  sort: {
    by: 'measure',
    direction: 'desc',
  },
}}

Line Chart Slots

As with the Line chart, I’ve used an axis for one of the columns from the dataset. In this case, it’s the y-axis which displays the page title.

slots={[
  {
    name: 'measure',
    content: [
      {
        label: {
          en: 'Total users',
        },
        column: '<column id>', // Total users
        set: datasetId,
        type: 'numeric',
        format: '.4f',
      },
    ],
  },
  {
    name: 'y-axis',
    content: [
      {
        label: {
          en: 'Page title',
        },
        column: '<column id>', // Page title
        set: datasetId,
        type: 'hierarchy',
      },
    ],
  },
]}

Bar Chart Filters

I’ve used the same filters as I used in the Donut and Line chart.

What’s Next

As you can see, there are plenty of types of charts and customization options. Because this is just an “ordinary” React component, you can very easily make it configurable by an end user by allowing options to be set and unset using HTML input elements, checkbox, select, date, and so on.

But for me, the real power behind this is not having to mutate data!

This is particularly pertinent when displaying multiple charts with different reporting dimensions. Typically, this would require each to have their own utility function or reformatting method. That said, setting column IDs and dataset IDs is a little fiddly, but once you have the component hooked up to the dataset, you can configure and reconfigure as much as you like, all without having to rewrite data formatting functions.

If you’re interested in bringing data to life in your application and want to get it done without the usual headaches, to learn more!

Why Anticipatory Design Isn’t Working For Businesses

Consider the early days of the internet, when websites like NBC News and Amazon cluttered their pages with flashing banners and labyrinthine menus. In the early 2000s, Steve Krug’s book Don’t Make Me Think arrived like a lighthouse in a storm, advocating for simplicity and user-centric design.

Today’s digital world is flooded with choices, information, and data, which is both exciting and overwhelming. Unlike Krug’s time,

Today, the problem isn’t interaction complexity but opacity. AI-powered solutions often lack transparency and explainability, raising concerns about user trust and accountability.

The era of click-and-command is fading, giving way to a more seamless and intelligent relationship between humans and machines.

Expanding on Krug’s Call for Clarity: The Pillars of Anticipatory Design

Krug’s emphasis on clarity in design is more relevant than ever. In anticipatory design, clarity is not just about simplicity or ease of use — it’s about transparency and accountability. These two pillars are crucial but often missing as businesses navigate this new paradigm. Users today find themselves in a digital landscape that is not only confusing but increasingly intrusive. AI predicts their desires based on past behavior but rarely explains how these predictions are made, leading to growing mistrust.

Transparency is the foundation of clarity. It involves openly communicating how AI-driven decisions are made, what data is being collected, and how it is being used to anticipate needs. By demystifying these processes, designers can alleviate user concerns about privacy and control, thereby building trust.

Accountability complements transparency by ensuring that anticipatory systems are designed with ethical considerations in mind. This means creating mechanisms for users to understand, question, and override automated decisions if needed. When users feel that the system is accountable to them, their trust in the technology — and the brand — deepens.

What Makes a Service Anticipatory?

Image AI like a waiter at a restaurant. Without AI, they wait for you to interact with them and place your order. But with anticipatory design powered by AI and ML, the waiter can analyze your past orders (historical data) and current behavior (contextual data) — perhaps, by noticing you always start with a glass of sparkling water.

This proactive approach has evolved since the late 1990s, with early examples like Amazon’s recommendation engine and TiVo’s predictive recording. These pioneering services demonstrated the potential of predictive analytics and ML to create personalized, seamless user experiences.

Amazon’s Recommendation Engine (Late 1990s)

Amazon was a pioneer in using data to predict and suggest products to customers, setting the standard for personalized experiences in e-commerce.

TiVo (1999)

TiVo’s ability to learn users’ viewing habits and automatically record shows marked an early step toward predictive, personalized entertainment.

Netflix’s Recommendation System (2006)

Netflix began offering personalized movie recommendations based on user ratings and viewing history in 2006. It helped popularize the idea of anticipatory design in the digital entertainment space.

How Businesses Can Achieve Anticipatory Design

Designing for anticipation is designing for a future that is not here yet but has already started moving toward us.

Designing for anticipation involves more than reacting to current trends; it requires businesses to plan strategically for future user needs. Two critical concepts in this process are forecasting and backcasting.

  • Forecasting analyzes past trends and data to predict future outcomes, helping businesses anticipate user needs.
  • Backcasting starts with a desired future outcome and works backward to identify the steps needed to achieve that goal.

Think of it like planning a dream vacation. Forecasting would involve looking at your past trips to guess where you might go next. But backcasting lets you pick your ideal destination first, then plan the perfect itinerary to get you there.

Forecasting: A Core Concept for Future-Oriented Design

This method helps in planning and decision-making based on probable future scenarios. Consider Netflix, which uses forecasting to analyze viewers’ past viewing habits and predict what they might want to watch next. By leveraging data from millions of users, Netflix can anticipate individual preferences and serve personalized recommendations that keep users engaged and satisfied.

Backcasting: Planning From the Desired Future

Backcasting takes a different approach. Instead of using data to predict the future, it starts with defining a desired future outcome — a clear user intent. The process then works backward to identify the steps needed to achieve that goal. This goal-oriented approach crafts an experience that actively guides users toward their desired future state.

For instance, a financial planning app might start with a user’s long-term financial goal, such as saving for retirement, and then design an experience that guides the user through each step necessary to reach that goal, from budgeting tips to investment recommendations.

Integrating Forecasting and Backcasting In Anticipatory Design

The true power of anticipatory design emerges when businesses efficiently integrate both forecasting and backcasting into their design processes.

For example, Tesla’s approach to electric vehicles exemplifies this integration. By forecasting market trends and user preferences, Tesla can introduce features that appeal to users today. Simultaneously, by backcasting from a vision of a sustainable future, Tesla designs its vehicles and infrastructure to guide society toward a world where electric cars are the norm and carbon emissions are significantly reduced.

Over-Promising and Under-Delivering: The Pitfalls of Anticipatory Design

As businesses increasingly adopt anticipatory design, the integration of forecasting and backcasting becomes essential. Forecasting allows businesses to predict and respond to immediate user needs, while backcasting ensures these responses align with long-term goals. Despite its potential, anticipatory design often fails in execution, leaving few examples of success.

Over the past decade, I’ve observed and documented the rise and fall of several ambitious anticipatory design ventures. Among them, three — Digit, LifeBEAM Vi Sense Headphones, and Mint — highlight the challenges of this approach.

Digit: Struggling with Contextual Understanding

Digit aimed to simplify personal finance with algorithms that automatically saved money based on user spending. However, the service often missed the mark, lacking the contextual awareness necessary to accurately assess users’ real-time financial situations. This led to unexpected withdrawals, frustrating users, especially those living paycheck to paycheck. The result was a breakdown in trust, with the service feeling more intrusive than supportive.

LifeBEAM Vi Sense Headphones: Complexity and User Experience Challenges

LifeBEAM Vi Sense Headphones was marketed as an AI-driven fitness coach, promising personalized guidance during workouts. In practice, the AI struggled to deliver tailored coaching, offering generic and unresponsive advice. As a result, users found the experience difficult to navigate, ultimately limiting the product’s appeal and effectiveness. This disconnection between the promised personalized experience and the actual user experience left many disappointed.

Mint: Misalignment with User Goals

Mint aimed to empower users to manage their finances by providing automated budgeting tools and financial advice. While the service had the potential to anticipate user needs, users often found that the suggestions were not tailored to their unique financial situations, resulting in generic advice that did not align with their personal goals.

The lack of personalized, actionable steps led to a mismatch between user expectations and service delivery. This misalignment caused some users to disengage, feeling that Mint was not fully attuned to their unique financial journeys.

The Risks of Over-promising and Under-Delivering

The stories of Digit, LifeBEAM Vi Sense, and Mint underscore a common pitfall: over-promising and under-delivering. These services focused too much on predictive power and not enough on user experience. When anticipatory systems fail to consider individual nuances, they breed frustration rather than satisfaction, highlighting the importance of aligning design with human experience.

Digit’s approach to automated savings, for instance, became problematic when users found its decisions opaque and unpredictable. Similarly, LifeBEAM’s Vi Sense headphones struggled to meet diverse user needs, while Mint’s rigid tools failed to offer the personalized insights users expected. These examples illustrate the delicate balance anticipatory design must strike between proactive assistance and user control.

Failure to Evolve with User Needs

Many anticipatory services rely heavily on data-driven forecasting, but predictions can fall short without understanding the broader user context. Mint initially provided value with basic budgeting tools but failed to evolve with users’ growing needs for more sophisticated financial advice. Digit, too, struggled to adapt to different financial habits, leading to dissatisfaction and limited success.

Complexity and Usability Issues

Balancing the complexity of predictive systems with usability and transparency is a key challenge in anticipatory design.

When systems become overly complex, as seen with LifeBEAM Vi Sense headphones, users may find them difficult to navigate or control, compromising trust and engagement. Mint’s generic recommendations, born from a failure to align immediate user needs with long-term goals, further illustrate the risks of complexity without clarity.

Privacy and Trust Issues

Trust is critical in anticipatory design, particularly in services handling sensitive data like finance or health. Digit and Mint both encountered trust issues as users grew skeptical of how decisions were made and whether these services truly had their best interests in mind. Without clear communication and control, even the most sophisticated systems risk alienating users.

Inadequate Handling of Edge Cases and Unpredictable Scenarios

While forecasting and backcasting work well for common scenarios, they can struggle with edge cases or unpredictable user behaviors. If an anticipatory service can’t handle these effectively, it risks providing a poor user experience and, in the worst-case scenario, harming the user. Anticipatory systems must be prepared to handle edge cases and unpredictable scenarios.

LifeBEAM Vi Sense headphones struggled when users deviated from expected fitness routines, offering a one-size-fits-all experience that failed to adapt to individual needs. This highlights the importance of allowing users control, even when a system proactively assists them.

Designing for Anticipatory Experiences

Anticipatory design should empower users to achieve their goals, not just automate tasks.

We can follow a layered approach to plan a service that can evolve according to user actions and explicit ever-evolving intent.

But how do we design for intent without misaligning anticipation and user control or mismatching user expectations and service delivery?

At the core of this approach is intent — the primary purpose or goal that the design must achieve. Surrounding this are workflows, which represent the structured tasks to achieve the intent. Finally, algorithms analyze user data and optimize these workflows.

For instance, Thrive (see the image below), a digital wellness platform, aligns algorithms and workflows with the core intent of improving well-being. By anticipating user needs and offering personalized programs, Thrive helps users achieve sustained behavior change.

It perfectly exemplifies the three-layered concentric representation for achieving behavior change through anticipatory design:

1. Innermost layer: Intent

Improve overall well-being: Thrive’s core intent is to help users achieve a healthier and more fulfilling life. This encompasses aspects like managing stress, improving sleep quality, and boosting energy levels.

2. Middle layer: Workflows

Personalized programs and support: Thrive uses user data (sleep patterns, activity levels, mood) to create programs tailored to their specific needs and goals. These programs involve various workflows, such as:

  • Guided meditations and breathing exercises to manage stress and anxiety.
  • Personalized sleep routines aimed at improving sleep quality.
  • Educational content and coaching tips to promote healthy habits and lifestyle changes.

3. Outermost layer: Algorithms

Data analysis and personalized recommendations: Thrive utilizes algorithms to analyze user data and generate actionable insights. These algorithms perform tasks like the following:

  • Identify patterns in sleep, activity, and mood to understand user challenges.
  • Predict user behavior to recommend interventions that address potential issues.
  • Optimize program recommendations based on user progress and data analysis.

By aligning algorithms and workflows with the core intent of improving well-being, Thrive provides a personalized and proactive approach to behavior change. Here’s how it benefits users:

  • Sustained behavior change: Personalized programs and ongoing support empower users to develop healthy habits for the long term.
  • Data-driven insights: User data analysis helps users gain valuable insights into their well-being and identify areas for improvement.
  • Proactive support: Anticipates potential issues and recommends interventions before problems arise.

The Future of Anticipatory Design: Combining Anticipation with Foresight

Anticipatory design is inherently future-oriented, making it both appealing and challenging. To succeed, businesses must combine anticipation — predicting future needs — with foresight, a systematic approach to analyzing and preparing for future changes.

Foresight involves considering alternative future scenarios and making informed decisions to navigate toward desired outcomes. For example, Digit and Mint struggled because they didn’t adequately handle edge cases or unpredictable scenarios, a failure in their foresight strategy (see an image below).

As mentioned, while forecasting and backcasting work well for common scenarios, they can struggle with edge cases or unpredictable user behaviors. Under anticipatory design, if we demote foresight for a second plan, the business will fail to account for and prepare for emerging trends and disruptive changes. Strategic foresight helps companies to prepare for the future and develop strategies to address possible challenges and opportunities.

The Foresight process generally involves interrelated activities, including data research, trend analysis, planning scenarios, and impact assessment. The ultimate goal is to gain a broader and deeper understanding of the future to make more informed and strategic decisions in the design process and foresee possible frictions and pitfalls in the user experience.

Actionable Insights for Designer

  • Enhance contextual awareness
    Help data scientists or engineers to ensure that the anticipatory systems can understand and respond to the full context of user needs, not just historical data. Plan for pitfalls so you can design safety measures where the user can control the system.
  • Maintain user control
    Provide users with options to customize or override automated decisions, ensuring they feel in control of their experiences.
  • Align short-term predictions with long-term goals
    Use forecasting and backcasting to create a balanced approach that meets immediate needs while guiding users toward their long-term objectives.

Proposing an Anticipatory Design Framework

Predicting the future is no easy task. However, design can borrow foresight techniques to imagine, anticipate, and shape a future where technology seamlessly integrates with users evolving needs. To effectively implement anticipatory design, it’s essential to balance human control with AI automation. Here’s a 3-step approach to integrate future thinking into your workflow:

  1. Anticipate Directions of Change
    Identify major trends shaping the future.
  2. Imagine Alternative Scenarios
    Explore potential futures to guide impactful design decisions.
  3. Shape Our Choices
    Leverage these scenarios to align design with user needs and long-term goals.

This proposed framework (see an image above) aims to integrate forecasting and backcasting while emphasizing user intent, transparency, and continuous improvement, ensuring that businesses create experiences that are both predictive and deeply aligned with user needs.

Step 1: Anticipate Directions of Change

Objective: Identify the major trends and forces shaping the future landscape.

Components:

1. Understand the User’s Intent

  • User Research: Conduct in-depth user research through interviews, surveys, and observations to uncover user goals, motivations, pain points, and long-term aspirations or Jobs-to-be-Done (JTBD). This foundational step helps clearly define the user’s intent.
  • Persona Development: Develop detailed user personas that represent the target audience, including their long-term goals and desired outcomes. Prioritize understanding how the service can adapt in real-time to changing user needs, offering recommendations, or taking actions aligned with the persona’s current context.

2. Forecasting: Predicting Near-Term User Needs

  • Data Collection and Analysis: Collaborate closely with data scientists and data engineers to analyze historical data (past interactions), user behavior, and external factors. This collaboration ensures that predictive analytics enhance overall user experience, allowing designers to better understand the implications of data on user behaviors.
  • Predictive Modeling: Implement continuous learning algorithms that refine predictions over time. Regularly assess how these models evolve, adapting to users’ changing needs and circumstances.
  • Explore the Delphi Method: This is a structured communication technique that gathers expert opinions to reach a consensus on future developments. It’s particularly useful for exploring complex issues with uncertain outcomes. Use the Delphi Method to gather insights from industry experts, user researchers, and stakeholders about future user needs and the best strategies to meet those needs. The consensus achieved can help in clearly defining the long-term goals and desired outcomes.

Activities:

  • Conduct interviews and workshops with experts using the Delphi Method to validate key trends.
  • Analyze data and trends to forecast future directions.

Step 2: Imagine Alternative Scenarios

Objective: Explore a range of potential futures based on these changing directions.

Components:

1. Scenario Planning

  • Scenario Development: It involves creating detailed, plausible future scenarios based on various external factors, such as technological advancements, social trends, and economic changes. Develop multiple future scenarios that represent different possible user contexts and their impact on their needs.
  • Scenario Analysis: From these scenarios, you can outline the long-term goals that users might have in each scenario and design services that anticipate and address these needs. Assess how these scenarios impact user needs and experiences.

2. Backcasting: Designing from the Desired Future

  • Define Desired Outcomes: Clearly outline the long-term goals or future states that users aim to achieve. Use backcasting to reduce cognitive load by designing a service that anticipates future needs, streamlining user interactions, and minimizing decision-making efforts.
    • Use Visioning Planning: This is a creative process that involves imagining the ideal future state you want to achieve. It helps in setting clear, long-term goals by focusing on the desired outcomes rather than current constraints. Facilitate workshops or brainstorming sessions with stakeholders to co-create a vision of the future. Define what success looks like from the user’s perspective and use this vision to guide the backcasting process.
  • Identify Steps to Reach Goals: Reverse-engineer the user journey by starting from the desired future state and working backward. Identify the necessary steps and milestones and ensure these are communicated transparently to users, allowing them control over their experience.
  • Create Roadmaps: Develop detailed roadmaps that outline the sequence of actions needed to transition from the current state to the desired future state. These roadmaps should anticipate obstacles, respect privacy, and avoid manipulative behaviors, empowering users rather than overwhelming them.

Activities:

  • Develop and analyze alternative scenarios to explore various potential futures.
  • Use backcasting to create actionable roadmaps from these scenarios, ensuring they align with long-term goals.

Step 3: Shape Our Choices

Objective: Leverage these scenarios to spark new ideas and guide impactful design decisions.

Components:

1. Integrate into the Human-Centered Design Process

  • Iterative Design with Forecasting and Backcasting: Embed insights from forecasting and backcasting into every stage of the design process. Use these insights to inform user research, prototype development, and usability testing, ensuring that solutions address both predicted future needs and desired outcomes. Continuously refine designs based on user feedback.
  • Agile Methodologies: Adopt agile development practices to remain flexible and responsive. Ensure that the service continuously learns from user interactions and feedback, refining its predictions and improving its ability to anticipate needs.

2. Implement and Monitor: Ensuring Ongoing Relevance

  • User Feedback Loops: Establish continuous feedback mechanisms to refine predictive models and workflows. Use this feedback to adjust forecasts and backcasted plans as necessary, keeping the design aligned with evolving user expectations.
  • Automation Tools: Collaborate with data scientists and engineers to deploy automation tools that execute workflows and monitor progress toward goals. These tools should adapt based on new data, evolving alongside user behavior and emerging trends.
  • Performance Metrics: Define key performance indicators (KPIs) to measure the effectiveness, accuracy, and quality of the anticipatory experience. Regularly review these metrics to ensure that the system remains aligned with intended outcomes.
  • Continuous Improvement: Maintain a cycle of continuous improvement where the system learns from each interaction, refining its predictions and recommendations over time to stay relevant and useful.
    • Use Trend Analysis: This involves identifying and analyzing patterns in data over time to predict future developments. This method helps you understand the direction in which user behaviors, technologies, and market conditions are heading. Use trend analysis to identify emerging trends that could influence user needs in the future. This will inform the desired outcomes by highlighting what users might require or expect from a service as these trends evolve.

Activities:

  • Implement design solutions based on scenario insights and iterate based on user feedback.
  • Regularly review and adjust designs using performance metrics and continuous improvement practices.

Conclusion: Navigating the Future of Anticipatory Design

Anticipatory design holds immense potential to revolutionize user experiences by predicting and fulfilling needs before they are even articulated. However, as seen in the examples discussed, the gap between expectation and execution can lead to user dissatisfaction and erode trust.

To navigate the future of anticipatory design successfully, businesses must prioritize transparency, accountability, and user empowerment. By enhancing contextual awareness, maintaining user control, and aligning short-term predictions with long-term goals, companies can create experiences that are not only innovative but also deeply resonant with their users’ needs.

Moreover, combining anticipation with foresight allows businesses to prepare for a range of future scenarios, ensuring that their designs remain relevant and effective even as circumstances change. The proposed 3-step framework — anticipating directions of change, imagining alternative scenarios, and shaping our choices — provides a practical roadmap for integrating these principles into the design process.

As we move forward, the challenge will be to balance the power of AI with the human need for clarity, control, and trust. By doing so, businesses can fulfill the promise of anticipatory design, creating products and services that are not only efficient and personalized but also ethical and user-centric.

In the end,

The success of anticipatory design will depend on its ability to enhance, rather than replace, the human experience.

It is a tool to empower users, not to dictate their choices. When done right, anticipatory design can lead to a future where technology seamlessly integrates with our lives, making everyday experiences simpler, more intuitive, and ultimately more satisfying.

How To Create A Weekly Google Analytics Report That Posts To Slack

Google Analytics is great, but not everyone in your organization will be granted access. In many places I’ve worked, it was on a kind of “need to know” basis.

In this article, I’m gonna flip that on its head and show you how I wrote a that queries Google Analytics, generates a top ten list of the most frequently viewed pages on from the last seven days and compares them to the previous seven days to tell me which pages have increased in views, which pages have decreased in views, which pages have stayed the same and which pages are new to the list.

The report is then nicely formatted with icon indicators and posted to a public Slack channel every Friday at 10 AM.

Not only would this surfaced data be useful for folks who might need it, but it also provides an easy way to copy and paste or screenshot the report and add it to a slide for the weekly company/department meeting.

Here’s what the finished report looks like in Slack, and below, you’ll find a link to the GitHub Repository.

GitHub

To use this repository, follow the steps outlined in the README.

Prerequisites

To build this workflow, you’ll need admin access to your and Accounts and administrator privileges for GitHub Actions and Secrets for a GitHub repository.

Customizing the Report and Action

Naturally, all of the code can be changed to suit your requirements, and in the following sections, I’ll explain the areas you’ll likely want to take a look at.

Customizing the GitHub Action

The file name of the Action isn’t seen anywhere other than in the code/repo but naturally, change it to whatever you like, you won’t break anything.

The name and jobs: names detailed below are seen in the GitHub UI and Workflow logs.

The cron syntax determines when the Action will run. use and by changing the numbers you can determine when the Action runs.

You could also change the secrets variable names; just make sure you update them in your repository Settings.

# .github/workflows/weekly-analytics-report.yml

name: Weekly Analytics Report

on:
  schedule:
    - cron: '0 10 * * 5' # Runs every Friday at 10 AM UTC
  workflow_dispatch: # Allows manual triggering

jobs:
  analytics-report:
    runs-on: ubuntu-latest

    env:
      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      GA4_PROPERTY_ID: ${{ secrets.GA4_PROPERTY_ID }}
      GOOGLE_APPLICATION_CREDENTIALS_BASE64: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS_BASE64 }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'

      - name: Install dependencies
        run: npm install

      - name: Run the JavaScript script
        run: node src/services/weekly-analytics.js

Customizing the Google Analytics Report

The I’m using is set to pull the fullPageUrl and pageTitle for the totalUsers in the last seven days, and a for the previous seven days, and then aggregates the totals and limits the responses to 10.

You can use Google’s to construct your own query, then replace the requests.

// src/services/weekly-analytics.js#L75

const [thisWeek] = await analyticsDataClient.runReport({
  property: `properties/${process.env.GA4_PROPERTY_ID}`,
  dateRanges: [
    {
      startDate: '7daysAgo',
      endDate: 'today',
    },
  ],
  dimensions: [
    {
      name: 'fullPageUrl',
    },
    {
      name: 'pageTitle',
    },
  ],
  metrics: [
    {
      name: 'totalUsers',
    },
  ],
  limit: reportLimit,
  metricAggregations: ['MAXIMUM'],
});

Creating the Comparisons

There are two functions to determine which page views have increased, decreased, stayed the same, or are new.

The first is a simple reduce function that returns the URL and a count for each.

const lastWeekMap = lastWeekResults.reduce((items, item) => {
  const { url, count } = item;
  items[url] = count;
  return items;
}, {});

The second maps over the results from this week and compares them to last week.

// Generate the report for this week
const report = thisWeekResults.map((item, index) => {
  const { url, title, count } = item;
  const lastWeekCount = lastWeekMap[url];
  const status = determineStatus(count, lastWeekCount);

  return {
    position: (index + 1).toString().padStart(2, '0'), // Format the position with leading zero if it's less than 10
    url,
    title,
    count: { thisWeek: count, lastWeek: lastWeekCount || '0' }, // Ensure lastWeekCount is displayed as '0' if not found
    status,
  };
});

The final function is used to determine the status of each.

// Function to determine the status
const determineStatus = (count, lastWeekCount) => {
  const thisCount = Number(count);
  const previousCount = Number(lastWeekCount);

  if (lastWeekCount === undefined || lastWeekCount === '0') {
    return NEW;
  }

  if (thisCount > previousCount) {
    return HIGHER;
  }

  if (thisCount < previousCount) {
    return LOWER;
  }

  return SAME;
};

I’ve purposely left the code fairly verbose, so it’ll be easier for you to add console.log to each of the functions to see what they return.

Customizing the Slack Message

The I’m using creates a heading with an emoji, a divider, and a paragraph explaining what the message is.

Below that I’m using the context object to construct a and returning an object containing Slack specific message syntax which includes an icon, a count, the name of the page and a link to each item.

You can use Slack’s to construct your own message format.

// src/services/weekly-analytics.js#151 

    const slackList = report.map((item, index) => {
      const {
        position,
        url,
        title,
        count: { thisWeek, lastWeek },
        status,
      } = item;

      return {
        type: 'context',
        elements: [
          {
            type: 'image',
            image_url: ${reportConfig.url}/images/${status},
            alt_text: 'icon',
          },
          {
            type: 'mrkdwn',
            text: ${position}.  &lt;${url}|${title}&gt; | &#42;${x${thisWeek}}`* / x${lastWeek}`,
          },
        ],
      };
    });

Before you can run the GitHub Action, you will need to complete a number of Google, Slack, and GitHub steps.

Ready to get going?

Creating a Google Cloud Project

Head over to your , and from the dropdown menu at the top of the screen, click Select a project, and when the modal opens up, click NEW PROJECT.

Project name

On the next screen, give your project a name and click CREATE. In my example, I’ve named the project smashing-weekly-analytics.

Enable APIs & Services

In this step, you’ll enable the Google Analytics Data API for your new project. From the left-hand sidebar, navigate to APIs & Services > Enable APIs & services. At the top of the screen, click + ENABLE APIS & SERVICES.

Enable Google Analytics Data API

Search for “Google analytics data API,” select it from the list, then click ENABLE.

Create Credentials for Google Analytics Data API

With the API enabled in your project, you can now create the required credentials. Click the CREATE CREDENTIALS button at the top right of the screen to set up a new Service account.

A Service account allows an “application” to interact with Google APIs, providing the credentials include the required services. In this example, the credentials grant access to the Google Analytics Data API.

Service Account Credentials Type

On the next screen, select Google Analytics Data API from the dropdown menu and Application data, then click NEXT.

Service Account Details

On the next screen, give your Service account a name, ID, and description (optional). Then click CREATE AND CONTINUE.

In my example, I’ve given my service account a name and ID of smashing-weekly-analytics and added a short description that explains what the service account does.

Service Account Role

On the next screen, select Owner for the Role, then click CONTINUE.

Service Account Done

You can leave the fields blank in this last step and click DONE when you’re ready.

Service Account Keys

From the left-hand navigation, select Service Accounts, then click the “more dots” to open the menu and select Manage keys.

Service Accounts Add Key

On the next screen, locate the KEYS tab at the top of the screen, then click ADD KEY and select Create new key.

Service Accounts Download Keys

On the next screen, select JSON as the key type, then click CREATE to download your Google Application credentials .json file.

Google Application Credentials

If you open the .json file in your code editor, you should be looking at something similar to the one below.

In case you’re wondering, no, you can’t use an object as a variable defined in an .env file. To use these credentials, it’s necessary to convert the whole file into a base64 string.

Note: I wrote a more detailed post about how to use Google Application credentials as environment variables here: “.”

From your terminal, run the following: replace name-of-creds-file.json with the name of your .json file.

cat name-of-creds-file.json | base64

If you’ve already cloned the repo and followed the , add the base64 string returned after running the above and add it to the GOOGLE_APPLICATION_CREDENTIALS_BASE64 variable in your .env file, but make sure you wrap the string with double quotation makes.

GOOGLE_APPLICATION_CREDENTIALS_BASE64="abc123"

That completes the Google project side of things. The next step is to add your service account email to your Google Analytics property and find your Google Analytics Property ID.

Google Analytics Properties

Whilst your service account now has access to the Google Analytics Data API, it doesn’t yet have access to your Google Analytics account.

Get Google Analytics Property ID

To make queries to the Google Analytics API, you’ll need to know your Property ID. You can find it by heading over to your . Make sure you’re on the correct property (in the screenshot below, I’ve selected paulie.dev — GA4).

Click the admin cog in the bottom left-hand side of the screen, then click Property details.

On the next screen, you’ll see the PROPERTY ID in the top right corner. If you’ve already cloned the repo and followed the , add the property ID value to the GA4_PROPERTY_ID variable in your .env file.

Add Client Email to Google Analytics

From the Google application credential .json file you downloaded earlier, locate the client_email and copy the email address.

In my example, it looks like this: .

Now navigate to Property access management from the left hide side navigation and click the + in the top right-hand corner, then click Add users.

On the next screen, add the client_email to the Email addresses input, uncheck Notify new users by email, and select Viewer under Direct roles and data restrictions, then click Add.

That completes the Google Analytics properties section. Your “application” will use the Google application credentials containing the client_email and will now have access to your Google Analytics account via the Google Analytics Data API.

Slack Channels and Webhook

In the following steps, you’ll create a new Slack channel that will be used to post messages sent from your “application” using a .

Creating The Slack Channel

Create a new channel in your Slack workspace. I’ve named mine #weekly-analytics-report. You’ll need to set this up before proceeding to the next step.

Creating a Slack App

Head over to the dashboard, and click Create an App.

On the next screen, select From an app manifest.

On the next screen, select your Slack workspace, then click Next.

On this screen, you can give your app a name. In my example, I’ve named my Weekly Analytics Report. Click Next when you’re ready.

On step 3, you can just click Done.

With the App created, you can now set up a Webhook.

Creating a Slack Webhook

Navigate to Incoming Webhooks from the left-hand navigation, then switch the Toggle to On to activate incoming webhooks. Then, at the bottom of the screen, click Add New Webook to Workspace.

On the next screen, select your Slack workspace and a channel that you’d like to use to post messages, too, and click Allow.

You should now see your new Slack Webhook with a copy button. Copy the Webhook URL, and if you’ve already cloned the repo and followed the , add the Webhook URL to the SLACK_WEBHOOK_URL variable in your .env file.

Slack App Configuration

From the left-hand navigation, select Basic Information. On this screen, you can customize your app and add an icon and description. Be sure to click Save Changes when you’re done.

If you now head over to your Slack, you should see that your app has been added to your workspace.

That completes the Slack section of this article. It’s now time to add your environment variables to GitHub Secrets and run the workflow.

Add GitHub Secrets

Head over to the Settings tab of your GitHub repository, then from the left-hand navigation, select Secrets and variables, then click Actions.

Add the three variables from your .env file under Repository secrets.

A note on the base64 string: You won’t need to include the double quotes!

Run Workflow

To test if your Action is working correctly, head over to the Actions tab of your GitHub repository, select the Job name (Weekly Analytics Report), then click Run workflow.

If everything worked correctly, you should now be looking at a nicely formatted list of the top ten page views on your site in Slack.

Finished

And that’s it! A fully automated Google Analytics report that posts directly to your Slack. I’ve worked in a few places where Google Analytics data was on lockdown, and I think this approach to sharing Analytics data with Slack (something everyone has access to) could be super valuable for various people in your organization.

Sticky Headers And Full-Height Elements: A Tricky Combination

I was recently asked by a student to help with a seemingly simple problem. She’d been working on a website for a coffee shop that sports a sticky header, and she wanted the hero section right underneath that header to span the rest of the available vertical space in the viewport.

Here’s a visual demo of the desired effect for clarity.

Looks like it should be easy enough, right? I was sure (read: overconfident) that the problem would only take a couple of minutes to solve, only to find it was a much deeper well than I’d assumed.

Before we dive in, let’s take a quick look at the initial markup and CSS to see what we’re working with:

<body>
  <header class="header">Header Content</header>
  <section class="hero">Hero Content</section>
  <main class="main">Main Content</main>
</body>
.header {
  position: sticky;
  top: 0; /* Offset, otherwise it won't stick! */
}

/* etc. */

With those declarations, the .header will stick to the top of the page. And yet the .hero element below it remains intrinsically sized. This is what we want to change.

The Low-Hanging Fruit

The first impulse you might have, as I did, is to enclose the header and hero in some sort of parent container and give that container 100vh to make it span the viewport. After that, we could use Flexbox to distribute the children and make the hero grow to fill the remaining space.

<body>
  <div class="container">
    <header class="header">Header Content</header>
    <section class="hero">Hero Content</section>
  </div>
  <main class="main">Main Content</main>
</body>
.container {
  height: 100vh;
  display: flex;
  flex-direction: column;
}

.hero {
  flex-grow: 1;
}

/* etc. */

This looks correct at first glance, but watch what happens when scrolling past the hero.

See the Pen by .

The sticky header gets trapped in its parent container! But.. why?

If you’re anything like me, this behavior is unintuitive, at least initially. You may have heard that , meaning it participates in the normal flow of the document but only until it hits the edges of its scrolling container, at which point it becomes fixed. While viewing sticky as a combination of other values can be a useful mnemonic, it fails to capture one important difference between sticky and fixed elements:

A position: fixed element doesn’t care about the parent it’s nested in or any of its ancestors. It will break out of the normal flow of the document and place itself directly offset from the viewport, as though glued in place a certain distance from the edge of the screen.

Conversely, a position: sticky element will be pushed along with the edges of the viewport (or next closest scrolling container), but it will never escape the boundaries of its direct parent. Well, at least if you don’t count visually transform-ing it. So a better way to think about it might be, , that “position: sticky is, in a sense, a locally scoped position: fixed.” This is an intentional design decision, one that allows for section-specific sticky headers like the ones made famous by alphabetical lists in mobile interfaces.

See the Pen by .

Okay, so this approach is a no-go for our predicament. We need to find a solution that doesn’t involve a container around the header.

Fixed, But Not Solved

Maybe we can make our lives a bit simpler. Instead of a container, what if we gave the .header element a fixed height of, say, 150px? Then, all we have to do is define the .hero element’s height as height: calc(100vh - 150px).

See the Pen by .

This approach kinda works, but the downsides are more insidious than our last attempt because they may not be immediately apparent. You probably noticed that the header is too tall, and we’d wanna do some math to decide on a better height.

Thinking ahead a bit,

  • What if the .header’s children need to wrap or rearrange themselves at different screen sizes or grow to maintain legibility on mobile?
  • What if JavaScript is manipulating the contents?

All of these things could subtly change the .header’s ideal size, and chasing the right height values for each scenario has the potential to spiral into a maintenance nightmare of unmanageable breakpoints and magic numbers — especially if we consider this needs to be done not only for the .header but also the .hero element that depends on it.

I would argue that this workaround also just feels wrong. Fixed heights break one of the main affordances of CSS layout — the way elements automatically grow and shrink to adapt to their contents — and not relying on this usually makes our lives harder, not simpler.

So, we’re left with…

A Novel Approach

Now that we’ve figured out the constraints we’re working with, another way to phrase the problem is that we want the .header and .hero to collectively span 100vh without sizing the elements explicitly or wrapping them in a container. Ideally, we’d find something that already is 100vh and align them to that. This is where it dawned on me that display: grid may provide just what we need!

Let’s try this: We declare display: grid on the body element and add another element before the .header that we’ll call .above-the-fold-spacer. This new element gets a height of 100vh and spans the grid’s entire width. Next, we’ll tell our spacer that it should take up two grid rows and we’ll anchor it to the top of the page.

This element must be entirely empty because we don’t ever want it to be visible or to register to screen readers. We’re merely using it as a crutch to tell the grid how to behave.

<body>
  <!-- This spacer provides the height we want -->
  <div class="above-the-fold-spacer"></div>

  <!-- These two elements will place themselves on top of the spacer -->
  <header class="header">Header Content</header>
  <section class="hero">Hero Content</section>

  <!-- The rest of the page stays unaffected -->
  <main class="main">Main Content</main>
</body>
body {
  display: grid;
}

.above-the-fold-spacer {
  height: 100vh;
  /* Span from the first to the last grid column line */
  /* (Negative numbers count from the end of the grid) */
  grid-column: 1 / -1;
  /* Start at the first grid row line, and take up 2 rows */
  grid-row: 1 / span 2; 
}

/* etc. */

This is the magic ingredient.

By adding the spacer, we’ve created two grid rows that together take up exactly 100vh. Now, all that’s left to do, in essence, is to tell the .header and .hero elements to align themselves to those existing rows. We do have to tell them to start at the same grid column line as the .above-the-fold-spacer element so that they won’t try to sit next to it. But with that done… ta-da!

See the Pen by .

The reason this works is that a grid container can have multiple children occupying the same cell overlaid on top of each other. In a situation like that, the tallest child element defines the grid row’s overall height — or, in this case, the combined height of the two rows (100vh).

To control how exactly the two visible elements divvy up the available space between themselves, we can use the grid-template-rows property. I made it so that the first row uses min-content rather than 1fr. This is necessary so that the .header doesn’t take up the same amount of space as the .hero but instead only takes what it needs and lets the hero have the rest.

Here’s our full solution:


body {
  display: grid;
  grid-template-rows: min-content 1fr;
}

.above-the-fold-spacer {
  height: 100vh;
  grid-column: 1 / -1;
  grid-row: 1 / span 2;
}

.header {
  position: sticky;
  top: 0;
  grid-column-start: 1;
  grid-row-start: 1;
}

.hero {
  grid-column-start: 1;
  grid-row-start: 2;
}

And voila: A sticky header of arbitrary size above a hero that grows to fill the remaining visible space!

Caveats and Final Thoughts

It’s worth noting that the HTML order of the elements matters here. If we define .above-the-fold-spacer after our .hero section, it will overlay and block access to the elements underneath. We can work around this by declaring either order: -1, z-index: -1, or visibility: hidden.

Keep in mind that this is a simple example. If you were to add a sidebar to the left of your page, for example, you’d need to adjust at which column the elements start. Still, in the majority of cases, using a CSS Grid approach is likely to be less troublesome than the Sisyphean task of manually managing and coordinating the height values of multiple elements.

Another upside of this approach is that it’s adaptable. If you decide you want a group of three elements to take up the screen’s height rather than two, then you’d make the invisible spacer span three rows and assign the visible elements to the appropriate one. Even if the hero element’s content causes its height to exceed 100vh, the grid adapts without breaking anything. It’s even well-supported in all modern browsers.

The more I think about this technique, the more I’m persuaded that it’s actually quite clean. Then again, you know how lawyers can talk themselves into their own arguments? If you can think of an even simpler solution I’ve overlooked, feel free to reach out and let me know!

How to Download a YouTube Video or Channel

The ability to download media on the internet almost feels like a lost art. When I was in my teens, piracy of mp3s, movies, and just about everything else via torrents and apps like Kazaa, LimeWire, Napster, etc. was in full swing. These days sites use blob URLs and other means to prevent downloads. Luckily we have tools like yt-dlp to download individual YouTube videos or entire channels of content.

To download an entire channel, you can use yt-dlp:

yt-dlp https://www.youtube.com/@beetlejuicearchives3490

If you’re like me and only care for the audio, you can use a few more arguments:

yt-dlp -x --audio-format mp3 https://www.youtube.com/@beetlejuicearchives3490

youtube-dl used to be the standard for downloading YouTube videos but yt-dlp seems to have taken the throne. YouTube has such a wealth of information on just about anything, be sure to download content for travel, long walks, or any other reason!

The post appeared first on .

The Big Difference Between Digital Product And Web Design

In the early days of the web, I remember how annoying it was when print designers would claim they could design websites, too. They assumed that just because they could design for one medium, they could design for the other.

That assumption often led to bad user experiences. The skills for effective web design are quite different from those for print design.

A similar thing happens today. Designers know how to design traditional marketing and e-commerce sites. They, therefore, presume they have the skills to work on SaaS apps and other digital projects.

But when it comes to design, there’s a big distinction between traditional websites and digital products. If we want to work on digital products, we need to understand those differences and adopt a different approach to our work.

People Interact with Digital Products More Regularly

The biggest difference is that users interact with digital products more than most websites.

Think about your own web use. What are the sites you visit most often? If you listed your top ten, well over half would be some form of digital product, from a social media application to a productivity tool.

So, with that in mind, let’s dive into the specifics of how the frequency of usage impacts our design approach and what we can do about it.

Why Frequency of Use Matters So Much

The more we interact with a web app or website, the more important the overall user experience becomes. Users develop deeper connections with digital products. They also form more complex mental models of products they use often. This changes how they see the app in two fundamental ways.

Friction Becomes Significantly More Irritating

First, friction points become increasingly annoying. Users interact with a digital product many times per day. Any small problem in the interface compounds quickly.

When you encounter a clunky UI or confusing workflow on a website you only visit once in a while, it’s frustrating but easy to overlook. But, when that same friction occurs in an app you use multiple times per day, it becomes a major source of irritation.

Change Undermines Our Procedural Knowledge

Second, the more we use an app, the more familiar we become with it and how it works. We end up using the app automatically, without even thinking, much like when you’ve been driving a car for years, you don’t think about the process. This is known as .

This is great news for digital product designers, as it means we can create interfaces that become second nature to our users. But, if we break their mental models or introduce unexpected changes, we risk causing frustration and disruption.

So, knowing these two things, how does this affect our approach to digital product design? Well, let’s start by considering the problem of friction.

Fixing Friction Points

As digital product designers, we need to become obsessed with removing friction from the user experience. Failure to do so will alienate users over time and ultimately lead to churn.

To mitigate friction, we need to constantly seek out friction points. We need to diagnose the exact problem and then test any solution to ensure it does, in fact, make things better.

So, how exactly do we find friction points?

Finding Friction

The most obvious way is to listen to customers. User feedback is crucial in identifying friction points in the user experience. However, we can’t simply rely on that. Analytics can be your friend, too.

Microsoft Clarity offers essential insights to pinpoint issues on your app.

I would highly recommend using a tool like . It gives detailed insights into user behavior. They help find points of friction. These include the following:

  • Rage clicks: Where individuals continuously click on something due to frustration.
  • Dead clicks: Where people click on something that is not clickable.
  • Excessive scrolling: Where users scroll up and down looking for something.
  • Quick backs: Where a person accidentally lands on a screen and promptly navigates back to the previous one.
  • Error messages: Where the user is triggering an error in the system.

These will help you identify potential friction points that you can then investigate further.

Diagnosing Friction

Once you know where things are going wrong, you can use heat maps and session recordings in Clarity. They will help you understand the problem. Why are people excessively scrolling or rage-clicking, for example?

Session recordings are valuable for pinpointing particular problems in the interface.

If the heat maps or session recordings don’t make things clear, that is where you would need to consider usability testing.

Once you understand the problem, you can then begin exploring solutions and testing them rigorously to ensure they effectively reduce friction.

Testing Your Friction Busting Solutions

How you test your solution to the point of friction will depend on the size and complexity of the changes you need to make.

For small changes, such as tweaking the UI or changing some text, A/B testing is often the best approach. You show the new solution to a subset of your users and measure the impact on those indicators of frustration.

But A/B testing isn’t always the right approach. If your app has lower levels of traffic, getting results from a statistically significant A/B test can be time-consuming.

Also, when your solution involves big changes, like adding new features or redesigning many screens, A/B testing can be expensive. That is because you need to first fully develop the solution before you can test it, meaning that it can prove costly if that solution turns out not to work.

Your best approach in such situations is to create a prototype for remote testing.

Initially, I usually conduct unfacilitated testing with a tool such as . Unfacilitated testing is easy to set up. It requires minimal time investment, and Maze offers analytics, so you don’t necessarily need to watch every session back.

Maze serves as a valuable resource for conducting remote testing, offering both test data and recordings for each test.

If testing uncovers issues you can’t fix, then try facilitated testing. Facilitated testing enables you to delve into any arising issues by asking questions.

Once you have a solution that works, it’s time to roll that feature out. But you need to be careful at this point because of the procedural knowledge I mentioned earlier.

Dealing With the Dangers of Procedural Knowledge

Introducing fixes to a user interface has a good chance of breaking a user’s procedural knowledge. Interface elements are often moved and so are no longer where users expect to find them, or they look different, and so users miss them.

This can upset many existing customers. That can panic stakeholders and lead to rash decisions.

To some extent, you need to accept that this is inevitable and prepare stakeholders for this eventuality. Users will normally adapt in a couple of weeks of regular use, and so there is no immediate need to panic.

That said, there are things you can do to mitigate the reaction.

  1. To start with, you can let people know that change is coming. This allows people to mentally adapt to the change before it occurs.
  2. Secondly, if the change is significant, you may wish to give people the ability to opt out of it, at least in the short term. That is why some apps roll out features in beta and give users the option to opt in or out. This provides a sense of control that reduces people’s reactions.
  3. Finally, you can also provide guidance within the user interface itself. Tooltips and overlays can show users where features have been moved so new interface elements can be highlighted.

Slack use tooltips to explain how their interface works.

The key is to strike a balance. You must add needed improvements while causing minimal disruption to users’ workflows. You will also need to carefully monitor adoption and adapt accordingly.

Change The Way We Work

That constant monitoring and adaptation lies at the heart of digital product design. You cannot rely solely on the initial solution but must be prepared to continuously refine and iterate as user behavior and needs evolve.

Goodbye Summer, Hello September (2024 Wallpapers Edition)

Lush green slowly turning into yellows and reds in the Northern hemisphere; nature reawakening in the Southern part of the world: September is a time of change. A chance to leave old habits behind and embrace the beginning of something new. And, well, sometimes it only takes a small change in routines to spark fresh inspiration and, who knows, maybe even great ideas.

With that in mind, we started our more than 13 years ago, and from the very beginning to today, artists and designers from across the globe have submitted their designs to it to cater for a bit of variety on your screens every month. Of course, it wasn’t any different this time around.

In this post, you’ll find their wallpaper designs for September 2024. All of them come in versions with and without a calendar and can be downloaded for free. As a little bonus goodie, we also added some favorites from past years’ September editions to the collection. So maybe you’ll spot one of your almost-forgotten favorites in here, too? A huge thank-you to everyone who shared their wallpapers with us this month — this post wouldn’t exist without you!

  • You can click on every image to see a larger preview,
  • We respect and carefully consider the ideas and motivation behind each and every artist’s work. This is why we give all artists the full freedom to explore their creativity and express emotions and experience through their works. This is also why the themes of the wallpapers weren’t anyhow influenced by us but rather designed from scratch by the artists themselves.

  • Did you know that you could get featured in our next wallpapers post, too? We are always looking for creative talent.

National Elephant Appreciation Day

“Today, we celebrate these magnificent creatures who play such a vital role in our ecosystems and cultures. Elephants are symbols of wisdom, strength, and loyalty. Their social bonds are strong, and their playful nature, especially in the young ones, reminds us of the importance of joy and connection in our lives.” — Designed by from Serbia.

  • with calendar: , , , , , , , , , , , , , , , , , , ,
  • without calendar: , , , , , , , , , , , , , , , , , , ,

Summer In Costa Rica

“We continue in tropical climates. In this case, we travel to Costa Rica to observe the Arenal volcano from the lake while we use a kayak.” — Designed by from Spain.

  • with calendar: , , , , , , , , ,
  • without calendar: , , , , , , , , ,

Pigman And Robin

Designed by from Spain.

  • with calendar: , , , , , , , , , , , , , , , , , , , ,
  • without calendar: , , , , , , , , , , , , , , , , , , , ,

A Mind Of Their Own

“My eyes have a mind of their own: they see what they want to see…” — Designed by from India.

  • with calendar: , , , , , , ,
  • without calendar: , , , , , , ,

More Bananas

Designed by from Spain.

  • with calendar: , , , , , , , , , , , , , , , , , , , ,
  • without calendar: , , , , , , , , , , , , , , , , , , , ,

Quality Education For All

“Our team takes pride in aligning our volunteer initiatives with the 2030 Agenda for Sustainable Development’s ‘Quality Education’ goal. This goal reflects a global commitment to ensure inclusive and equitable quality education and promote lifelong learning opportunities for all. We encourage our team members to volunteer with non-profits they care about year-round. Explore local opportunities and use your skills to make a meaningful impact!” — Designed by from Portland, OR.

  • with calendar: , , , , ,
  • without calendar: , , , , ,

Green Jewellery

“I was thinking about African bead necklaces when making this wallpaper. I chose green and warm colors, because summer has not ended in the north — let’s enjoy it.” — Designed by from France.

  • with calendar: , , , , , , , ,
  • without calendar: , , , , , , , ,

Discover, Dream, Travel!

“Celebrate World Tourism Day by exploring new destinations and cultures around the globe!” — Designed by from London.

  • with calendar: , , , , , , , , , , , , , , , , , ,
  • without calendar: , , , , , , , , , , , , , , , , , ,

Happy Labor Day

“I wanted my design to revolve around the themes of unity, hard work, and patriotism to honor the workforce that builds a great nation. The flags, the skyline, and the human figure outline evoke a sense of pride, appreciation, dedication, and solidarity.” — Designed by from the United States.

  • with calendar: , , , , , , , , , , , , , , , , , , , , , , , , ,
  • without calendar: , , , , , , , , , , , , , , , , , , , , , , , , ,

Autumn Rains

“This autumn, we expect to see a lot of rainy days and blues, so we wanted to change the paradigm and wish a warm welcome to the new season. After all, if you come to think of it: rain is not so bad if you have an umbrella and a raincoat. Come autumn, we welcome you!” — Designed by from Serbia.

  • without calendar: , , , , , , , , , , , , , , , , , , , ,

Terrazzo

“With the end of summer and fall coming soon, I created this terrazzo pattern wallpaper to brighten up your desktop. Enjoy the month!” — Designed by from Belgium.

  • without calendar: , , , , , , , , , , , , , , , , , , ,

Funny Cats

“Cats are beautiful animals. They’re quiet, clean, and warm. They’re funny and can become an endless source of love and entertainment. Here for the cats!” — Designed by UrbanUI from India.

  • without calendar: , , , , , , , , , ,

Cacti Everywhere

“Seasons come and go, but our brave cactuses still stand. Summer is almost over and autumn is coming, but the beloved plants don’t care.” — Designed by from Hungary.

  • without calendar: , , , , , , , , ,

Summer Ending

“As summer comes to an end, all the creatures pull back to their hiding places, searching for warmth within themselves and dreaming of neverending adventures under the tinted sky of closing dog days.” — Designed by Ana Masnikosa from Belgrade, Serbia.

  • without calendar: , , , , , , , , , , , , , , , , , , ,

The Rebel

Designed by from Spain.

  • without calendar: , , , , , , , , , , , , , , , , , , , ,

National Video Games Day Delight

“September 12th brings us National Video Games Day. US-based video game players love this day and celebrate with huge gaming tournaments. What was once a 2D experience in the home is now a global phenomenon with players playing against each other across statelines and national borders via the internet. National Video Games Day gives gamers the perfect chance to celebrate and socialize! So grab your controller, join online and let the games begin!” — Designed by from the United Kingdom.

  • without calendar: , , , , , , , , , , , , , , , , , , , ,

Long Live Summer

“While September’s Autumnal Equinox technically signifies the end of the summer season, this wallpaper is for all those summer lovers, like me, who don’t want the sunshine, warm weather, and lazy days to end.” — Designed by Vicki Grunewald from Washington.

  • without calendar: , , , , , , , , , , , , , , , , , , , ,

Flower Soul

“The earth has music for those who listen. Take a break and relax and while you drive out the stress, catch a glimpse of the beautiful nature around you. Can you hear the rhythm of the breeze blowing, the flowers singing, and the butterflies fluttering to cheer you up? We dedicate flowers which symbolize happiness and love to one and all.” — Designed by from India.

  • without calendar: , , , , , , , , , , , , , , , , , , ,

Stay Or Leave?

Designed by from Spain.

  • without calendar: , , , , , , , , , , , , , , , , , , , ,

Hungry

Designed by from Belgium.

  • without calendar: , , , , , , , , , , , , , , , , , , ,

Rainy Flowers

Designed by from Bulgaria.

  • without calendar: , , , , , , , , , , , , ,

Science Is Magic

“Science is like magic, except it’s real.” — Designed by from India.

  • without calendar: , , , , , , ,

Listen Closer… The Mushrooms Are Growing

“It’s this time of the year when children go to school and grown-ups go to collect mushrooms.” — Designed by from Canada.

  • without calendar: , , , , , , , , , , , , , , , , , , ,

Batmom

Designed by from Spain.

  • without calendar: , , , , , , , , , , , , , , , , , , , ,

Wine Harvest Season

“Welcome to the wine harvest season in Serbia. It’s September, and the hazy sunshine bathes the vines on the slopes of Fruška Gora. Everything is ready for the making of Bermet, the most famous wine from Serbia. This spiced wine was a favorite of the Austro-Hungarian elite and was served even on the Titanic. Bermet’s recipe is a closely guarded secret, and the wine is produced by just a handful of families in the town of Sremski Karlovci, near Novi Sad. On the other side of Novi Sad, plains of corn and sunflower fields blend in with the horizon, catching the last warm sun rays of this year.” — Designed by from Serbia.

  • without calendar: , , , , , , , , , , , , , , , , , , , ,

Bear Time

Designed by from Serbia.

  • without calendar: , , , , , , , , , , , , , , , , , , , ,

Maryland Pride

“As summer comes to a close, so does the end of blue crab season in Maryland. Blue crabs have been a regional delicacy since the 1700s and have become Maryland’s most valuable fishing industry, adding millions of dollars to the Maryland economy each year. The blue crab has contributed so much to the state’s regional culture and economy, in 1989 it was named the State Crustacean, cementing its importance in Maryland history.” — Designed by from Washington DC.

  • without calendar: , , , , , , , , , , ,

Finding Jaguar

“Nature and our planet have given us life, enabled us to enjoy the most wonderful place known to us in the universe. People have given themselves the right to master something they do not fully understand. We dedicate this September calendar to a true nature lover, Vedran Badjun from Dalmatia, Croatia, who inspires us to love our planet, live in harmony with it and appreciate all that it has to offer. Amazon, Siberia, and every tree or animal on the planet are treasures we lose every day. Let’s change that!” — Designed by from Serbia.

  • without calendar: , , , , , , , , , , , , , , , , , , ,

Penguin Family

“Penguins are sociable, independent and able to survive harsh winters. They work as a team to care for their offspring and I love that!” — Designed by from Australia.

  • without calendar: , , , , , , , , , ,

Early Autumn

“September is usually considered as early autumn so I decided to draw some trees and leaves. However, nobody likes that summer is coming to an end, that’s why I kept summerish colors and style.” — Designed by from Germany.

  • without calendar: , , , , , , , ,

Summer Is Leaving

“It is inevitable. Summer is leaving silently. Let us think of ways to make the most of what is left of the beloved season.” — Designed by from India.

  • without calendar: , , , , , , , , , , ,

Lucha Libre

“This month is Mexico’s independence day and I decided to illustrate one of the things Mexico’s best known for: the Lucha Libre.” — Designed by from Mexico.

  • without calendar: , , , , , , , , , , , , , , , , , , , , , , , ,

Geometric Autumn

“I designed this wallpaper to remind everyone that autumn is here.” — Designed by from Romania.

  • without calendar: , , , , , , , , , ,

Never Stop Exploring

Designed by from Spain.

  • without calendar: , , , , , , , , , , , , , , , , , , , ,

Still In Vacation Mood

“It’s officially the end of summer and I’m still in vacation mood, dreaming about all the amazing places I’ve seen. This illustration is inspired by a small town in France, on the Atlantic coast, right by the beach.” — Designed by from Romania.

  • without calendar: , , , , , , , , , , , , , , , , , , ,

Colors Of September

“I love September. Its colors and smells.” — Designed by from Ukraine.

  • without calendar: , , , , , , , ,

Office

“Clean, minimalistic office for a productive day.” — Designed by Antun Hiršman from Croatia.

  • without calendar: , , , , , , , ,

Integrating Image-To-Text And Text-To-Speech Models (Part 2)

of this brief two-part series, we developed an application that turns images into audio descriptions using vision-language and text-to-speech models. We combined an image-to-text that analyses and understands images, generating description, with a text-to-speech model to create an audio description, helping people with sight challenges. We also discussed how to choose the right model to fit your needs.

Now, we are taking things a step further. Instead of just providing audio descriptions, we are building that can have interactive conversations about images or videos. This is known as Conversational AI — a technology that lets users talk to systems much like chatbots, virtual assistants, or agents.

While the first iteration of the app was great, the output still lacked some details. For example, if you upload an image of a dog, the description might be something like “a dog sitting on a rock in front of a pool,” and the app might produce something close but miss additional details such as the dog’s breed, the time of the day, or location.

The aim here is simply to build a more advanced version of the previously built app so that it not only describes images but also provides more in-depth information and engages users in meaningful conversations about them.

We’ll use , a model that combines understanding images and conversational capabilities. After building our tool, we’ll explore multimodal models that can handle images, videos, text, audio, and more, all at once to give you even more options and easiness for your applications.

Visual Instruction Tuning and LLaVA

We are going to look at visual instruction tuning and the multimodal capabilities of LLaVA. We’ll first explore how visual instruction tuning can enhance the large language models to understand and follow instructions that include visual information. After that, we’ll dive into LLaVA, which brings its own set of tools for image and video processing.

Visual Instruction Tuning

Visual instruction tuning is a technique that helps large language models (LLMs) understand and follow instructions based on visual inputs. This approach connects language and vision, enabling AI systems to understand and respond to human instructions that involve both text and images. For example, Visual IT enables a model to describe an image or answer questions about a scene in a photograph. This fine-tuning method makes the model more capable of handling these complex interactions effectively.

There’s a new training approach called that has been developed, and you can think of it as a tool for handling tasks related to PDFs, invoices, and text-heavy images. It’s pretty exciting, but we won’t dive into that since it is outside the scope of the app we’re making.

Examples of Visual Instruction Tuning Datasets

To build good models, you need good data — rubbish in, rubbish out. So, here are two datasets that you might want to use to train or evaluate your multimodal models. Of course, you can always add your own datasets to the two I’m going to mention.

  • Instruction datasets: English;
  • Multi-task: Datasets containing multiple tasks;
  • Mixed dataset: Contains both human and machine-generated data.

Vision-CAIR provides a high-quality, well-aligned image-text dataset created using conversations between two bots. This dataset was initially introduced in a paper titled “,” and it provides more detailed image descriptions and can be used with predefined instruction templates for image-instruction-answer fine-tuning.

There are more multimodal datasets out there, but these two should help you get started if you want to fine-tune your model.

Let’s Take a Closer Look At LLaVA

LLaVA (which stands for Large Language and Vision Assistant) is a groundbreaking multimodal model developed by researchers from the University of Wisconsin, Microsoft Research, and Columbia University. The researchers aimed to create a powerful, open-source model that could compete with the best in the field, just like GPT-4, Claude 3, or Gemini, to name a few. For developers like you and me, its open nature is a huge benefit, allowing for easy fine-tuning and integration.

One of LLaVA’s standout features is its ability to understand and respond to complex visual information, even with unfamiliar images and instructions. This is exactly what we need for our tool, as it goes beyond simple image descriptions to engage in meaningful conversations about the content.

Architecture

LLaVA’s strength lies in its smart use of existing models. Instead of starting from scratch, the researchers used two key models:

  • This is an advanced version of the CLIP (Contrastive Language–Image Pre-training) model developed by OpenAI. CLIP learns visual concepts from natural language descriptions. It can handle any visual classification task by simply being given the names of the visual categories, similar to the “zero-shot” capabilities of GPT-2 and GPT-3.
  • This is an open-source chatbot trained by fine-tuning on 70,000 user-shared conversations collected from . Training Vicuna-13B costs around $300, and it performs exceptionally well, even when compared to other models like .

These components make LLaVA highly effective by combining state-of-the-art visual and language understanding capabilities into a single powerful model, perfectly suited for applications requiring both visual and conversational AI.

Training

LLaVA’s training process involves two important stages, which together enhance its ability to understand user instructions, interpret visual and language content, and provide accurate responses. Let’s detail what happens in these two stages:

  1. Pre-training for Feature Alignment
    LLaVA ensures that its visual and language features are aligned. The goal here is to update the projection matrix, which acts as a bridge between the CLIP visual encoder and the Vicuna language model. This is done using a subset of the CC3M dataset, allowing the model to map input images and text to the same space. This step ensures that the language model can effectively understand the context from both visual and textual inputs.
  2. End-to-End Fine-Tuning
    The entire model undergoes fine-tuning. While the visual encoder’s weights remain fixed, the projection layer and the language model are adjusted.

The second stage is tailored to specific application scenarios:

  • Instructions-Based Fine-Tuning
    For general applications, the model is fine-tuned on a dataset designed for following instructions that involve both visual and textual inputs, making the model versatile for everyday tasks.
  • Scientific reasoning
    For more specialized applications, particularly in science, the model is fine-tuned on data that requires complex reasoning, helping the model excel at answering detailed scientific questions.

Now that we’re keen on what LLaVA is and the role it plays in our applications, let’s turn our attention to the next component we need for our work, Whisper.

Using Whisper For Text-To-Speech

In this chapter, we’ll check out Whisper, a great model for turning text into speech. Whisper is accurate and easy to use, making it perfect for adding natural-sounding voice responses to our app. We’ve used Whisper in a different article, but here, we’re going to use a new version — large v3. This updated version of the model offers even better performance and speed.

Whisper large-v3

Whisper was developed by OpenAI, which is the same folks behind ChatGPT. Whisper is a pre-trained model for automatic speech recognition (ASR) and speech translation. The original Whisper was trained on 680,000 hours of labeled data.

Now, what’s different with Whisper large-v3 compared to other models? In my experience, it comes down to the following:

  • Better inputs
    Whisper large-v3 uses 128 Mel frequency bins instead of 80. Think of Mel frequency bins as a way to break down audio into manageable chunks for the model to process. More bins mean finer detail, which helps the model better understand the audio.
  • More training
    This specific Whisper version was trained on 1 million hours of weakly labeled audio and 4 million hours of pseudo-labeled audio that was collected from Whisper large-v2. From there, the model was trained for 2.0 epochs over this mix.

Whisper models come in different sizes, from tiny to large. Here’s a table comparing the differences and similarities:

Size Parameters English-only Multilingual
tiny 39 M
base 74 M
small 244 M
medium 769 M
large 1550 M
large-v2 1550 M
large-v3 1550 M

Integrating LLaVA With Our App

Alright, so we’re going with LLaVA for image inputs, and this time, we’re adding video inputs, too. This means the app can handle both images and videos, making it more versatile.

We’re also keeping the speech feature so you can hear the assistant’s replies, which makes the interaction even more engaging. How cool is that?

For this, we’ll use Whisper. We’ll stick with the Gradio framework for the app’s visual layout and user interface. You can, of course, always swap in other models or frameworks — the main goal is to get a working prototype.

Installing and Importing the Libraries

We will start by installing and importing all the required libraries. This includes the transformers libraries for loading the LLaVA and Whisper models, bitsandbytes for quantization, gtts, and moviepy to help in processing video files, including frame extraction.

#python
!pip install -q -U transformers==4.37.2
!pip install -q bitsandbytes==0.41.3 accelerate==0.25.0
!pip install -q git+https://github.com/openai/whisper.git
!pip install -q gradio
!pip install -q gTTS
!pip install -q moviepy

With these installed, we now need to import these libraries into our environment so we can use them. We’ll use for that:

#python
import torch
from transformers import BitsAndBytesConfig, pipeline
import whisper
import gradio as gr
from gtts import gTTS
from PIL import Image
import re
import os
import datetime
import locale
import numpy as np
import nltk
import moviepy.editor as mp

nltk.download('punkt')
from nltk import sent_tokenize

# Set up locale
os.environ["LANG"] = "en_US.UTF-8"
os.environ["LC_ALL"] = "en_US.UTF-8"
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')

Configuring Quantization and Loading the Models

Now, let’s set up a 4-bit quantization to make the LLaVA model more efficient in terms of performance and memory usage.

#python

# Configuration for quantization
quantization_config = BitsAndBytesConfig(
  load_in_4bit=True,
  bnb_4bit_compute_dtype=torch.float16
)

# Load the image-to-text model
model_id = "llava-hf/llava-1.5-7b-hf"
pipe = pipeline("image-to-text",
  model=model_id,
  model_kwargs={"quantization_config": quantization_config})

# Load the whisper model
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
model = whisper.load_model("large-v3", device=DEVICE)

In this code, we’ve configured the quantization to four bits, which reduces memory usage and improves performance. Then, we load the LLaVA model with these settings. Finally, we load the whisper model, selecting the device based on GPU availability for better performance.

Note: We’re using as the model. Please feel free to explore . For Whisper, we’re loading the “large” size, but you can also switch to another size like “medium” or “small” for your experiments.

To get our assistant up and running, we need to implement five essential functions:

  1. Handling conversations,
  2. Converting images to text,
  3. Converting videos to text,
  4. Transcribing audio,
  5. Converting text to speech.

Once these are in place, we will create another function to tie all this together seamlessly. The following sections provide the code that defines each function.

Conversation History

We’ll start by setting up the conversation history and a function to log it:

#python

# Initialize conversation history
conversation_history = []

def writehistory(text):
  """Write history to a log file."""
  tstamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  logfile = f'{tstamp}_log.txt'
  with open(logfile, 'a', encoding='utf-8') as f:
    f.write(text + 'n')

Image to Text

Next, we’ll create a function to convert images to text using LLaVA and iterative prompts.

#python
def img2txt(input_text, input_image):
  """Convert image to text using iterative prompts."""
  try:
    image = Image.open(input_image)

    if isinstance(input_text, tuple):
      input_text = input_text[0]  # Take the first element if it's a tuple

      writehistory(f"Input text: {input_text}")
      prompt = "USER: <image>n" + input_text + "nASSISTANT:"
      while True:
        outputs = pipe(image, prompt=prompt, generate_kwargs={"max_new_tokens": 200})

          if outputs and outputs[0]["generated_text"]:
            match = re.search(r'ASSISTANT:s*(.*)', outputs[0]["generated_text"])
            reply = match.group(1) if match else "No response found."
            conversation_history.append(("User", input_text))
            conversation_history.append(("Assistant", reply))
            prompt = "USER: " + reply + "nASSISTANT:"
            return reply  # Only return the first response for now
          else:
            return "No response generated."
  except Exception as e:
    return str(e)

Video to Text

We’ll now create a function to convert videos to text by extracting frames and analyzing them.

#python
def vid2txt(input_text, input_video):
  """Convert video to text by extracting frames and analyzing."""
  try:
    video = mp.VideoFileClip(input_video)
    frame = video.get_frame(1)  # Get a frame from the video at the 1-second mark
    image_path = "temp_frame.jpg"
    mp.ImageClip(frame).save_frame(image_path)
    return img2txt(input_text, image_path)
  except Exception as e:
    return str(e)

Audio Transcription

Let’s add a function to transcribe audio to text using Whisper.

#python
def transcribe(audio_path):
  """Transcribe audio to text using Whisper model."""
  if not audio_path:
    return ''

  audio = whisper.load_audio(audio_path)
  audio = whisper.pad_or_trim(audio)
  mel = whisper.log_mel_spectrogram(audio).to(model.device)
  options = whisper.DecodingOptions()
  result = whisper.decode(model, mel, options)
  return result.text

Text to Speech

Lastly, we create a function to convert text responses into speech.

#python
def text_to_speech(text, file_path):
  """Convert text to speech and save to file."""
  language = 'en'
  audioobj = gTTS(text=text, lang=language, slow=False)
  audioobj.save(file_path)
  return file_path

With all the necessary functions in place, we can create the main function that ties everything together:

#python

def chatbot_interface(audio_path, image_path, video_path, user_message):
  """Process user inputs and generate chatbot response."""
  global conversation_history

  # Handle audio input
  if audio_path:
    speech_to_text_output = transcribe(audio_path)
  else:
    speech_to_text_output = ""

  # Determine the input message
  input_message = user_message if user_message else speech_to_text_output

  # Ensure input_message is a string
  if isinstance(input_message, tuple):
    input_message = input_message[0]

  # Handle image or video input
  if image_path:
    chatgpt_output = img2txt(input_message, image_path)
  elif video_path:
      chatgpt_output = vid2txt(input_message, video_path)
  else:
    chatgpt_output = "No image or video provided."

  # Add to conversation history
  conversation_history.append(("User", input_message))
  conversation_history.append(("Assistant", chatgpt_output))

  # Generate audio response
  processed_audio_path = text_to_speech(chatgpt_output, "Temp3.mp3")

  return conversation_history, processed_audio_path

Using Gradio For The Interface

The final piece for us is to create the layout and user interface for the app. Again, we’re using to build that out for quick prototyping purposes.

#python

# Define Gradio interface
iface = gr.Interface(
  fn=chatbot_interface,
  inputs=[
    gr.Audio(type="filepath", label="Record your message"),
    gr.Image(type="filepath", label="Upload an image"),
    gr.Video(label="Upload a video"),
    gr.Textbox(lines=2, placeholder="Type your message here...", label="User message (if no audio)")
  ],
  outputs=[
    gr.Chatbot(label="Conversation"),
    gr.Audio(label="Assistant's Voice Reply")
  ],
  title="Interactive Visual and Voice Assistant",
  description="Upload an image or video, record or type your question, and get detailed responses."
)

# Launch the Gradio app
iface.launch(debug=True)

Here, we want to let users record or upload their audio prompts, type their questions if they prefer, upload videos, and, of course, have a conversation block.

Here’s a preview of how the app will look and work:

Looking Beyond LLaVA

LLaVA is a great model, but there are even greater ones that don’t require a separate ASR model to build a similar app. These are called multimodal or “any-to-any” models. They are designed to process and integrate information from multiple modalities, such as text, images, audio, and video. Instead of just combining vision and text, these models can do it all: image-to-text, video-to-text, text-to-speech, speech-to-text, text-to-video, and image-to-audio, just to name a few. It makes everything simpler and less of a hassle.

Examples of Multimodal Models that Handle Images, Text, Audio, and More

Now that we know what multimodal models are, let’s check out some cool examples. You may want to integrate these into your next personal project.

CoDi

So, the first on our list is or Composable Diffusion. This model is pretty versatile, not sticking to any one type of input or output. It can take in text, images, audio, and video and turn them into different forms of media. Imagine it as a sort of AI that’s not tied down by specific tasks but can handle a mix of data types seamlessly.

CoDi was developed by researchers from the University of North Carolina and Microsoft Azure. It uses something called to sync different types of data, like aligning audio perfectly with the video, and it can generate outputs that weren’t even in the original training data, making it super flexible and innovative.

ImageBind

Now, let’s talk about , a model from Meta. This model is like a multitasking genius, capable of binding together data from six different modalities all at once: images, video, audio, text, depth, and even thermal data.

Source: . ()

ImageBind doesn’t need explicit supervision to understand how these data types relate. It’s great for creating systems that use multiple types of data to enhance our understanding or create immersive experiences. For example, it could combine 3D sensor data with to design virtual worlds or enhance memory searches across different media types.

Gato

is another fascinating model. It’s built to be a generalist agent that can handle a wide range of tasks using the same network. Whether it’s playing games, chatting, captioning images, or controlling a robot arm, Gato can do it all.

The key thing about Gato is its ability to switch between different types of tasks and outputs using the same model.

GPT-4o

The next on our list is ; GPT-4o is a groundbreaking multimodal large language model (MLLM) developed by OpenAI. It can handle any mix of text, audio, image, and video inputs and give you text, audio, and image outputs. It’s super quick, responding to audio inputs in just 232ms to 320ms, almost like a real conversation.

There’s a smaller version of the model called . Small models are becoming a trend, and this one shows that even small models can perform really well. Check out this evaluation to see how the small model stacks up against other large models.

Conclusion

We covered a lot in this article, from setting up LLaVA for handling both images and videos to incorporating Whisper large-v3 for top-notch speech recognition. We also explored the versatility of multimodal models like CoDi or GPT-4o, showcasing their potential to handle various data types and tasks. These models can make your app more robust and capable of handling a range of inputs and outputs seamlessly.

Which model are you planning to use for your next app? Let me know in the comments!