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

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?...

“Music and code have a lot in common,” freeCodeCamp’s Jessica Wilkins on what the tech community is doing right to onboard new software engineers cover image

“Music and code have a lot in common,” freeCodeCamp’s Jessica Wilkins on what the tech community is doing right to onboard new software engineers

Before she was a software developer at freeCodeCamp, Jessica Wilkins was a classically trained clarinetist performing across the country. Her days were filled with rehearsals, concerts, and teaching, and she hadn’t considered a tech career until the world changed in 2020. > “When the pandemic hit, most of my gigs were canceled,” she says. “I suddenly had time on my hands and an idea for a site I wanted to build.” That site, a tribute to Black musicians in classical and jazz music, turned into much more than a personal project. It opened the door to a whole new career where her creative instincts and curiosity could thrive just as much as they had in music. Now at freeCodeCamp, Jessica maintains and develops the very JavaScript curriculum that has helped her and millions of developers around the world. We spoke with Jessica about her advice for JavaScript learners, why musicians make great developers, and how inclusive communities are helping more women thrive in tech. Jessica’s Top 3 JavaScript Skill Picks for 2025 If you ask Jessica what it takes to succeed as a JavaScript developer in 2025, she won’t point you straight to the newest library or trend. Instead, she lists three skills that sound simple, but take real time to build: > “Learning how to ask questions and research when you get stuck. Learning how to read error messages. And having a strong foundation in the fundamentals” She says those skills don’t come from shortcuts or shiny tools. They come from building. > “Start with small projects and keep building,” she says. “Books like You Don’t Know JS help you understand the theory, but experience comes from writing and shipping code. You learn a lot by doing.” And don’t forget the people around you. > “Meetups and conferences are amazing,” she adds. “You’ll pick up things faster, get feedback, and make friends who are learning alongside you.” Why So Many Musicians End Up in Tech A musical past like Jessica’s isn’t unheard of in the JavaScript industry. In fact, she’s noticed a surprising number of musicians making the leap into software. > “I think it’s because music and code have a lot in common,” she says. “They both require creativity, pattern recognition, problem-solving… and you can really get into flow when you’re deep in either one.” That crossover between artistry and logic feels like home to people who’ve lived in both worlds. What the Tech Community Is Getting Right Jessica has seen both the challenges and the wins when it comes to supporting women in tech. > “There’s still a lot of toxicity in some corners,” she says. “But the communities that are doing it right—like Women Who Code, Women in Tech, and Virtual Coffee—create safe, supportive spaces to grow and share experiences.” She believes those spaces aren’t just helpful, but they’re essential. > “Having a network makes a huge difference, especially early in your career.” What’s Next for Jessica Wilkins? With a catalog of published articles, open-source projects under her belt, and a growing audience of devs following her journey, Jessica is just getting started. She’s still writing. Still mentoring. Still building. And still proving that creativity doesn’t stop at the orchestra pit—it just finds a new stage. Follow Jessica Wilkins on X and Linkedin to keep up with her work in tech, her musical roots, and whatever she’s building next. Sticker illustration by Jacob Ashley....

Exploring Open Props and its Capabilities cover image

Exploring Open Props and its Capabilities

Exploring Open Props and its Capabilities With its intuitive approach and versatile features, Open Props empowers you to create stunning designs easily. It has the perfect balance between simplicity and power. Whether you're a seasoned developer or just starting, Open Props makes styling websites a breeze. Let's explore how Open Props can help your web development workflow. What is Open Props Open Props is a CSS library that packs a set of CSS variables for quickly creating consistent components using “Sub-Atomic” Styles. These web design tokens are crafted to help you get great-looking results from the start using consistent naming conventions and providing lots of possibilities out-of-the-box. At the same time, it's customizable and can be gradually adopted. Installing open props There are many ways to get started with open props, each with its advantages. The library can be imported from a CDN or installed using npm. You can import all or specific parts of it, and for greater control of what's bundled or not, you can use PostCSS to include only the variables you used. From Zero to Open Props Let's start with the simplest way to test and use open props. I'll create a simple HTML file with some structure, and we'll start from there. Create an index.html file. ` Edit the content of your HTML file. In this example, we’ll create a landing page containing a few parts: a hero section, a section for describing the features of a service, a section for the different pricing options available and finally a section with a call to action. We’ll start just declaring the document structure. Next we’ll add some styles and finally we’ll switch to using open props variables. ` To serve our page, we could just open the file, but I prefer to use serve, which is much more versatile. To see the contents of our file, let's serve our content. ` This command will start serving our site on port 3000. Our site will look something like this: Open-props core does not contain any CSS reset. After all, it’s just a set of CSS variables. This is a good start regarding the document structure. Adding open props via CDN Let's add open-props to our project. To get started, add: ` This import will make the library's props available for us to use. This is a set of CSS variables. It contains variables for fonts, colors, sizes, and many more. Here is an excerpt of the content of the imported file: ` The :where pseudo-class wraps all the CSS variables declarations, giving them the least specificity. That means you can always override them with ease. This imported file is all you need to start using open props. It will provide a sensible set of variables that give you some constraints in terms of what values you can use, a palette of colors, etc. Because this is just CSS, you can opt-out by not using the variables provided. I like these constraints because they can help with consistency and allow me to focus on other things. At the same time, you can extend this by creating your own CSS variables or just using any value whenever you want to do something different or if the exact value you want is not there. We should include some styles to add a visual hierarchy to our document. Working with CSS variables Let's create a new file to hold our styles. ` And add some styles to it. We will be setting a size hierarchy to headings, using open props font-size variables. Additionally, gaps and margins will use the size variables. ` We can explore these variables further using open-props’ documentation. It's simple to navigate (single page), and consistent naming makes it easy to learn them. Trying different values sometimes involves changing the number at the end of the variable name. For example: font-size-X, where X ranges from 0 to 8 (plus an additional 00 value). Mapped to font-sizes from 0.5rem up to 3.5rem. If you find your font is too small, you can add 1 to it, until you find the right size. Colors range from 0-12: –red-0 is the lightest one (rgb(255, 245, 245)) while –red-12 is the darkest (rgb(125, 26, 26)). There are similar ranges for many properties like font weight, size (useful for padding and margins), line height and shadows, to name a few. Explore and find what best fits your needs. Now, we need to include these styles on our page. ` Our page looks better now. We could keep adding more styles, but we'll take a shortcut and add some defaults with Open Props' built in normalized CSS file. Besides the core of open props (that contains the variables) there’s an optional normalization file that we can use. Let's tweak our recently added styles.css file a bit. Let’s remove the rules for headings. Our resulting css will now look like this. ` And add a new import from open-props. ` Open props provides a normalization file for our CSS, which we have included. This will establish a nice-looking baseline for our styles. Additionally, it will handle light/dark mode based on your preferences. I have dark mode set and the result already looks a lot better. Some font styles and sizes have been set, and much more. More CSS Variables Let's add more styles to our page to explore the variables further. I'd like to give the pricing options a card style. Open Props has a section on borders and shadows that we can use for this purpose. I would also like to add a hover effect to these cards. Also, regarding spacing, I want to add more margins and padding when appropriate. ` With so little CSS added and using many open props variables for sizes, borders, shadows, and easing curves, we can quickly have a better-looking site. Optimizing when using the CDN Open props is a pretty light package; however, using the CDN will add more CSS than you'll probably use. Importing individual parts of these props according to their utility is possible. For example, import just the gradients. ` Or even a subset of colors ` These are some options to reduce the size of your app if using the CDN. Open Props with NPM Open Props is framework agnostic. I want my site to use Vite. Vite is used by many frameworks nowadays and is perfect to show you the next examples and optimizations. ` Let's add a script to our package.json file to start our development server. ` Now, we can start our application on port 5173 (default) by running the following command: ` Your application should be the same as before, but we will change how we import open props. Stop the application and remove the open-props and normalize imports from index.html. Now in your terminal install the open-props package from npm. ` Once installed, import the props and the normalization files at the beginning of your styles.css file. ` Restart your development server, and you should see the same results. Optimizing when using NPM Let's analyze the size of our package. 34.4 kb seems a bit much, but note that this is uncompressed. When compressed with gzip, it's closer to 9 kb. Similar to what we did when using the CDN, we can add individual sections of the package. For example in our CSS file we could import open-props/animations or open-props/sizes. If this concerns you, don't worry; we can do much better. JIT Props To optimize our bundled styles, we can use a PostCSS plugin called posts-jit-props. This package will ensure that we only ship the props that we are using. Vite has support for PostCSS, so setting it up is straightforward. Let's install the plugin: ` After the installation finishes, let's create a configuration file to include it. ` The content of your file should look like this: ` Finally, remove the open-props/style import from styles.css. Remember that this file contains the CSS variables we will add "just in time". Our page should still look the same, but if we analyze the size of our styles.css file again, we can see that it has already been reduced to 13.2kb. If you want to know where this size is coming from, the answer is that Open Props is adding all the variables used in the normalize file + the ones that we require in our file. If we were to remove the normalize import, we would end up with much smaller CSS files, and the number of props added just in time would be minimal. Try removing commenting it out (the open-props/normalize import) from the styles.css file. The page will look different, but it will be useful to show how just the props used are added. 2.4kB uncompressed. That's a lot less for our example. If we take a quick look at our generated file, we can see the small list of CSS variables added from open props at the top of our file (those that we use later on the file). Open props ships with tons of variables for: - Colors - Gradients - Shadows - Aspect Ratios - Typography - Easing - Animations - Sizes - Borders - Z-Index - Media Queries - Masks You probably won't use all of these but it's hard to tell what you'll be using from the beginning of a project. To keep things light, add what you need as you go, or let JIT handle it for you. Conclusion Open props has much to offer and can help speed your project by leveraging some decisions upfront and providing a sensible set of predefined CSS Variables. We've learned how to install it (or not) using different methods and showcased how simple it is to use. Give it a try!...

What does it actually look like to build software with AI today? Not in theory, but in practice. cover image

What does it actually look like to build software with AI today? Not in theory, but in practice.

What does it actually look like to build software with AI today? Not in theory, but in practice. At the Leadership Exchange, this was the question at the center of the Developer Panel, where leaders from across the industry unpacked what’s really changing inside engineering teams and what organizations need to do right now to keep up. The Developer Panel at the Leadership Exchange explored the cutting edge of AI in software engineering and examined what organizations should focus on today to prepare for the future. Moderated by Jeff Cross, Co-Founder & CEO at Nx, the panel featured Victor Savkin, Cofounder & CTO at Nx, Alex Sover, Vice President of Engineering at OpenAP, Brent Zucker, Senior Director of Engineering at Visa, and Jonathan Fontanez, AI Engineering Lead at This Dot Labs. Panelists shared insights into how AI is transforming the software development lifecycle and how teams can adopt tools effectively while preparing for organizational change. Panelists discussed emerging workflows, including CI-in-the-loop, agentic healing, and context engineering. They examined how validation, code reviews, and PRDs are evolving alongside AI capabilities and how teams are integrating external sources such as production traces to improve quality and reliability. The discussion also covered what the next generation of agentic tools might look like and how these capabilities will shape engineering practices in the near future. Adoption of AI comes with challenges. Teams often rely on plugins or extensions without foundational understanding, and individual contributors may fear displacement. Panelists emphasized that education, governance, and skill-building are essential for teams to manage AI agents effectively while maintaining quality. They also highlighted the need to standardize workflows and ensure organizational alignment to fully leverage AI capabilities. The conversation extended beyond technical challenges to organizational implications. Panelists discussed how teams can avoid issues like Conway’s Law, manage distributed teams effectively, and evolve engineering practices alongside AI adoption. Leadership and management strategies play a crucial role in ensuring that AI integration delivers meaningful outcomes while maintaining efficiency and alignment with business objectives. Key Takeaways - AI workflows require both technical and organizational preparation. - Education, governance, and skill development are essential for successful implementation. - Forward-looking teams are rethinking validation, CI pipelines, and context management to fully leverage agentic AI. The discussion highlighted that adopting AI at the cutting edge is not just about new tools - it is about rethinking processes, workflows, and organizational culture. Companies that embrace this holistic approach are most likely to succeed in leveraging AI to its full potential. Are you interested in more conversations like this? Message us for an invite to the next, or for a private discussion around these topics. Tracy can be reached at tlee@thisdot.co....

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