Skip to content

Animations in Svelte

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Animations in Svelte

Animations are more present than ever in our websites and applications. They can make them look and feel different if done right, engaging your users. In this post, we'll learn how animations can be used in Svelte, and how you can extend those that are shipped with the library.

Modules

Svelte ships a series of modules that will help us while creating animations. We'll explore each of these to understand what they do.

  • animate
  • easing
  • motion
  • transition

svelte/easing

This package contains a series of functions with equations to produce different easing curves Available curves are:

  • back
  • bounce
  • circ
  • cubic
  • elastic
  • expo
  • quad
  • quart
  • quint
  • sine

But you can create your custom function as long as it's a function that will accept 1 parameter varying from 0 to 1 (1 represents the total duration of the animation) and returns another value, also ranging from 0 to 1.

svelte/motion

Two functions are exported in this package: tweened and spring.

Both of them will return a reactive value, interpolating in-between values given a set of parameters.

Note that these functions do not necessarily animate anything visually, but rather, create a ramp between values. These values can then be displayed or assigned to something else, like CSS properties.

Both functions can interpolate numbers, dates, arrays, and objects. You can also provide a different function for interpolating values.

tweened

Let's initialize a new Svelte app to see how it works.

npm init vite

✔ Project name: · svelte-animations
✔ Select a framework: · svelte
✔ Select a variant: · svelte-ts

cd svelte-web-components
pnpm install //use the package manager you prefer
pnpm run dev

// remove default Counter component
rm src/lib/Counter.svelte

Clear the App.svelte component to contain only what we need now.

<script>
	// add imports here
</script>

<main>
</main>

<style>
  :root {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
      Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
  }

  main {
    text-align: center;
    padding: 1em;
    margin: 0 auto;
  }

  :global(main > * + *)  {
    margin-top: 24px;
  }
</style>

I'll create a new component named Tasks.svelte inside the lib folder.

<script lang="ts">
  import { tweened } from 'svelte/motion';
  export let tasks: { id; title; date }[] = [];

  let selected;
  tasks = tasks.sort((a, b) => {
    if (a.date > b.date) {
      return 1;
    } else if (a.date === b.date) {
      return 0;
    } else {
      return -1;
    }
  });

  function pad(num) {
    if (num < 10) {
      return `0${num}`;
    }
    return num;
  }

  function getDate(date) {
    return date
      ? `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(
          date.getDate(),
        )}`
      : '';
  }

  function getTime(date) {
    return date ? `${pad(date.getHours())}:${pad(date.getMinutes())}` : '';
  }

  let now = new Date();
  let date = tweened(now, { duration: 500 });

  function selectTask(task) {
    selected = task.id;
    date.set(task.date);
  }
</script>

<div class="task-view">
  <div class="task-list">
    <h2>Next tasks</h2>
    <ul>
      {#each tasks as task}
        <li
          class={selected === task.id ? 'selected' : ''}
          on:click={() => selectTask(task)}
        >
          {task.title}
        </li>
      {/each}
    </ul>
  </div>
  <div class="task-details">
    <h2>When?</h2>
    {#if selected}
      <p>{getDate($date)}</p>
      <p>{getTime($date)}</p>
    {/if}
  </div>
</div>

<style>
  .task-view {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    width: 300px;
    border: 2px solid #4f4f4f;
    border-radius: 8px;
    padding: 16px;
  }
  li {
    padding: 4px 8px;
  }
  li.selected {
    background-color: lightcyan;
  }
  li:hover {
    background-color: lightgray;
  }
</style>

The component will receive a list of tasks with a title and date, and then we'll create a transition between these dates when clicking any of them. (Look at how we auto-subscribe to the reactive value, prepending the variable name with $)

Let's update the App to use this component.

<script lang="ts">
  import Tasks from './lib/Tasks.svelte';

  let tasks = [
    { id: 1, title: 'Meeting', date: new Date('2021-12-17T03:24:00') },
    { id: 2, title: 'Gym', date: new Date('2021-08-22T09:12:00') },
    { id: 3, title: 'Movie', date: new Date('2021-09-01T22:07:00') },
  ];
</script>

<main>
  <Tasks {tasks} />
</main>
<!-- ... -->

And the result looks like this: anim01

In this example we are animating the value, but we can also apply these changes to CSS properties.

Let's create another example that achieves this. (Tweened.svelte).

<script>
  import { tweened } from 'svelte/motion';
  import { cubicOut } from 'svelte/easing';

  const toColor = tweened([255, 0, 0], {
    duration: 2000,
    easing: cubicOut,
  });

  let loop = () =>
    toColor
      .set([255, 0, 0])
      .then(() => toColor.set([0, 255, 0]))
      .then(() => toColor.set([0, 0, 255]))
      .then(() => loop());
  loop();
</script>

<div style={'background-color:rgb(' + $toColor.join(',') + ')'} />

<style>
  div {
    display: block;
    width: 100px;
    height: 100px;
  }
</style>

Here, we created a single div, and use tweened to interpolate values of an array.

When we set the value using the set function, it will return a promise that resolves when the final value is reached (for our purposes, the animation has ended). Then, we trigger a new value using set again. We can see in action how we can interpolate array values.

We must remember to update our application

<script lang="ts">
// ...
  import Tweened from './lib/Tweened.svelte';
// ...
</script>

<main>
  <!-- ... -->
  <Tweened />
</main>
anim02

The possible parameters for tweened are: delay (time before starting), duration (in milliseconds), easing (one of the easing functions shown before), interpolate (a (from, to) => t => value) function

Spring

Spring works differently to transition a variable from one value to another. We can set three parameters: stiffness, damping, which will set how the spring behaves while settling in the final value, and precision, which will determine when the value is considered settled.

Let's create a new component named Spring.svelte

<script>
	import { spring } from 'svelte/motion';

	const number = spring(0,{
	stiffness: 0.1,
	damping: 0.08
});

function changeValueTo(newValue) {
	number.set(newValue)
}

function resetValue() {
	number.set(0, {hard:true})
}

</script>

<div>
	<span>{$number.toFixed(1)}</span>
	<button on:click={() => changeValueTo(10)}>To 10</button>
	<button on:click={() => changeValueTo(100)}>To 100</button>
	<button on:click={() => changeValueTo(1000)}>To 1000</button>
	<button	 on:click={() => resetValue()}>Reset</button>
</div>

<style>
	div {
		display: flex;
		flex-direction:column;
		max-width:300px;
	}
</style>

Our component has a number reactive value that will bounce when changed until finally settling in the desired result. The larger the distance to the target value, the more it will bounce.

We need to update our app to import the component.

<script lang="ts">
// ...
  import Spring from './lib/Spring.svelte';
// ...
</script>

<main>
  <!-- ... -->
  <Spring />
</main>

This is what the final result looks like. anim03

svelte/transition

A transition is a function with the following signature:

(node: HTMLElement, params: any) => {
	delay?: number,
	duration?: number,
	easing?: (t: number) => number,
	css?: (t: number, u: number) => string,
	tick?: (t: number, u: number) => void
}

The svelte/transition module includes a series of functions that will let us animate our DOM: blur, draw, fade, fly, scale, slide and crossfade(this last function returns two transition functions)

They are used with the transition, in, or out directives. Transition is executed when the element enters or leaves the DOM. Four events are available with this directive introstart, introend, outrostart, outroend they are triggered whenever the initial or final animations start and end.

The in and out directives work like transition, but they only act when either the element is added or removed.

Create a new component named Transition.svelte.

<script lang="ts">
  import { onDestroy, onMount } from 'svelte';
  import {
    blur,
    crossfade,
    draw,
    fade,
    fly,
    scale,
    slide,
  } from 'svelte/transition';

  let show = false;
  let interval;

  let [from, to] = crossfade({
    fallback: () => {
      return { css: (t, u) => 'color:red' };
    },
  });

  onMount(() => {
    interval = setInterval(() => {
      show = !show;
    }, 2000);
  })

  onDestroy(() => {
	if (interval) {
	  clearInterval(interval);
    }  
  });
</script>

<div class="playground">
  <div class="transition-item">
    <svg
      fill="#ffffff"
      width="32"
      height="32"
      viewBox="0 0 16 16"
      xmlns="http://www.w3.org/2000/svg"
    >
      {#if show}
        <path
          in:draw={{ duration: 1500 }}
          d="M1.414213562373095 0 16 14.585786437626904 L14.585786437626904 16 L0 1.414213562373095"
        />
        <path
          in:draw={{ duration: 1500 }}
          d="M14.585786437626904 0 L16 1.414213562373095 L1.414213562373095 16 L0 14.585786437626904"
        />
      {/if}
    </svg>
  </div>
  <div class="transition-item teleport">
    <div>
      {#if show}
        <span in:from={{ key: 'a' }} out:to={{ key: 'a' }}>cross...</span>
      {/if}
    </div>
    <div>
      {#if !show}
        <span in:from={{ key: 'a' }} out:to={{ key: 'a' }}>...fade</span>
      {/if}
    </div>
  </div>

  {#if show}
    <div class="transition-item" transition:blur>
      <span>Blur</span>
    </div>
    <div class="transition-item" transition:fade>
      <span>Fade</span>
    </div>
    <div class="transition-item" transition:fly={{ x: 30 }}>
      <span>Fly</span>
    </div>
    <div class="transition-item" transition:scale={{ start: 10 }}>
      <span>Scale</span>
    </div>
    <div class="transition-item" transition:slide>
      <span>Slide</span>
    </div>
  {/if}
</div>

<style>
  .teleport {
    display: flex;
    flex-direction: row;
    justify-content: center;
    width: 200px;
    margin-left:auto;
    margin-right:auto;
    border: 2px solid #4f4f4f;
    border-radius: 8px;
    padding: 16px;

  }
  .teleport > div {
      width: 100px;
    }

  svg {
    height: 128px;
    width: 128px;
  }

  path {
    stroke: black;
  }

  .transition-item + .transition-item {
    margin-top: 40px;
  }
</style>

I've added all of the provided animations to this example, so you can play around with them. anim07

Custom transitions

We can create custom transitions by creating a function that accepts an HTML element, and a configuration object, and returns an object with the required properties.

We'll create a new function called skew.

export function skew(node: HTMLElement, {delay = 0, duration = 1000, easing = cubicInOut, deg = 45} = {}) {
        const style = getComputedStyle(node);
        const target_opacity = +style.opacity;
        const transform = style.transform === 'none' ? '' : style.transform;
        return {
            delay,
            duration,
            easing,
            css: (_t, u) => `
                transform: ${transform} skew(${deg * u}deg);
                opacity: ${target_opacity * _t}
            `
        };
}

delay, duration, and easing are pretty standard to all shipped functions, so we'll keep them the same for ease of use. The magic happens in our css property. Based on our parameters, we will add the skew transform. u is nothing but 1-_t, so in this case, we will start from deg (skew is applied) to 0 (no skew) when the element is shown. The opposite will happen when removed.

Let's test it by creating a new component. (Skew.svelte)

<script lang="ts">
  import { onDestroy, onMount } from 'svelte';
  import { skew } from './skew';

  export let skewOptions = {};

  let show = false;
  let interval;

  onMount(() => {
    interval = setInterval(() => {
      show = !show;
    }, 2000);
  });

  onDestroy(() => {
    if (interval) {
      clearInterval(interval);
    }
  });
</script>

<div class="playground">
  {#if show}
    <div class="transition-item" transition:skew={skewOptions}>
      <span>Skew</span>
    </div>
  {/if}
</div>
anim06

svelte/animate

This package exports a single function: flip. Animations are to be used with the animate directive.

Note that there is a requirement to use this directive. The element that uses the animate directive must be the immediate child of a keyed each block.

Animations are triggered when the elements of an each block are reordered.

The signature of an animation is:

(node: HTMLElement, { from: DOMRect, to: DOMRect } , params: any) => {
	delay?: number,
	duration?: number,
	easing?: (t: number) => number,
	css?: (t: number, u: number) => string,
	tick?: (t: number, u: number) => void
}

As you can see the signature is very similar to those of the transitions. We'll make use of this similarity later.

Create a new component to test what flip and the animate directive do.

<!-- Flip.svelte -->

<script lang="ts">
  import { flip } from 'svelte/animate';
  let things = [
    { id: 1, name: 'foo', ready: true },
    { id: 2, name: 'bar', ready: false },
    { id: 3, name: 'baz', ready: true },
    { id: 4, name: 'fizz', ready: false },
  ];
  let sortBy = { field: 'id', order: 'DESC' };

  let sortedThings = things;

  function sortById() {
    if (
      sortBy.field !== 'id' ||
      (sortBy.field === 'id' && sortBy.order === 'DESC')
    ) {
      sortedThings = things.sort((a, b) => {
        if (a.id > b.id) {
          return 1;
        } else if (a.id < b.id) {
          return -1;
        }
        return 0;
      });
      sortBy = { field: 'id', order: 'ASC' };
    } else {
      sortedThings = things.sort((a, b) => {
        if (a.id > b.id) {
          return -1;
        } else if (a.id < b.id) {
          return 1;
        }
        return 0;
      });
      sortBy = { field: 'id', order: 'DESC' };
    }
  }
  function sortByName() {
    if (
      sortBy.field !== 'name' ||
      (sortBy.field === 'name' && sortBy.order === 'DESC')
    ) {
      sortedThings = things.sort((a, b) => {
        if (a.name > b.name) {
          return 1;
        } else if (a.name < b.name) {
          return -1;
        }
        return 0;
      });
      sortBy = { field: 'name', order: 'ASC' };
    } else {
      sortedThings = things.sort((a, b) => {
        if (a.name > b.name) {
          return -1;
        } else if (a.name < b.name) {
          return 1;
        }
        return 0;
      });
      sortBy = { field: 'name', order: 'DESC' };
    }
  }
  function sortByReadyState() {
    if (
      sortBy.field !== 'ready' ||
      (sortBy.field === 'ready' && sortBy.order === 'DESC')
    ) {
      sortedThings = [
        ...sortedThings.filter((x) => x.ready),
        ...sortedThings.filter((x) => !x.ready),
      ];
      sortBy = { field: 'ready', order: 'ASC' };
    } else {
      sortedThings = [
        ...sortedThings.filter((x) => !x.ready),
        ...sortedThings.filter((x) => x.ready),
      ];
      sortBy = { field: 'ready', order: 'DESC' };
    }
  }
</script>

<div class="container">
  <table>
    <tr>
      <th on:click={sortById}>id</th>
      <th on:click={sortByName}>name</th>
      <th on:click={sortByReadyState}>ready</th>
    </tr>
    {#each sortedThings as thing (thing.id)}
      <tr animate:flip>
        <td>{thing.id}</td>
        <td>
          {thing.name}
        </td>
        <td><input type="checkbox" bind:checked={thing.ready} /></td>
      </tr>
    {/each}
  </table>
</div>

<style>
  td {
    width: 100px;
  }
  .container {
    width: 100vw;
    display: flex;
    flex-direction: row;
  }
  table,
  tr,
  td,
  th {
    border: 1px solid gray;
    border-collapse: collapse;
  }
  th {
    cursor: pointer;
  }
</style>

We created a table with 4 rows, and the ability to order rows by different properties.

The elements are inside a keyed each block (remember this is a requirement). One of the cool things about the animate directive is that only the items that change will be animated. The rest will remain like they were.

The result looks like this. anim10

extending and reusing animations with transitions

Because transitions and animations are so similar, we can use transitions to extend flip or create new animations.

animations from transitions

If we look at both types of functions, we can see that we are able to create a wrapper function to convert our transition into an animation.

export function toAnimation<T>(
  fn: (node: HTMLElement, params) => T,
): (node: HTMLElement, { from, to }, params) => T {
  return (node, _animations, params = {}) => {
    return fn(node, params);
  };
}

Then, we can convert one of our transitions, and apply it with the animate directive.

<!--AnimationFromTransition.svelte -->
<script>
  import { fade } from 'svelte/transition';
  import { toAnimation } from './toAnimation';

  let fadeAnimation = toAnimation(fade);

 // ... same as Flip.svelte
</script>

<div class="container">
  <table>
    <tr>
      <th on:click={sortById}>id</th>
      <th on:click={sortByName}>name</th>
      <th on:click={sortByReadyState}>ready</th>
    </tr>
    {#each sortedThings as thing (thing.id)}
      <tr animate:fadeAnimation={{ duration: 400 }}>
        <td>{thing.id}</td>
        <td>
          {thing.name}
        </td>
        <td><input type="checkbox" bind:checked={thing.ready} /></td>
      </tr>
    {/each}
  </table>
</div>

<style>
  /* same as Flip.svelte*/
</style>
anim11

Now, instead of moving, reordered elements fade in/out.

Extending flip

We can also extend the flip animation with transitions. I'll create a wrapper function again.

// extendFlip.ts

import { flip } from 'svelte/animate';
export function extendFlip(fn) {
  return (node, animations, params = {}) => {
    let flipRes = flip(node, animations, params);
    let transitionRes = fn(node, params);

    let getTransform = (str) => {
      let results = str.match(/transform: (.*);/);
      if (results && results.length) {
        return results[results.length - 1];
      }
      return '';
    };

    let mergeTransform = (css1, css2) => {
      return `transform: ${getTransform(css1)} ${getTransform(css2)};`;
    };

    return {
      ...flipRes,
      css: (t, u) =>
        `${transitionRes.css(t, u)}; ${mergeTransform(
          flipRes.css(t, u),
          transitionRes.css(t, u),
        )};`,
    };
  };
}

Our function will get the transition function, and merge the transform property it returns with the one from flip.

Now, let's look at a slightly modified version of the previous component:

<script>
  import { scale, blur } from 'svelte/transition';
  import { extendFlip } from './extendFlip';

  let flipAndBlur = extendFlip(blur);
  let flipAndScale = extendFlip(blur);

  let things = [
    { id: 1, name: 'foo', ready: true },
    { id: 2, name: 'bar', ready: false },
    { id: 3, name: 'baz', ready: true },
    { id: 4, name: 'fizz', ready: false },
  ];

  let sortBy = { field: 'id', order: 'DESC' };

  let sortedThings = things;

  function sortById() {
    if (
      sortBy.field !== 'id' ||
      (sortBy.field === 'id' && sortBy.order === 'DESC')
    ) {
      sortedThings = things.sort((a, b) => {
        if (a.id > b.id) {
          return 1;
        } else if (a.id < b.id) {
          return -1;
        }
        return 0;
      });
      sortBy = { field: 'id', order: 'ASC' };
    } else {
      sortedThings = things.sort((a, b) => {
        if (a.id > b.id) {
          return -1;
        } else if (a.id < b.id) {
          return 1;
        }
        return 0;
      });
      sortBy = { field: 'id', order: 'DESC' };
    }
  }
  function sortByName() {
    if (
      sortBy.field !== 'name' ||
      (sortBy.field === 'name' && sortBy.order === 'DESC')
    ) {
      sortedThings = things.sort((a, b) => {
        if (a.name > b.name) {
          return 1;
        } else if (a.name < b.name) {
          return -1;
        }
        return 0;
      });
      sortBy = { field: 'name', order: 'ASC' };
    } else {
      sortedThings = things.sort((a, b) => {
        if (a.name > b.name) {
          return -1;
        } else if (a.name < b.name) {
          return 1;
        }
        return 0;
      });
      sortBy = { field: 'name', order: 'DESC' };
    }
  }
  function sortByReadyState() {
    if (
      sortBy.field !== 'ready' ||
      (sortBy.field === 'ready' && sortBy.order === 'DESC')
    ) {
      sortedThings = [
        ...sortedThings.filter((x) => x.ready),
        ...sortedThings.filter((x) => !x.ready),
      ];
      sortBy = { field: 'ready', order: 'ASC' };
    } else {
      sortedThings = [
        ...sortedThings.filter((x) => !x.ready),
        ...sortedThings.filter((x) => x.ready),
      ];
      sortBy = { field: 'ready', order: 'DESC' };
    }
  }
</script>

<div class="container">
  <table>
    <tr>
      <th on:click={sortById}>id</th>
      <th on:click={sortByName}>name</th>
      <th on:click={sortByReadyState}>ready</th>
    </tr>
    {#each sortedThings as thing (thing.id)}
      <tr animate:flipAndBlur>
        <td>{thing.id}</td>
        <td>
          {thing.name}
        </td>
        <td><input type="checkbox" bind:checked={thing.ready} /></td>
      </tr>
    {/each}
  </table>
</div>

<style>
  td {
    width: 100px;
  }
  .container {
    width: 100vw;
    display: flex;
    flex-direction: row;
  }
  table,
  tr,
  td,
  th {
    border: 1px solid gray;
    border-collapse: collapse;
  }
  th {
    cursor: pointer;
  }
</style>

And the results:

Blur + flip anim12

Scale + flip anim13

Final words

Svelte did a great job making animations and transitions easy with their API. The provided functions work great in many scenarios.

I hope this blog post invites you to explore the API, extend what's already there, and share it with other users. These examples are available in this repo.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

Understanding Sourcemaps: From Development to Production cover image

Understanding Sourcemaps: From Development to Production

What Are Sourcemaps? Modern web development involves transforming your source code before deploying it. We minify JavaScript to reduce file sizes, bundle multiple files together, transpile TypeScript to JavaScript, and convert modern syntax into browser-compatible code. These optimizations are essential for performance, but they create a significant problem: the code running in production does not look like the original code you wrote. Here's a simple example. Your original code might look like this: ` After minification, it becomes something like this: ` Now imagine trying to debug an error in that minified code. Which line threw the exception? What was the value of variable d? This is where sourcemaps come in. A sourcemap is a JSON file that contains a mapping between your transformed code and your original source files. When you open browser DevTools, the browser reads these mappings and reconstructs your original code, allowing you to debug with variable names, comments, and proper formatting intact. How Sourcemaps Work When you build your application with tools like Webpack, Vite, or Rollup, they can generate sourcemap files alongside your production bundles. A minified file references its sourcemap using a special comment at the end: ` The sourcemap file itself contains a JSON structure with several key fields: ` The mappings field uses an encoding format called VLQ (Variable Length Quantity) to map each position in the minified code back to its original location. The browser's DevTools use this information to show you the original code while you're debugging. Types of Sourcemaps Build tools support several variations of sourcemaps, each with different trade-offs: Inline sourcemaps: The entire mapping is embedded directly in your JavaScript file as a base64 encoded data URL. This increases file size significantly but simplifies deployment during development. ` External sourcemaps: A separate .map file that's referenced by the JavaScript bundle. This is the most common approach, as it keeps your production bundles lean since sourcemaps are only downloaded when DevTools is open. Hidden sourcemaps: External sourcemap files without any reference in the JavaScript bundle. These are useful when you want sourcemaps available for error tracking services like Sentry, but don't want to expose them to end users. Why Sourcemaps During development, sourcemaps are absolutely critical. They will help avoid having to guess where errors occur, making debugging much easier. Most modern build tools enable sourcemaps by default in development mode. Sourcemaps in Production Should you ship sourcemaps to production? It depends. While security by making your code more difficult to read is not real security, there's a legitimate argument that exposing your source code makes it easier for attackers to understand your application's internals. Sourcemaps can reveal internal API endpoints and routing logic, business logic, and algorithmic implementations, code comments that might contain developer notes or TODO items. Anyone with basic developer tools can reconstruct your entire codebase when sourcemaps are publicly accessible. While the Apple leak contained no credentials or secrets, it did expose their component architecture and implementation patterns. Additionally, code comments can inadvertently contain internal URLs, developer names, or company-specific information that could potentially be exploited by attackers. But that’s not all of it. On the other hand, services like Sentry can provide much more actionable error reports when they have access to sourcemaps. So you can understand exactly where errors happened. If a customer reports an issue, being able to see the actual error with proper context makes diagnosis significantly faster. If your security depends on keeping your frontend code secret, you have bigger problems. Any determined attacker can reverse engineer minified JavaScript. It just takes more time. Sourcemaps are only downloaded when DevTools is open, so shipping them to production doesn't affect load times or performance for end users. How to manage sourcemaps in production You don't have to choose between no sourcemaps and publicly accessible ones. For example, you can restrict access to sourcemaps with server configuration. You can make .map accessible from specific IP addresses. Additionally, tools like Sentry allow you to upload sourcemaps during your build process without making them publicly accessible. Then configure your build to generate sourcemaps without the reference comment, or use hidden sourcemaps. Sentry gets the mapping information it needs, but end users can't access the files. Learning from Apple's Incident Apple's sourcemap incident is a valuable reminder that even the largest tech companies can make deployment oversights. But it also highlights something important: the presence of sourcemaps wasn't actually a security vulnerability. This can be achieved by following good security practices. Never include sensitive data in client code. Developers got an interesting look at how Apple structures its Svelte codebase. The lesson is that you must be intentional about your deployment configuration. If you're going to include sourcemaps in production, make that decision deliberately after considering the trade-offs. And if you decide against using public sourcemaps, verify that your build process actually removes them. In this case, the public repo was quickly removed after Apple filed a DMCA takedown. (https://github.com/github/dmca/blob/master/2025/11/2025-11-05-apple.md) Making the Right Choice So what should you do with sourcemaps in your projects? For development: Always enable them. Use fast options, such as eval-source-map in Webpack or the default configuration in Vite. The debugging benefits far outweigh any downsides. For production: Consider your specific situation. But most importantly, make sure your sourcemaps don't accidentally expose secrets. Review your build output, check for hardcoded credentials, and ensure sensitive configurations stay on the backend where they belong. Conclusion Sourcemaps are powerful development tools that bridge the gap between the optimized code your users download and the readable code you write. They're essential for debugging and make error tracking more effective. The question of whether to include them in production doesn't have a unique answer. Whatever you decide, make it a deliberate choice. Review your build configuration. Verify that sourcemaps are handled the way you expect. And remember that proper frontend security doesn't come from hiding your code. Useful Resources * Source map specification - https://tc39.es/ecma426/ * What are sourcemaps - https://web.dev/articles/source-maps * VLQ implementation - https://github.com/Rich-Harris/vlq * Sentry sourcemaps - https://docs.sentry.io/platforms/javascript/sourcemaps/ * Apple DMCA takedown - https://github.com/github/dmca/blob/master/2025/11/2025-11-05-apple.md...

Incremental Hydration in Angular cover image

Incremental Hydration in Angular

Incremental Hydration in Angular Some time ago, I wrote a post about SSR finally becoming a first-class citizen in Angular. It turns out that the Angular team really treats SSR as a priority, and they have been working tirelessly to make SSR even better. As the previous blog post mentioned, full-page hydration was launched in Angular 16 and made stable in Angular 17, providing a great way to improve your Core Web Vitals. Another feature aimed to help you improve your INP and other Core Web Vitals was introduced in Angular 17: deferrable views. Using the @defer blocks allows you to reduce the initial bundle size and defer the loading of heavy components based on certain triggers, such as the section entering the viewport. Then, in September 2024, the smart folks at Angular figured out that they could build upon those two features, allowing you to mark parts of your application to be server-rendered dehydrated and then hydrate them incrementally when needed - hence incremental hydration. I’m sure you know what hydration is. In short, the server sends fully formed HTML to the client, ensuring that the user sees meaningful content as quickly as possible and once JavaScript is loaded on the client side, the framework will reconcile the rendered DOM with component logic, event handlers, and state - effectively hydrating the server-rendered content. But what exactly does "dehydrated" mean, you might ask? Here's what will happen when you mark a part of your application to be incrementally hydrated: 1. Server-Side Rendering (SSR): The content marked for incremental hydration is rendered on the server. 2. Skipped During Client-Side Bootstrapping: The dehydrated content is not initially hydrated or bootstrapped on the client, reducing initial load time. 3. Dehydrated State: The code for the dehydrated components is excluded from the initial client-side bundle, optimizing performance. 4. Hydration Triggers: The application listens for specified hydration conditions (e.g., on interaction, on viewport), defined with a hydrate trigger in the @defer block. 5. On-Demand Hydration: Once the hydration conditions are met, Angular downloads the necessary code and hydrates the components, allowing them to become interactive without layout shifts. How to Use Incremental Hydration Thanks to Mark Thompson, who recently hosted a feature showcase on incremental hydration, we can show some code. The first step is to enable incremental hydration in your Angular application's appConfig using the provideClientHydration provider function: ` Then, you can mark the components you want to be incrementally hydrated using the @defer block with a hydrate trigger: ` And that's it! You now have a component that will be server-rendered dehydrated and hydrated incrementally when it becomes visible to the user. But what if you want to hydrate the component on interaction or some other trigger? Or maybe you don't want to hydrate the component at all? The same triggers already supported in @defer blocks are available for hydration: - idle: Hydrate once the browser reaches an idle state. - viewport: Hydrate once the component enters the viewport. - interaction: Hydrate once the user interacts with the component through click or keydown triggers. - hover: Hydrate once the user hovers over the component. - immediate: Hydrate immediately when the component is rendered. - timer: Hydrate after a specified time delay. - when: Hydrate when a provided conditional expression is met. And on top of that, there's a new trigger available for hydration: - never: When used, the component will remain static and not hydrated. The never trigger is handy when you want to exclude a component from hydration altogether, making it a completely static part of the page. Personally, I'm very excited about this feature and can't wait to try it out. How about you?...

What is Cypress Studio? cover image

What is Cypress Studio?

Introduction Cypress Studio has been around for some time. It was introduced in Cypress v6.3.0, removed in v10, and reintroduced in v10.7.0. This blog post will dive into its current state and how to use it today. What is Cypress Studio Cypress Studio is a tool built on top of Cypress. It offers an interface for creating tests as if you were using your site without requiring code to be written. It adds functionality to query elements and to add assertions from the interface. It's important to note that Cypress Studio is still under an experimental flag and lacks support for Component Testing (another feature of Cypress). Creating an app to test We first need a working app because the tool we will use is meant for end-to-end tests. I'll pick Svelte and Vite for this demo, but you can choose any framework. Cypress is agnostic in that matter. ` Make sure to select the Svelte and Typescript options to follow along. Now open your project location, install dependencies, and start your app to ensure everything works correctly. ` Installing and setting up Cypress Studio Now it's time to add Cypress to our dev dependencies. ` With our dependency installed, let's launch it so we can start configuring it. But first let’s add an entry under scripts in our package.json file. ` ` A new window will open that will guide us through the initial setup. Select the "E2E Testing" option to create the required configuration files. Then, you can close the window. A new folder called cypress and a configuration file cypress.config.ts are created at the root of our project. To enable Cypress Studio, we must open the newly created configuration file and add the experimentalStudio property. ` For this project, we need to ensure that TypeScript is configured properly for Cypress when it runs, as it conflicts with the one in our root folder. We will extend the original typescript configuration and override some properties. Create a new tsconfig.json file inside the' cypress' folder. ` You can find more information on setting up TypeScript with Cypress in their documentation Creating tests Now that our setup is complete let's write our first test. We will create a folder called e2e and a new test file. ` Open the new file and create an outline of your tests. Don't add anything to them. I initialized my file with a few tests. ` There is an option to create spec files from the interface, but it will contain some initial content that you most likely remove. There’s third option to scaffold multiple specs files, these can serve as a learning resource for writing tests as it contains many different scenarios tested. Make sure your app is running and, in another terminal, start cypress. ` Select E2E testing and Chrome. You should now see a list with all your test files. Click on home.cy.ts. Your tests will run and pass because we have not made any assertions. This looks the same as if we haven't enabled Studio, but there's a new detail added when you hover on any of the tests: a magic wand that you can click to start recording events and adding assertions directly in the UI. Let's start with our first tests and check what assertions we can make. The first step is to navigate to a page. This interaction will be recorded and added to the test. To make assertions, right-click on an element and select the ones you want to add. After we complete our test, let's review our generated code. Go to the test file and see the changes made to it. It should look something like this. ` Cypress Studio will attempt to pick the best selector for the element. In this case, it was able to choose button because it is unique to the page. If there were more buttons, the selector would've been different. You can interact with elements as if you were using the page regularly. Note that Studio supports a limited set of commands: check, click, select, type, and uncheck. Let's record our second test and verify that the button will increase its count when clicked. That was quick. Let's review our generated test. ` So far, our tests have been very accurate and didn't need any modifications. However, take Studio as a helper to write tests visually and their selector as suggestions. Let's take the bottom link with the text "SvelteKit" as an example. It will generate the assertion: ` This selector is accurate, but modifying the elements' structure or order would break the tests. We don’t want tests to break for reasons that do not affect our users. A change in order does not have the same impact for the user as changing the text of a button or changing the url of a link. These selectors should be as specific as possible, but rely as less as possible on implementation details. Selecting images by its alt attribute it’s a way to find a specific element and at the same time ensure that element is accessible. (Accessibility has a direct impact on users) As a counterpart, when clicking any of the logos above, we will get very interesting selectors (and assertions). ` With these assertions, we check that our images have an alt attribute set and are tied to a specified link. Conclusion As with any other automated tool, check the output and solve any issues that you may find. Results depend on the structure of the page and Cypress Studio's ability to choose an adequate selector. Overall, this tool can still help you write complex tests with plenty of interactions, and you can later modify any selectors. Besides the selector, I found it helpful in writing the assertions. If you’re new to Cypress or e2e testing, it can be of great help to go from an idea to a working test. Reviewing these tests can be a great way to learn how these tests are built....

Vercel BotID: The Invisible Bot Protection You Needed cover image

Vercel BotID: The Invisible Bot Protection You Needed

Nowadays, bots do not act like “bots”. They can execute JavaScript, solve CAPTCHAs, and navigate as real users. Traditional defenses often fail to meet expectations or frustrate genuine users. That’s why Vercel created BotID, an invisible CAPTCHA that has real-time protections against sophisticated bots that help you protect your critical endpoints. In this blog post, we will explore why you should care about this new tool, how to set it up, its use cases, and some key considerations to take into account. We will be using Next.js for our examples, but please note that this tool is not tied to this framework alone; the only requirement is that your app is deployed and running on Vercel. Why Should You Care? Think about these scenarios: - Checkout flows are overwhelmed by scalpers - Signup forms inundated with fake registrations - API endpoints draining resources with malicious requests They all impact you and your users in a negative way. For example, when bots flood your checkout page, real customers are unable to complete their purchases, resulting in your business losing money and damaging customer trust. Fake signups clutter the app, slowing things down and making user data unreliable. When someone deliberately overloads your app’s API, it can crash or become unusable, making users angry and creating a significant issue for you, the owner. BotID automatically detects and filters bots attempting to perform any of the above actions without interfering with real users. How does it work? A lightweight first-party script quickly gathers a high set of browser & environment signals (this takes ~30ms, really fast so no worry about performance issues), packages them into an opaque token, and sends that token with protected requests via the rewritten challenge/proxy path + header; Vercel’s edge scores it, attaches a verdict, and checkBotId() function simply reads that verdict so your code can allow or block. We will see how this is implemented in a second! But first, let’s get started. Getting Started in Minutes 1. Install the SDK: ` 1. Configure redirects Wrap your next.config.ts with BotID’s helper. This sets up the right rewrites so BotID can do its job (and not get blocked by ad blockers, extensions, etc.): ` 2. Integrate the client on public-facing pages (where BotID runs checks): Declare which routes are protected so BotID can attach special headers when a real user triggers those routes. We need to create instrumentation-client.ts (place it in the root of your application or inside a src folder) and initialize BotID once: ` instrumentation-client.ts runs before the app hydrates, so it’s a perfect place for a global setup! If we have an inferior Next.js version than 15.3, then we would need to use a different approach. We need to render the React component inside the pages or layouts you want to protect, specifying the protected routes: ` 3. Verify requests on your server or API: ` - NOTE: checkBotId() will fail if the route wasn’t listed on the client, because the client is what attaches the special headers that let the edge classify the request! You’re all set - your routes are now protected! In development, checkBotId() function will always return isBot = false so you can build without friction. To disable this, you can override the options for development: ` What happens on a failed check? In our example above, if the check failed, we return a 403, but it is mostly up to you what to do in this case; the most common approaches for this scenario are: - Hard block with a 403 for obviously automated traffic (just what we did in the example above) - Soft fail (generic error/“try again”) when you want to be cautious. - Step-up (require login, email verification, or other business logic). Remember, although rare, false positives can occur, so it’s up to you to determine how you want to balance your fail strategy between security, UX, telemetry, and attacker behavior. checkBotId() So far, we have seen how to use the property isBot from checkBotId(), but there are a few more properties that you can leverage from it. There are: isHuman (boolean): true when BotID classifies the request as a real human session (i.e., a clear “pass”). BotID is designed to return an unambiguous yes/no, so you can gate actions easily. isBot (boolean): We already saw this one. It will be true when the request is classified as automated traffic. isVerifiedBot (boolean): Here comes a less obvious property. Vercel maintains and continuously updates a comprehensive directory of known legitimate bots from across the internet. This directory is regularly updated to include new legitimate services as they emerge. This could be helpful for allowlists or custom logic per bot. We will see an example in a sec. verifiedBotName? (string): The name for the specific verified bot (e.g., “claude-user”). verifiedBotCategory? (string): The type of the verified bot (e.g., “webhook”, “advertising”, “ai_assistant”). bypassed (boolean): it is true if the request skipped BotID check due to a configured Firewall bypass (custom or system). You could use this flag to avoid taking bot-based actions when you’ve explicitly bypassed protection. Handling Verified Bots - NOTE: Handling verified bots is available in botid@1.5.0 and above. It might be the case that you don’t want to block some verified bots because they are not causing damage to you or your users, as it can sometimes be the case for AI-related bots that fetch your site to give information to a user. We can use the properties related to verified bots from checkBotId() to handle these scenarios: ` Choosing your BotID mode When leveraging BotID, you can choose between 2 modes: - Basic Mode: Instant session-based protection, available for all Vercel plans. - Deep Analysis Mode: Enhanced Kasada-powered detection, only available for Pro and Enterprise plan users. Using this mode, you will leverage a more advanced detection and will block the hardest to catch bots To specify the mode you want, you must do so in both the client and the server. This is important because if either of the two does not match, the verification will fail! ` Conclusion Stop chasing bots - let BotID handle them for you! Bots are and will get smarter and more sophisticated. BotID gives you a simple way to push back without slowing your customers down. It is simple to install, customize, and use. Stronger protection equals fewer headaches. Add BotID, ship with confidence, and let the bots trample into a wall without knowing what’s going on....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co