<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Felix</title>
    <description>The latest articles on DEV Community by Felix (@felixrunquist).</description>
    <link>https://dev.to/felixrunquist</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1090467%2Fa9a75e44-5cb3-4b50-86ab-37018ac24030.jpg</url>
      <title>DEV Community: Felix</title>
      <link>https://dev.to/felixrunquist</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/felixrunquist"/>
    <language>en</language>
    <item>
      <title>Locate Website Visitors in Next.js with IP and Supabase</title>
      <dc:creator>Felix</dc:creator>
      <pubDate>Sun, 02 Jun 2024 11:30:34 +0000</pubDate>
      <link>https://dev.to/felixrunquist/locate-website-visitors-in-nextjs-with-ip-and-supabase-p0p</link>
      <guid>https://dev.to/felixrunquist/locate-website-visitors-in-nextjs-with-ip-and-supabase-p0p</guid>
      <description>&lt;p&gt;Using API routes with Next.js as well as an IP address geolocation service is an easy way to display the last visitor of a website. I wanted to use this in a widget on my website homepage. I got this idea after visiting &lt;a href="https://rauno.me/"&gt;Rauno Freiberg's website&lt;/a&gt; – he's a designer at Vercel and I admire his work.&lt;/p&gt;

&lt;p&gt;First of all, I mapped out what needs to be done:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Locating the user&lt;/strong&gt;. There are a few different ways to locate a user on a website, I chose to go with the IP address. This isn't always extremely precise, but it's good enough for getting a rough location to display. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storing user locations in a database&lt;/strong&gt;. There obviously needs to be something on the server side remembering locations of users to display to the next user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;APIs for updating and retrieving the last user's location&lt;/strong&gt;. We also need to think about the order of requests, to make sure that when a user visits the website and the last location is updated, he doesn't get his location back from the database but the location of the previous user. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also had a few priorities for this project: speed and minimal server load. I don't want the last visitor feature to cause a significant increase in page loading times for the client, or to increase server actions beyond what is provided in the Vercel free tier. We'll have a look at how to do this in the best way possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fetching the country from IP
&lt;/h2&gt;

&lt;p&gt;As IP addresses change frequently, and the ranges get reallocated, it's difficult to know which range of addresses maps to which country. Luckily, MaxMind has a free IP lookup tool called &lt;a href="https://dev.maxmind.com/geoip/geolite2-free-geolocation-data"&gt;GeoLite2&lt;/a&gt;, which they update frequently. It's more or less precise, but good enough for what we're trying to do.&lt;/p&gt;

&lt;p&gt;After creating an account and getting a license key, getting the city and country from an IP address is quite easy using the &lt;code&gt;@maxmind/geoip2-node&lt;/code&gt; NPM package. Here's some example code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { WebServiceClient } from '@maxmind/geoip2-node';

const MAXMIND_USER = '1234567';
const MAXMIND_LICENSE = 'Your license here';

const ip = '43.247.156.26'; 
const client = new WebServiceClient(MAXMIND_USER, MAXMIND_LICENSE, {host: 'geolite.info'});
const response = await client.city(ip);
const data = {country: response.country ? response.country.names.en : null, city: response.city ? response.city.names.en : null}
console.log(data)
`}&amp;lt;/code2&amp;gt;

I added some extra logic after noticing that not all IP addresses map to specific cities in the lookup tool.

## Implementing the database

To simplify things in the database, I decided to create a table with `ip`, `city` and `country` columns. Setting the new country is equivalent to adding a new row, instead of updating the last row. This to make it easier to handle the location of the previous user/current user. There's a primary key, `ID`, which will be automatically set.

I decided to go with Supabase, as I've already used their services and am impressed with the ease of use and the excellent Next.js integration.

I created a country table, and I disabled Row-Level-Security as this would require some form of authentication, and we're going to do all the database queries on the server-side as you'll see next, so the client will not have access to the database.

![](https://felixrunquist.com/static/5e00a3c789e4bcf773b65f0b802d7b7f6c06be84.png)
_The columns of our views table_

## Adding the middleware and APIs

In order to fully separate the user from the database, I decided to perform all database queries on the server, and handle the country updating logic using Next.js middleware. This has the added benefit of not sending any additional client requests to log the users' country, which lightens the client-side stack.

To retrieve the country, instead of using `getServerSideProps`, which would make each page dynamic and block it from loading until the database has responded, we do this on the client side through an API route. Need a refresher on Next.js page generation? [Have a look at this post](https://felixrunquist.com/posts/generating-sitemap-rss-feed-with-next-js).

Let's map this out:

![](https://felixrunquist.com/static/3d59e45fdc8d8f4672541d8819d5205f2b85f12f.png%20)
_A diagram of the different requests to the server and database_

### Middleware

Let's first look at the middleware: It makes a request to an API endpoint. For added security, we can prevent the client from interfering with the `set-last-country` API by adding an API key: a secret that will be shared between the middleware and the API to ensure that only the middleware is able to update the country.

To prevent the same user from constantly setting their country as the last, for example if they navigate across multiple pages, we can set a session cookie from the middleware to prevent subsequent requests from triggering the country update.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;import { NextRequest, NextResponse } from 'next/server'&lt;/p&gt;

&lt;p&gt;import { SITE_PATH, COUNTRY_SET_KEY, VIEW_SET_KEY } from '@/lib/constants'&lt;/p&gt;

&lt;p&gt;export const config = {&lt;br&gt;
  matcher: ['/:path', '/posts/:path*', '/pages/:path*', '/creations/:path*']&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;export async function middleware(req) {&lt;br&gt;
  const res = NextResponse.next()&lt;/p&gt;

&lt;p&gt;//check if there is no cookie set&lt;br&gt;
  if(!req.cookies.has('country')){&lt;br&gt;
    res.cookies.set('country', 'true')&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const forwarded = req.headers.get("x-forwarded-for")
var ip = forwarded ? forwarded.split(/, /)[0] : req.headers.get("x-real-ip")
try {
  fetch('https://' + req.headers.get('host') + '/api/set-last-country', {
    method: 'POST',
    body: JSON.stringify({ip, key: COUNTRY_SET_KEY})
  })
} catch(error){
  console.log(error)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;}&lt;br&gt;
  return res&lt;br&gt;
}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
We're using the matcher to only trigger the middleware on certain routes, and the country cookie is used to check if the user's country has already been uploaded to the database during the current session. We then send the IP to the API, as well as the API key mentioned earlier.

You'll also notice that there's no `await` key preceding the fetch, this speeds up loading times since it isn't necessary to wait for the database to update the last country in order to serve the page to the user.

### API setter

In the `set-last-country` API, we'll need to do the following:

- Use geolite2 to get a country and city from the IP address
- Store the country and city in the Supabase database. 

We also need to make sure that the API only accepts POST requests, as it's the method the middleware uses for sending the data.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;import { WebServiceClient } from '@maxmind/geoip2-node';&lt;br&gt;
import { createClient } from '@supabase/supabase-js'&lt;/p&gt;

&lt;p&gt;import { COUNTRY_SET_KEY, MAXMIND_USER, MAXMIND_LICENSE } from '@/lib/constants';&lt;/p&gt;

&lt;p&gt;export default async function handler(req, res) {&lt;/p&gt;

&lt;p&gt;if(req.method != 'POST'){&lt;br&gt;
    console.log("Unauthorized")&lt;br&gt;
    return res.status(403).json({error: 'Unauthorized'})&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;const body = JSON.parse(req.body)&lt;/p&gt;

&lt;p&gt;if(body.key != COUNTRY_SET_KEY){&lt;br&gt;
    console.log("Unauthorized")&lt;br&gt;
    return res.status(403).json({error: 'Unauthorized'})&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;//Get country from body.ip&lt;br&gt;
  const client = new WebServiceClient(MAXMIND_USER, MAXMIND_LICENSE, {host: 'geolite.info'});&lt;br&gt;
  const response = await client.city(body.ip);&lt;br&gt;
  const data = {country: response.country ? response.country.names.en : null, city: response.city ? response.city.names.en : null, ip: body.ip}&lt;/p&gt;

&lt;p&gt;//Send to SB&lt;br&gt;
  const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)&lt;/p&gt;

&lt;p&gt;var { error } = await supabase&lt;br&gt;
  .from('visitors')&lt;br&gt;
  .insert(data)&lt;/p&gt;

&lt;p&gt;res.status(200).json(data)&lt;br&gt;
}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;### API getter

The `get-last-country` is the only API route we want to make available to the client. For this reason, we're not going to be using any API keys. To reduce overhead, caching can be used: since it doesn't really matter if we get the last user, or the 10th last user, we can cache the result for 10 minutes which will reduce server requests as well as database access. Caching on the client and server side can be done using special headers.

We also need to make sure that when the user visits the website and the last location is updated, he doesn't get his location back from the database but the location of the previous user. To do this, we can add an additional column in our `country` table to check if the second to last row has been read or not. If it hasn't been read, we'll return that row, otherwise we'll return the last row. This ensures that the user never gets their location on the first visit, but they might on the next.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;import { createClient } from '@supabase/supabase-js'&lt;/p&gt;

&lt;p&gt;export default async function handler(req, res) {&lt;br&gt;
  res.setHeader('Vercel-CDN-Cache-Control', 'max-age=1200');//Cache for 20 minutes&lt;br&gt;
  res.setHeader('CDN-Cache-Control', 'max-age=600');//Cache for 10 minutes&lt;br&gt;
  res.setHeader('Cache-Control', 'max-age=600');//Cache for 10 minutes&lt;/p&gt;

&lt;p&gt;const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)&lt;br&gt;
  var { data, error } = await supabase&lt;br&gt;
  .from('visitors')&lt;br&gt;
  .select('id, country, city, read')&lt;br&gt;
  .order('id', { ascending: false })&lt;br&gt;
  .limit(2)&lt;/p&gt;

&lt;p&gt;if(!data[1].read){&lt;br&gt;
    var { error } = await supabase&lt;br&gt;
    .from('visitors')&lt;br&gt;
    .update({read: true})&lt;br&gt;
    .eq('id', data[1].id)&lt;br&gt;
    data = data[1]&lt;br&gt;
  }else{&lt;br&gt;
    data = data[0]&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;delete data.read&lt;br&gt;
  delete data.id&lt;br&gt;
  res.status(200).json({cached: false, value: data})&lt;/p&gt;

&lt;p&gt;}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Client-side logic

Now that we've implemented everything on the server side, all that's left to do is to create a component which will display the location of the last visitor:

[View the article](https://felixrunquist.com/posts/locating-last-visitors-in-next-js) for the live preview below:

`/App.js`: 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;import { useEffect, useState } from 'react'&lt;br&gt;
import styles from './lastvisitor.module.scss';&lt;/p&gt;

&lt;p&gt;export default function LastVisitor(){&lt;br&gt;
  const [location, setLocation] = useState("")&lt;br&gt;
  useEffect(() =&amp;gt; {&lt;br&gt;
    getLastVisitor()&lt;br&gt;
  }, [])&lt;br&gt;
  async function getLastVisitor(){&lt;br&gt;
    const res = await fetch('&lt;a href="http://felixrunquist.com/api/get-last-country'"&gt;http://felixrunquist.com/api/get-last-country'&lt;/a&gt;)&lt;br&gt;
    if(res.status == 200){&lt;br&gt;
      const json = await res.json()&lt;br&gt;
      setLocation((json.value.city ? json.value.city + ', ' : "") + json.value.country)&lt;br&gt;
    }&lt;br&gt;
  }&lt;br&gt;
  return (&lt;br&gt;
    &lt;/p&gt;
&lt;br&gt;
      &lt;p&gt;Last visitor: {location}&lt;/p&gt;
&lt;br&gt;
    &lt;br&gt;
  )&lt;br&gt;
}&lt;br&gt;
&lt;code&gt;/lastvisitor.module.scss&lt;/code&gt;:&lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.container {transition: opacity .3s ease-in-out; }
.container.hidden {opacity: 0; }
.container:not(.hidden) {opacity: 1; }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The last visitor element stays visually hidden until the data has been fetched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing notes
&lt;/h2&gt;

&lt;p&gt;That's it, we now have a working last visitor system! Since the priority here is to reduce server load, caching and other features are used to prevent too many requests from being made. This comes with a tradeoff: the last visitor might not always be accurate.&lt;/p&gt;

&lt;p&gt;For additional safeguards against server usage, we could also implement rate limiting using &lt;code&gt;@upstash/ratelimit&lt;/code&gt;, as mentioned in &lt;a href="https://vercel.com/guides/rate-limiting-edge-middleware-vercel-kv"&gt;Next.js documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to dig further into the different methods of generating content in Next.js, I've &lt;a href="https://felixrunquist.com/posts/generating-sitemap-rss-feed-with-next-js"&gt;written an article&lt;/a&gt; which explains how &lt;code&gt;getServerSideProps&lt;/code&gt;, &lt;code&gt;getStaticProps&lt;/code&gt; and &lt;code&gt;revalidate&lt;/code&gt; work.&lt;/p&gt;

&lt;p&gt;Have any questions? Feel free to &lt;a href="https://twitter.com/intent/tweet?via=felixrunquist"&gt;send me a message on Twitter&lt;/a&gt;!&lt;/p&gt;

&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.maxmind.com/geoip/geolite2-free-geolocation-data"&gt;Maxmind documentation&lt;/a&gt;, helpful for translating an IP address to a rough geographical location&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://rauno.me"&gt;Rauno Freiberg's website&lt;/a&gt; with the last visitor counter is what got me inspired to create one!&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.joshwcomeau.com/react/serverless-hit-counter/"&gt;Josh W. Comeau's article&lt;/a&gt; on hit counters explains the integration of serverless functions in static sites&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Credits to &lt;a href="https://app.spline.design/community/file/8cd53f6b-868e-4fad-a00c-42c76ab73557"&gt;Big Loong's template&lt;/a&gt; which was used as a starting point for the article illustration. I made it with Spline, a &lt;a href="https://felixrunquist.com/posts/creating-3d-models-spline-three-js"&gt;tool I discuss in this article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This article was retrieved from &lt;a href="https://felixrunquist.com/"&gt;felixrunquist.com&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Making the Internet More Human</title>
      <dc:creator>Felix</dc:creator>
      <pubDate>Mon, 15 Jan 2024 16:28:30 +0000</pubDate>
      <link>https://dev.to/felixrunquist/making-the-internet-more-human-157c</link>
      <guid>https://dev.to/felixrunquist/making-the-internet-more-human-157c</guid>
      <description>&lt;p&gt;In the early days of the internet, the World Wide Web was a collection of static webpages. Updating these pages required a meticulous process of manually uploading each change to the server as a separate file. Given this long procedure and the sluggish download speeds associated with dial-up modems at the time, it was in everyone’s best interest to deliver information as plainly and efficiently as possible. One couldn’t run the risk of making the user wait a lot of time just to load a webpage full of garbage.&lt;/p&gt;

&lt;p&gt;Then came the technological improvements that led to the internet as we know it today. ADSL and fiber allowed for a thousand-fold increase in speed. Scripting technologies like Javascript and AJAX allowed webpages to seamlessly fetch items from the server helped build complex sites like Google Maps. The problem with the internet of today is that these tools that were built to make navigating the internet faster and easier, have had the precise opposite effect. Driven by counterintuitive design trends and legislation, Javascript has been used extensively to display intrusive pop-ups and to override native scroll behavior. The overhead in data rates has been used to deliver annoying autoplay videos. The agressive expansion big corporations has led to a high proportion of ads silencing what’s important.&lt;/p&gt;

&lt;p&gt;This has impacted people differently depending on revenue and location. On average, bandwidths in urban areas are &lt;a href="https://www.itu.int/en/mediacentre/Pages/pr27-2020-facts-figures-urban-areas-higher-internet-access-than-rural.aspx"&gt;twice as high&lt;/a&gt; as in rural areas. In developing regions such as Africa and the CIS, average internet bandwidth is at &lt;a href="https://www.itu.int/itu-d/reports/statistics/2022/11/24/ff22-international-bandwidth-usage/"&gt;less than a third&lt;/a&gt; of that of developed regions such as Europe. This means that internet-heavy technologies such as autoplaying web video have an even worse impact on users in rural and developing regions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--th5JwvZn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://wp.felixrunquist.com/wp-content/uploads/2024/01/image-4-1024x576.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--th5JwvZn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://wp.felixrunquist.com/wp-content/uploads/2024/01/image-4-1024x576.png" alt="" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The aim of this article is to explore issues in information delivery and to figure out how what we can do to make the internet a better place for everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility
&lt;/h2&gt;

&lt;p&gt;A common design trend on websites has been to use new CSS specifications to drastically change how the website is displayed on the web. The more drastic ones go as far as implementing custom scrollbars (or worse, not displaying &lt;em&gt;any&lt;/em&gt; scrollbars at all!) and scrolling interactions, also known as &lt;em&gt;scroll hijacking&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Most users are familiar with the way their operating system handles common interactions such as scrolling, and they may have customised the settings to their liking. Another problem is that OS implementations are much likelier to accommodate for accessibility, whereas custom implementations often overlook accessibility.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vP0PqqP6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2024/01/Screenshot-2024-01-12-at-4.44.06-PM-1024x947.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vP0PqqP6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2024/01/Screenshot-2024-01-12-at-4.44.06-PM-1024x947.png" alt="" width="800" height="740"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A customised scrollbar on css-tricks.com&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;These recent design trends are part of a bigger problem: the trade for design over function. A good-looking website isn’t necessarily the best website: Regardless of it’s content, a website shouldn’t make users feel disoriented, or even worse, unable to navigate the website.&lt;/p&gt;

&lt;p&gt;I personally looked into ten websites featured on &lt;a href="https://www.awwwards.com/websites/"&gt;Awwwards&lt;/a&gt;, a company that makes “Website Awards that recognize and promote the talent and effort of the best developers, designers and web agencies in the world”. Of the ten websites I visited on desktop,&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;7 implement scroll jacking i.e. the scrolling speed and inertia is different from my OS settings&lt;/li&gt;
&lt;li&gt;2 use a custom cursor i.e. the pointer is not the default one provided by my OS&lt;/li&gt;
&lt;li&gt;1 website uses a custom scrollbar&lt;/li&gt;
&lt;li&gt;1 website displays no scrollbar at all. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Say what you want, but if ~70% of the “talent and effort of the best developers” results in unfamiliar scroll behavior, this is telling of a deeper issue in the web design world.&lt;/p&gt;

&lt;h3&gt;
  
  
  Best practices
&lt;/h3&gt;

&lt;p&gt;Here are a few best practices that can be taken into consideration to reduce potential accessibility issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use native implementations when possible&lt;/strong&gt;. For many website interactions, browsers expose OS elements and functionality. Because they are directly provided by the OS, they handle accessibility … such as tabular navigation better. They can often be accessed through a CSS class. For example, scrollbars, cursors, hyperlinks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Respect contrast recommendations&lt;/strong&gt;. Make sure the foreground and background colors respect &lt;a href="https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html"&gt;WCAG guidelines on color contrast&lt;/a&gt; (&lt;a href="http://felixrunquist.com/contrast"&gt;here’s a tool&lt;/a&gt; to check the contrast between colors). Bad contrast will disproportionately impact users with color blindness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don’t display irrelevant or unexpected content&lt;/strong&gt;. Many users will click to your site as a result of a search. If they’ve clicked on that link, it’s because they expect the webpage and its content to match what they were looking for.&lt;br&gt;&lt;br&gt;
For example, if a page is a programming tutorial in the form of an article, they will likely visit it if they’re looking for a &lt;em&gt;written&lt;/em&gt; tutorial. In this case, displaying a large, autoplaying video would be irrelevant, and most likely be more annoying than useful. They won’t be likely to be expecting pages and pages of backstory on the history of the tutorial or of the language.&lt;/p&gt;

&lt;p&gt;The a11y project has developed a &lt;a href="http://a11yproject.com/checklist/"&gt;web accessibility checklist&lt;/a&gt; with best practices regarding content, layout and visual design. While it may be difficult to tick everything off the list, and I certainly don’t claim to have done so, it’s a good indicator to know where you are at regarding accessibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search engines
&lt;/h2&gt;

&lt;p&gt;Much of the online content we come across today is with the help of search engines. To be able to display the content efficiently, search engines need to &lt;em&gt;index&lt;/em&gt; websites beforehand. This is done with the help of robots, called &lt;em&gt;crawlers&lt;/em&gt;, that navigate webpages and search for keywords that would be relevant to the page. With the development of crawlers, techniques to optimise webpages for them were developed, appropriately dubbed &lt;em&gt;Search Engine Optimisation&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The issue is that crawlers have trouble replicating the ways humans navigate, and often fall for techniques aimed to improve search rank, i.e. the position of a webpage in a search result. Crawlers tend to appreciate verbose pages with a diverse range of keywords, while humans often prefer straight to the point content from which they can glean information as efficiently as possible. Think of someone searching for a recipe: they have the exact meal they want in mind, and their goal is to know the ingredients and preparation steps. More often than not, results will contain a lengthy backstory on the history of the recipe, the author’s grandma etc. before getting to the actual recipe. This isn’t relevant to the user’s search, but the crawlers latch onto the numerous recipe-related keywords in the backstory which is the reason the recipe ranked so high up in the users search.&lt;/p&gt;

&lt;p&gt;Over time, websites have become less and less targeted towards humans, and more towards search engines. This paradoxical, since the end consumer remains the same whether the website has been found directly or through a search engine: the human reader. The situation is that since search engines control people’s access to a website, websites optimised for humans are less likely to be visited than websites optimised for search engines. Websites less visited earn less revenue, so website creators have no choice but to favor crawlers over humans, and to have an inefficient delivery of information.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_VBGuIAd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2024/01/OIG.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_VBGuIAd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2024/01/OIG.jpeg" alt="" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A robot feeding websites to a human. [AI-generated]&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The need for human input
&lt;/h3&gt;

&lt;p&gt;The main factor that has led to this dystopian trend is the complete removal of human input from the process of crawling websites. As long as robots are unable to replicate human behavior, we need to add humans to the feedback loop so that content relevant to humans is favored. This could be done in the form of a upvote/downvote button at the bottom of search results.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bcPYjaec--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2024/01/Artboard-1-1024x407.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bcPYjaec--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2024/01/Artboard-1-1024x407.png" alt="" width="800" height="318"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Human input on search results&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Ads
&lt;/h2&gt;

&lt;p&gt;A large amount of the content we come across on the internet is sponsored. It is estimated that a whopping 21.2% of posts shown to users on Facebook and 20.6% on Instagram &lt;a href="http://digitalinformationworld.com/2020/08/what-percentage-of-posts-in-feed-on-facebook-instagram-linkedin-twitter-and-tiktok-are-advertisements.html"&gt;are advertisements&lt;/a&gt;. Revenue from advertisements make up &lt;a href="https://www.doofinder.com/en/statistics/google-revenue-breakdown"&gt;over 80%&lt;/a&gt; of Google’s annual revenue in 2022. This is a conflict of interest as the company which controls the way 91% of people search earns more by manually overriding its algorithm.&lt;/p&gt;

&lt;p&gt;Let’s look at the some of the main types of ads:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pop-up and prestitial ads&lt;/strong&gt;. These ads open a modal in front of content, often as soon as the page loads. On video content, they appear before the video. They provide a frustrating experience to the user as they provide an obstacle before the user is able to access content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sticky ads&lt;/strong&gt;. These ads appear when scrolling down and stick to the viewport for some time as the user scrolls, lengthening the amount of time the ad is displayed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Xe0zdSqE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2024/01/IMG_2227-e1705334752101-513x1024.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Xe0zdSqE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2024/01/IMG_2227-e1705334752101-513x1024.png" alt="" width="513" height="1024"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inserted ads&lt;/strong&gt;. These ads appear in content such as social media posts or emails, and are crafted to look visually similar to the rest of the page. This is frustrating for the user as they have trouble distinguishing ads from what’s important.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PxtjgZxy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://wp.felixrunquist.com/wp-content/uploads/2024/01/image-3-1024x656.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PxtjgZxy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://wp.felixrunquist.com/wp-content/uploads/2024/01/image-3-1024x656.png" alt="" width="800" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZLF1fp83--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://wp.felixrunquist.com/wp-content/uploads/2024/01/IMG_5405-2-708x1024.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZLF1fp83--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://wp.felixrunquist.com/wp-content/uploads/2024/01/IMG_5405-2-708x1024.jpg" alt="" width="708" height="1024"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Inserted ads in Gmail and Twitter respectively&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most users are frustrated by countdown ads which they can’t click or scroll away from, as well as ads that take up a high proportion of the screen (over 30%). It is clear that ads impact mobile and desktop devices differently: the same ad size takes up a higher proportion of the screen on mobile than on desktop.&lt;/p&gt;

&lt;p&gt;Over the years, as online companies seek to increase year-on-year revenue, they have traded usability for profitability by making their ad strategy increasingly agressive in an attempt to squeeze the most money out of their services. Ad density has increased, and they are harder to distinguish from non-sponsored content. Youtube tried showing &lt;a href="https://9to5google.com/2022/09/16/youtube-ads-unskippable/"&gt;as many as 10 unskippable ads&lt;/a&gt; in 2022, Gmail experimented &lt;a href="https://www.theverge.com/2023/5/5/23712440/gmail-ads-more-annoying-middle-inbox"&gt;embedding ads in the middle of content&lt;/a&gt; in 2023, and I long for the sweet old times when Instagram didn’t show ads.&lt;/p&gt;

&lt;p&gt;This has resulted in a game of cat and mouse – as platforms increase the proportion of ads, users become frustrated. This increases their chances of using an ad blocker, which in turn causes websites to use ad block detectors to as an incentive to turn them off. The situation has gotten so bad that I’ve found found that disabling Javascript on some websites makes the whole navigation experience more friendly: there’s much less ads and this even removes paywalls on some websites. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--k2qKbWq9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://wp.felixrunquist.com/wp-content/uploads/2024/01/image-2-1024x580.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--k2qKbWq9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://wp.felixrunquist.com/wp-content/uploads/2024/01/image-2-1024x580.png" alt="" width="800" height="453"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Cat and mouse: Websites detecting ad blockers and displaying pop-ups&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2KDMa_ny--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://wp.felixrunquist.com/wp-content/uploads/2024/01/Screenshot-2024-01-15-at-4.37.28-PM-1024x635.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2KDMa_ny--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://wp.felixrunquist.com/wp-content/uploads/2024/01/Screenshot-2024-01-15-at-4.37.28-PM-1024x635.png" alt="" width="800" height="496"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Youtube ad blocker detection, which rolled out in Q4 2023&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now don’t get me wrong, I’m not saying we should remove all ads, as they help offset operating costs of platforms and keep them free, however the agressive growth tactics of large corporations has led to an excessive amount of ads on the web. I believe we need to rethink the way we present ads in order to put the end user back into the frame.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.betterads.org/"&gt;coalition for better ads&lt;/a&gt; is an organisation formed by a group of large corporations in order to improve the standards of ads online. They provide guidelines and best practices for different types of ads. While this is on the right track, it isn’t enough: for example, despite Google being a board member of the organisation, and &lt;a href="https://support.google.com/chrome/answer/7632919?hl=en&amp;amp;co=GENIE.Platform%3DDesktop"&gt;having pledged to block ads&lt;/a&gt; that are deemed below &lt;a href="https://www.betterads.org/standards/"&gt;certain standards&lt;/a&gt; regarding user experience on Chrome, still extensively uses mid-roll ads on its video platform without blocking them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6hlqlAH---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://wp.felixrunquist.com/wp-content/uploads/2024/01/Ads-blocked.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6hlqlAH---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://wp.felixrunquist.com/wp-content/uploads/2024/01/Ads-blocked.webp" alt="" width="696" height="447"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Google Chrome’s pre-installed ad limiter&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on cookies
&lt;/h2&gt;

&lt;p&gt;With the introduction of GDPR in the EU in 2018, almost all websites display cookie pop-ups when you first load a page. While it may not be that annoying on websites you frequently visit, such as Google, since it’s only displayed once, it becomes quite frustrating on websites one rarely visits, especially on mobile, when clicking on an article link on Twitter for example.&lt;/p&gt;

&lt;p&gt;While it was a good idea to introduce legislation around the usage of cookies, the current implementation is catastrophic. Cookie pop-ups provide a frustrating experience, an obstacle to the website which is precisely why Javascript was developed in the first place. Worst of all, the process of giving one’s cookie consent hasn’t been standardized: there is a variety of Javascript-based extensions that have varying ways of displaying information to the user, meaning that one can’t rely on muscle memory, as well as causing accessibility issues.&lt;/p&gt;

&lt;p&gt;The legislation also lacks clarity – some websites use the cookie pop-up as an ultimatum: Accept cookies or pay a subscription to access the site. It is uncertain whether this is allowed, as the Article 5(3) of the ePrivacy Directive, also known as the ‘Cookie Law’, &lt;em&gt;“the storing of information, or the gaining of access to information already stored, in the terminal equipment of a subscriber or user is only allowed on condition that the subscriber or user concerned has given his or her consent, having been provided with clear and comprehensive information&lt;/em&gt;.&lt;em&gt;“&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It is hard for average users to discern whether websites enforce cookie choices or not, as most websites store at least one cookie whether or not the user has consented.&lt;/p&gt;

&lt;p&gt;In this catch-22 situation, a &lt;a href="https://chromewebstore.google.com/detail/accept-all-cookies/ofpnikijgfhlmmjlpkfaifhhdonchhoi"&gt;variety&lt;/a&gt; &lt;a href="https://chromewebstore.google.com/detail/i-still-dont-care-about-c/edibdbjcniadpccecjdfdjjppcpchdlm"&gt;of&lt;/a&gt; &lt;a href="https://apps.apple.com/fr/app/cookie-blocker/id6446212408?l=en-GB"&gt;browser&lt;/a&gt; &lt;a href="https://addons.mozilla.org/en-US/firefox/addon/i-dont-want-cookies/?utm_source=addons.mozilla.org&amp;amp;utm_medium=referral&amp;amp;utm_content=search"&gt;extensions&lt;/a&gt; have been developed to accept cookies automatically.&lt;/p&gt;

&lt;p&gt;A solution would be to put the burden of cookie consent on browses, the same way that website notification requests are handled: deciding to refuse cookies on a particular website would effectively prevent it from storing any cookies, removing the question of enforcement on the websites’ side. It would also allow for setting a global policy: one could choose a default behavior so that one doesn’t have to choose every time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Yye7qxdv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2024/01/image.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Yye7qxdv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2024/01/image.png" alt="" width="433" height="222"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A request to display notifications in Google Chrome&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One could even define a standard for sending cookie choices in HTTP headers, which would allow the browser to notify the website whether the user has chosen to allow only essential, or all cookies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;p&gt;Maggie Appleton has written &lt;a href="https://maggieappleton.com/ai-dark-forest"&gt;an insightful article&lt;/a&gt; on the adverse effects of large-language models on the internet.&lt;/p&gt;

&lt;p&gt;Don't hesitate to &lt;a href="https://twitter.com/intent/tweet?via=felixrunquist"&gt;hit me up on Twitter&lt;/a&gt; if you have any comments!&lt;/p&gt;

&lt;p&gt;This article was retrieved from &lt;a href="https://felixrunquist.com/posts/making-the-internet-more-human"&gt;felixrunquist.com&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Designing spring animations for the web</title>
      <dc:creator>Felix</dc:creator>
      <pubDate>Mon, 24 Jul 2023 07:57:33 +0000</pubDate>
      <link>https://dev.to/felixrunquist/designing-spring-animations-for-the-web-5481</link>
      <guid>https://dev.to/felixrunquist/designing-spring-animations-for-the-web-5481</guid>
      <description>&lt;p&gt;One of the things that has constantly evolved since the beginning of the digital age is the way we present content. From printed tape to blinking lights to ascii terminals, right until the development of the graphical user interface with the Xerox Alto and the Macintosh, there was one thing in common: severely limited computational resources. In fact, resources were so limited that one could see content animating before one’s eyes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--tgPQ0CVe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://felixrunquist.com/static/apple-lisa.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tgPQ0CVe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://felixrunquist.com/static/apple-lisa.gif" alt="" width="600" height="300"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Scrolling through a list of files on the Apple Lisa. Credits: Zhizu.com&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With the technological improvements in computing power, this got to the point where the delay in content animation became imperceptible to the human eye. What was once animated now became a jarring flash to the user. This raises an important question: How does one provide contextual information to the user?&lt;/p&gt;

&lt;p&gt;The answer: Voluntarily slowing down the speed at which content presents itself. This is done by allocating some computer resources to &lt;em&gt;tweening&lt;/em&gt; content, i.e. creating intermediate frames between the first and last state, and there you have it: an animation. It might seem paradoxical to use &lt;em&gt;more&lt;/em&gt; computer resources towards doing what one used to do with &lt;em&gt;fewer&lt;/em&gt; resources, but it actually allows for more control.&lt;/p&gt;

&lt;p&gt;If you’ve been on any form of digital device in the past fifteen years, you’ve most experienced many different types of animations. Animation, when done right, can make content feel more natural and less stark to the user. However, all these options have led to a widely diverse set of implementations: We’ve all come across the one site which dispenses with any animation (Jarring cookie banners for example), or even worse, a site that implements animation, but in a way that feels counterintuitive. It’s the wild west out there. This post aims to provide good practices for animations, as well as ways to implement them in Javascript and CSS.&lt;/p&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---J5iHH31--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://felixrunquist.com/static/3cb5eede154a01898eec80d60e8740a6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---J5iHH31--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://felixrunquist.com/static/3cb5eede154a01898eec80d60e8740a6.gif" alt="" width="642" height="358"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A cookie banner that is disruptive and annoying&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Reasons for using animations
&lt;/h2&gt;

&lt;p&gt;There are many cases for using animations can improve user experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User-triggered: These are animations that run after a user action like pressing a button or opening a new window. &lt;/li&gt;
&lt;li&gt;Loading states: These trigger when content is being fetched or a lengthy process is running – it provides reassuring feedback to the user and lets them know that the page isn’t hanging. &lt;/li&gt;
&lt;li&gt;Presenting content: This helps content that might not be expected by the user seem less jarring, such as a notification for example. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How can we make animations feel more intuitive? By imitating the physical world. In the words of the first Apple Human Interface guidelines in 1987, use metaphors from the real world. Users are already familiar with the movements of objects in the real world, and by replicating these, we can create motion that feels intuitive.&lt;/p&gt;
&lt;h2&gt;
  
  
  How does a physical interaction work?
&lt;/h2&gt;

&lt;p&gt;Let’s go through a bit of physics to understand how interactions work (If you don’t like physics, you can totally skip this section!). In the human-scale physical world, we use Newtonian dynamics to study the evolution of an object in space. An object is subject to multiple forces, and these contribute to the change in velocity through a simple equation:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;ma=∑iFi(1)
m a = \sum_i F_i \hspace{0.5cm} (1)
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;ma&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mop op-limits"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="mop op-symbol large-op"&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;F&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;em&gt;The product of mass and acceleration of an object is equal to the sum of the forces. This is known as Newton’s second law.&lt;/em&gt;

&lt;blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;When it comes to a spring, a specific force is present, which tends to pull it back to its resting position – this is known as Hooke’s law:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;F=−k(x–x0)(2)
F = -k(x – x_0) \hspace{0.5cm} (2)
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;F&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;−&lt;/span&gt;&lt;span class="mord mathnormal"&gt;k&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mord"&gt;–&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;em&gt;Where k is the spring constant, a measure of its stiffness, l0 is the resting position and l is the position of the spring.&lt;/em&gt;

&lt;p&gt;Another thing to take note of is derivatives. Speed is the derivative of the position with respect to time, acceleration is the derivative of velocity with respect to time:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;v(t)=dxdt a(t)=dvdt=d2xdt2
v(t) = \frac {dx} {dt}\
a(t) = \frac {dv} {dt} = \frac {d^2x} {dt^2}
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt; &lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;If we combine the two previous equations (1 and 2), we get the following:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;md2xdt2=−k(x(t)–x0)
m\frac {d^2x} {dt^2} = -k(x(t) – x_0)
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;m&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;−&lt;/span&gt;&lt;span class="mord mathnormal"&gt;k&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mord"&gt;–&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;For any of you that have studied maths on a higher level, you’ll recognize that what we have is a &lt;em&gt;differential equation&lt;/em&gt;, i.e an expression of a function based on its derivatives. The mathematical resolution is beyond the scope of this article, but the essential thing to retain is that we can get an expression of the position of an objet at a given time based on the forces that act upon it. The solution of the above differential equation is the following:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;x(t)=x(0)cos(ωt),ω=km
x(t) = x(0) cos(\omega t), \hspace{0.5cm} \omega = \sqrt {\frac k m}
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;0&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mord mathnormal"&gt;cos&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;ω&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;ω&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord sqrt"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span class="svg-align"&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;m&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="hide-tail"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;&lt;a href="https://felixrunquist.com/posts/designing-spring-animations-for-the-web"&gt;View the blog post&lt;/a&gt; to see a simulation of a spring controlled by such a force.&lt;/p&gt;

&lt;p&gt;You’ll instantly notice something that feels wrong with the animation: It continues infinitely! That’s because there’s no loss of momentum. In reality, there is energy loss which leads the spring to a stable state. In a spring, this is called &lt;em&gt;damping&lt;/em&gt;. Think of it as an object attached to a spring scraping against the ground. The friction slows the object, bringing it to an eventual stop.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://felixrunquist.com/posts/designing-spring-animations-for-the-web"&gt;View the blog post&lt;/a&gt; to see the simulation.&lt;/p&gt;

&lt;p&gt;The damping force is represented as a function of velocity:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;Fd=−αv(3)
F_d = -\alpha v \hspace{0.5cm} (3)
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;F&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;d&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;−&lt;/span&gt;&lt;span class="mord mathnormal"&gt;αv&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;3&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;By combining all the forces above (1, 2 and 3), we get the following differential equation:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;md2xdt2=−k(x–x0)–αdxdt
 m \frac {d^2x} {dt^2} = -k (x – x_0) – \alpha \frac {dx} {dt}
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;m&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;−&lt;/span&gt;&lt;span class="mord mathnormal"&gt;k&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mord"&gt;–&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mord"&gt;–&lt;/span&gt;&lt;span class="mord mathnormal"&gt;α&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;With the following solution:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;x(t)=x(0)e−βtcos(ωt),β=α2m,ω=km–(α2m)2
x(t) = x(0) e^ {-\beta t } cos(\omega t), \hspace{0.5cm} \beta = \frac \alpha {2m}, \hspace{0.5cm} \omega = \sqrt {\frac k m – (\frac \alpha {2m})^2}
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;0&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;−&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;βt&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;cos&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;ω&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;β&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;span class="mord mathnormal"&gt;m&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;α&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;ω&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord sqrt"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span class="svg-align"&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;m&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;–&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;span class="mord mathnormal"&gt;m&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;α&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="hide-tail"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;As the damping force represents energy loss through friction, which is almost always present in the real world, it leads to a much more realistic simulation.&lt;/p&gt;

&lt;p&gt;Phew! That was a lot of physics. Let’s see how we can implement this in the context of a web-based animation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spring animations
&lt;/h2&gt;

&lt;p&gt;Let’s look at the basics of a spring animation. There are three main things we can control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mass (m)&lt;/li&gt;
&lt;li&gt;Spring stiffness (k)&lt;/li&gt;
&lt;li&gt;Damping (d)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You’ll notice that, contrary to usual CSS animations, we don’t control the duration of an animation. This is because controlling timing makes you lose control of speed. For example, a 0.2s-long animation could be rather slow for an element which doesn’t move much, but it could be extremely fast for an element moving across the screen. We can get a precise idea of an easing function by studying a graph of it: On the horizontal axes time is represented, and the progress on the vertical axis:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://felixrunquist.com/posts/designing-spring-animations-for-the-web"&gt;View the blog post&lt;/a&gt; to see the simulations. &lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;b&gt;Easing or timing function?&lt;/b&gt;&lt;br&gt;&lt;br&gt;
The terms “easing function” and “timing function” are used interchangeably to define the function that specifies the speed curve of an animation (&lt;a href="https://www.w3schools.com/cssref/css3_pr_animation-timing-function.php#:~:text=Definition%20and%20Usage,to%20make%20the%20changes%20smoothly." rel="noopener nofollow"&gt;W3 schools&lt;/a&gt;). However, I prefer and will use the term “easing function” as timing function seems to imply that you can control the duration of an animation, and I just mentioned that we are not seeking to do that. &lt;br&gt;
&lt;/p&gt;

&lt;p&gt;When we think of a spring, we tend to think of it as being bouncy. However, springs don’t necessarily have to bounce; they actually have two characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overdamped&lt;/strong&gt; : The spring doesn’t bounce.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Underdamped&lt;/strong&gt; : The spring will bounce.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The condition for an overdamped spring is the following:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;α&amp;gt;4mk
\alpha &amp;gt; \sqrt {4mk}
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;α&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord sqrt"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span class="svg-align"&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;4&lt;/span&gt;&lt;span class="mord mathnormal"&gt;mk&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="hide-tail"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;Try adjusting the sliders in the following simulation to see how each parameter adjusts speed and bounce.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://felixrunquist.com/posts/designing-spring-animations-for-the-web"&gt;View the blog post&lt;/a&gt; to see the simulation.&lt;/p&gt;

&lt;h3&gt;
  
  
  CSS implementation
&lt;/h3&gt;

&lt;p&gt;There are a few ways to implement spring animations. As we have to consider characteristics such as mass, and damping, Bézier curves are ineffective for this. I first wanted to try a CSS-only implementation, to make spring animations as easy as other easing animations, however there are a few difficulties to consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At its native level, CSS does not allow the implementation of user-defined functions&lt;/li&gt;
&lt;li&gt;There is no way to use transition easing functions other than the ones provided (ex: ease-in or quad), or using Bézier curves&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means that we need to use a CSS pre-processor, such as Sass, to calculate the easing function. This is how it works: We’ll first define functions in Sass to calculate our spring values, then implement these as an animation. Next, we’ll use CSS animations as substitutes for transitions. Here’s the function defined in SASS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
@use ‘sass:math’;

@function get-animation($time, $stiffness, $mass, $damping, $from, $to){
  $alpha: calc($damping / (2 * $mass));
  $omega: math.sqrt(calc($stiffness / $mass) – math.pow($alpha, 2));

  @return calc($from + ($to – $from) * (1 – math.pow(math.$e, calc(-1 * $alpha * $time / 50)) * math.cos($omega * calc($time/ 50))));
}

@mixin spring-animation($name, $attribute, $prefix, $suffix, $from, $to, $mass, $stiffness, $damping) {
  @keyframes test {
    @for $i from 0 through 200 {
      #{$i * .5%} {
        #{$attribute}: #{$prefix} + get-animation($i, $stiffness, $mass, $damping, $from, $to) + #{$suffix};
      }
    }
  }
}

@include spring-animation(‘test’, transform, ‘translateX(‘, ‘rem)’, 0, 5, .5, 10, 1);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code uses Sass math functions, which are only available in Dart Sass. Check which flavor of Sass you have installed before proceeding!&amp;gt;&lt;/p&gt;

&lt;p&gt;With the parameters I supplied, it compiles to the following code:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://felixrunquist.com/posts/designing-spring-animations-for-the-web"&gt;View the blog post&lt;/a&gt; to see the code. &lt;/p&gt;

&lt;p&gt;As you can see, implementing spring functions in CSS is complex, and involves hacky workarounds. While the SASS function is simple enough, it still lacks functionality such as timing and compiles to CSS which is over 600 lines long. It’s much easier to do this in Javascript. One could implement an equivalent spring function manually, or use libraries which already support spring animation, such as &lt;code&gt;framer-motion&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
I found an npm package called &lt;a href="https://www.npmjs.com/package/sass-spring"&gt;sass-spring&lt;/a&gt; which allows for more control, however the documentation is very sparse and I couldn’t get it to work. &lt;br&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Javascript implementation
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.framer.com/motion/"&gt;Framer motion&lt;/a&gt;, as its name suggests, is a tool created by Framer to make it easier to implement animations in React. It provides motion-enabled equivalents for many HTML elements, such as &lt;code&gt;div&lt;/code&gt;s. The great thing about this package is that it allows for much more flexibility than CSS transitions, for example animating to an &lt;code&gt;auto&lt;/code&gt; width.&lt;/p&gt;

&lt;p&gt;Here’s an example of animating with &lt;code&gt;framer-motion&lt;/code&gt; in React. We’re creating a draggable div that animates back to its resting position when released.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://felixrunquist.com/posts/designing-spring-animations-for-the-web"&gt;View the blog post&lt;/a&gt; to see the simulation.&lt;/p&gt;

&lt;p&gt;It’s this easy! The great thing is that you can save transitions in a file so that they’re easy to reuse in your project and so that they stay consistent:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://felixrunquist.com/posts/designing-spring-animations-for-the-web"&gt;View the blog post&lt;/a&gt; to see the simulation.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
If you want to implement spring animations manually in Javascript, Maxime Heckel wrote &lt;a href="https://blog.maximeheckel.com/posts/the-physics-behind-spring-animations/" rel="noopener"&gt;an excellent article&lt;/a&gt; on it!&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Design considerations
&lt;/h2&gt;

&lt;p&gt;There are a couple of things to keep in mind when animating: accessibility and performance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Privilege GPU-based animations
&lt;/h3&gt;

&lt;p&gt;In a web browser, animations can be processed by the GPU or the CPU. It’s best practice to run animations on the GPU: it’s specifically designed to handle graphics, whereas the CPU is less optimal for the task as it has to run other processes as well. Such animations that run on the GPU are called &lt;em&gt;&lt;a href="https://developer.chrome.com/en/docs/lighthouse/performance/non-composited-animations/"&gt;composited&lt;/a&gt;&lt;/em&gt; animations. I won’t go into details, but for the most part, animations leveraging the &lt;code&gt;transform&lt;/code&gt; or the &lt;code&gt;opacity&lt;/code&gt; properties are composited.&lt;/p&gt;

&lt;p&gt;Let’s see how one might implement this for an animation that expands content into full-screen. We don’t want the content itself to change size, we just want the container to clip it. One might think of leveraging the &lt;code&gt;clip-path&lt;/code&gt; property, however animations on this property are &lt;em&gt;non-composited&lt;/em&gt; so they aren’t optimized. Instead, one could scale the container, and apply an inverse scaling on the content. This ensures that we use composited animations, and setting the &lt;code&gt;overflow&lt;/code&gt; property to &lt;code&gt;hidden&lt;/code&gt; will prevent children from appearing beyond the container limits.&lt;/p&gt;



&lt;p&gt;&lt;em&gt;The container with overflow set to hidden, clipping its content when one scales it. The content has an inverse scaling to make sure it stays the same size.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here’s how one could program it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useState } from ‘react’;
import { motion } from ‘framer-motion’;

const transition = {type: “spring”, stiffness: 100, damping: 15};
const scale = 0.8;

export default function App(){
  const [expanded, setExpanded] = useState(false)

  return (
    &amp;lt;motion.div onclick="{()"&amp;gt; setExpanded(!expanded)} className=’container’ style={{overflow: ‘hidden’, width: ’10rem’, height: ’10rem’}} transition={transition} initial={false} animate={{borderRadius: expanded ? 0 : 2 / scale + ‘rem’, transform: ‘scale(‘ + (expanded ? 1 : scale) + ‘)’}}&amp;amp;gt;
      &amp;lt;motion.div style="{{width:" height: padding: background: classname="inner" transition="{transition}" initial="{false}" animate="{{transform:" : scale&amp;gt;
          &amp;lt;p&amp;gt;Click to {expanded ? ‘minimize’ : ‘expand’}&amp;lt;/p&amp;gt;
      &amp;lt;/motion.div&amp;gt;
    &amp;lt;/motion.div&amp;gt;
  )

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lastly, avoid animating properties which are heavy on resources, such as &lt;code&gt;blur&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make sure spring animations are necessary
&lt;/h3&gt;

&lt;p&gt;As spring animations do have a bit of overhead compared to classic CSS animations, make sure the additional step of implementing a spring animation will be noticed. Animations are most distinguishable when using properties that influence the scale and position of an element. Other properties, such as opacity or color, are clipped outside of their bounds, meaning that you won’t see a bounce effect. These properties can be animated using classic CSS easing functions as it’s difficult to perceive the difference.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://felixrunquist.com/posts/designing-spring-animations-for-the-web"&gt;View the blog post&lt;/a&gt; to see a comparison chart. &lt;/p&gt;

&lt;h3&gt;
  
  
  Put native interactions first
&lt;/h3&gt;

&lt;p&gt;For many device interactions, an OS implementation already exists. Scrolling, for example, is specific to each device, and the user is already comfortable with the way their device implements these through continued interaction. Before overriding the default interactions, consider whether it’s really necessary: It could leave the user confused that things aren’t working the way they’re used to. Plus, you run the risk of breaking accessibility features implemented by the operating system.&lt;/p&gt;

&lt;p&gt;For example, if you want to implement a smooth scroll animation when navigating to an element on the page, instead of implementing your own version of it, use the native API: &lt;code&gt;window.scrollTo&lt;/code&gt;. This will make sure that the OS handles scrolling, and that it will animate the way the user is used to. Here’s an example of smoothly scrolling to an element in React:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export default function App(){
  function scroll(e){
    e.preventDefault(); //Prevent default “jump” scrolling behavior
    const element = document.querySelector(e.target.getAttribute(‘href’));
    const rect = element.getBoundingClientRect();
    window.scrollTo({top: window.scrollX + rect.top, behavior: ‘smooth’});
  }

  return (
    &amp;amp;lt;&amp;amp;gt;
      &amp;lt;a onclick="{smoothScroll}" href="#element"&amp;gt;Click me to scroll to #element&amp;lt;/a&amp;gt;
      &amp;lt;div id="element"&amp;gt;
        &amp;lt;p&amp;gt;I’m #element!&amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;amp;gt;
  )

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Complement, not distract
&lt;/h3&gt;

&lt;p&gt;Animations should complement the user, not distract them. Like the annoying cookie banner example above, use of animations shouldn’t grab attention but merely improve the user experience by providing context. If an object expands from a closed state, it shows that it is the same and provides more context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Animation frequency
&lt;/h3&gt;

&lt;p&gt;Consider the amount of times the user will be triggering the animation: If it’s just a few times, an animation could surprise the user and give originality to your website. If the user experiences the animation quite often, however, it could start to feel sluggish.&lt;/p&gt;

&lt;p&gt;For example, launching an application on Mac never results in the window animating as it opens. This is because during normal use, a user could be opening dozens of applications a day, making the animation redundant. Even though it could be perceived as janky to see a window pop up without notice, this is &lt;em&gt;expected&lt;/em&gt; by the user as they triggered it. Plus, it makes launching applications feel faster.&lt;/p&gt;



&lt;p&gt;In contrast, even though a user could be receiving many notifications a day, these are unexpected, meaning that they often aren’t user-initiated. A sliding animation helps the user contextually.&lt;/p&gt;

&lt;p&gt;Not every design follows this. For instance, Microsoft in its 2016 Office update decided to animate all cursor movements in Word, Powerpoint and Excel. This is a great example of what &lt;em&gt;not&lt;/em&gt; to do: users generally type thousands of characters, meaning the animation runs &lt;em&gt;thousands&lt;/em&gt; of times, and instead of feeling snappy, like the press of a key, it feels sluggish. The typing of a character is user initiated, so it’s expected, making it a great candidate for a non-animated transition (I should also note that Microsoft has been known for its questionable design practices, such as putting the Shut Down and Restart buttons in the &lt;em&gt;Start&lt;/em&gt; menu, something I didn’t think much of until an older friend pointed out the oxymoron).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9pCbqkDh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://felixrunquist.com/static/108513034-f9034c00-72fc-11eb-8674-d5d2ca33ca94.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9pCbqkDh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://felixrunquist.com/static/108513034-f9034c00-72fc-11eb-8674-d5d2ca33ca94.gif" alt="" width="643" height="200"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Animated caret in Word 2016&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;&lt;em&gt;The caret isn’t animated in the current version of Word for Mac&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--lxkzUDdB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/07/animation-flowchart-1-860x1024.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lxkzUDdB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/07/animation-flowchart-1-860x1024.png" alt="" width="800" height="953"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A flowchart to help decide whether you should animate an object of not&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Lastly, beware of animations that can become associated with annoyances, such as an animation appearing consistently during frustrating moments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--WkJkw13p--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://felixrunquist.com/static/loading-windows.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--WkJkw13p--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://felixrunquist.com/static/loading-windows.gif" alt="" width="220" height="103"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TpqHTm3a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://felixrunquist.com/static/z02scgtydp121.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TpqHTm3a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://felixrunquist.com/static/z02scgtydp121.gif" alt="" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The spinning circle of doom, on Windows and MacOS respectively.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;p&gt;There are a lot of great resources out there to help with animation, as well as implementing animation while considering design principles. I’ll list a few here.&lt;/p&gt;

&lt;p&gt;Apple has released an &lt;a href="https://developer.apple.com/videos/play/wwdc2023/10158/"&gt;amazing video&lt;/a&gt; in WWDC 23 on creating spring animations.&lt;/p&gt;

&lt;p&gt;As I was writing this article, Rauno Freiburg wrote an &lt;a href="https://rauno.me/craft/interaction-design"&gt;extremely detailed article&lt;/a&gt; on the intricacies behind interaction design. He explores motion and what makes good design.&lt;/p&gt;

&lt;p&gt;The &lt;a href="http://felixrunquist.com/apple_human_interface_1987.pdf"&gt;Apple Human interface guidelines of 1987&lt;/a&gt;, which were surprisingly avant-garde at the time and are very much still relevant today (retrieved from &lt;a href="http://andymatuschak.org"&gt;andymatuschak.org&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Maxime Heckel dives into details on the implementation of spring physics in Javascript. If you want an in-depth explanation on the workings of Framer Motion’s spring animations, &lt;a href="https://blog.maximeheckel.com/posts/the-physics-behind-spring-animations/"&gt;I suggest you have a read&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Google released an &lt;a href="https://developer.chrome.com/blog/inside-browser-part1/"&gt;easy-to-read article&lt;/a&gt; on the workings of a web browser. It’s in 4 parts and helps understand how different items are rendered during different processes, and how to write performant code which leverages in-browser optimisations. Parts 3 and 4 are extremely helpful!&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We’ve seen the evolution of animations in the past fifty years, as well as some examples of good and bad animation design: Not every animation improves the user experience. We should keep in mind not to use animations as distractions, and to be careful of animations that can quickly become frustrating. After time, one develops an intuition for which animation is right. I discuss examples of good design animation in the &lt;a href="https://felixrunquist.com/posts/web-design-inspiration"&gt;following post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We’ve seen CSS and Javascript implementations of spring animations, which are a realistic representation of everyday physics. Look out for the everyday animations you see, such as the swaying of a branch in the wind, or opening an app on your smartphone, and see if you can distinguish which type of animation it is: damped or undamped, bouncy or not.&lt;/p&gt;

&lt;p&gt;Let me know what you think and don’t hesitate to &lt;a href="https://twitter.com/intent/tweet?via=felixrunquist"&gt;share your thoughts over on Twitter&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://phys.libretexts.org/Bookshelves/University_Physics/Book%3A_University_Physics_(OpenStax)/Book%3A_University_Physics_I_-_Mechanics_Sound_Oscillations_and_Waves_(OpenStax)/15%3A_Oscillations/15.06%3A_Damped_Oscillations"&gt;https://phys.libretexts.org/Bookshelves/University_Physics/Book%3A_University_Physics_(OpenStax)/Book%3A_University_Physics_I_-_Mechanics_Sound_Oscillations_and_Waves_(OpenStax)/15%3A_Oscillations/15.06%3A_Damped_Oscillations&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.framer.com/motion/"&gt;Framer Motion documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sass-lang.com/documentation/"&gt;Sass documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This article was retrieved from &lt;a href="https://felixrunquist.com/"&gt;felixrunquist.com&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Creating 3D models in Spline for Three.js</title>
      <dc:creator>Felix</dc:creator>
      <pubDate>Mon, 10 Jul 2023 10:42:35 +0000</pubDate>
      <link>https://dev.to/felixrunquist/creating-3d-models-in-spline-for-threejs-152b</link>
      <guid>https://dev.to/felixrunquist/creating-3d-models-in-spline-for-threejs-152b</guid>
      <description>&lt;p&gt;If you’ve been online lately, you’ve probably heard of Three.js, a web framework for 3D graphics. What you can do with it is absolutely stunning. However, if you want to display any complicated models which go beyond what a cube or a sphere can do, it’s best to create the model in a graphics application and then import it in Three.js. That’s where Spline comes in.&lt;/p&gt;

&lt;blockquote&gt;
People have already built incredible things with Three.js. Here are a few examples:
&lt;a href="https://my-room-in-3d.vercel.app/" rel="noopener"&gt;My room in 3D by Bruno Simons&lt;/a&gt;&lt;br&gt;
&lt;a href="https://atmos.leeroy.ca/" rel="noopener"&gt;Atmos by Leeroy&lt;/a&gt;&lt;br&gt;
&lt;a href="https://www.joshuas.world/" rel="noopener"&gt;Joshua’s world&lt;/a&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What is Spline?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="http://spline.design"&gt;Spline&lt;/a&gt; is a new online-based 3D software that allows just about any designer to create with intuitive tools. It doesn’t overwhelm you with menus and tools, it has just the basics. It has both a free and a paid version, but for most, the free tier does enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a 3D model in Spline
&lt;/h2&gt;

&lt;p&gt;To show what can be done, I decided to make a simple model: a “Roundel” – a circular disk with my logo which I was planning on using as part of my website identity.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DjhPD1Qz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3f0zryva4hj28kdba1pg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DjhPD1Qz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3f0zryva4hj28kdba1pg.png" alt="My 3D roundel" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To start off, create an account in Spline and open a new project. It will show you an window with a rectangle and a light, an empty slate if you will.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--724z3eHk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/07/Screenshot-2023-07-03-at-11.36.41-AM-1024x559.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--724z3eHk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/07/Screenshot-2023-07-03-at-11.36.41-AM-1024x559.png" alt="" width="800" height="437"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Opening a new project in Spline&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you’ve already used a 3D editor before, this should feel like child’s play. Otherwise, it’s a bit like using a 2D graphics editor like Figma or Sketch, except that everything is now in 3D.&lt;/p&gt;

&lt;p&gt;There are four main sections to the editor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On the left, the items list shows the objects you have in your 3D scene. &lt;/li&gt;
&lt;li&gt;On the top center, a toolbar has options for adding shapes and lights, exporting, and previewing.&lt;/li&gt;
&lt;li&gt;On the right, the edit panel allows you to change attributes of the selected object. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of the commands are intuitive and explained by the onboarding process, but here are the most important ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Alt + Drag&lt;/strong&gt; – pan around the 3D scene&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scroll&lt;/strong&gt; – Shift around the 3D scene&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alt + Scroll&lt;/strong&gt; – Shift around on one axis only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In order to make my roundel, I started off by adding a circle. This is done by selecting “+” on the toolbar and selecting ellipse (O). Click and drag in the scene to place the circle. In order to make the ellipse circular, hold shift while dragging to constrain proportions.&lt;/p&gt;

&lt;p&gt;We now have a 2D circle. To make it have volume and convert it to a cylinder, we can “extrude” it: Select the circle and add a value to the “extrusion” slider, under the shape menu. It might not look like we’ve changed much at first, but if you Alt+Drag, you’ll see that the circle now has a volume.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5cSE0J2L--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/07/Screenshot-2023-07-03-at-11.46.25-AM-1024x863.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5cSE0J2L--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/07/Screenshot-2023-07-03-at-11.46.25-AM-1024x863.png" alt="" width="800" height="674"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;My extruded cylinder&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I found the corners a little too sharp for my liking, so I decided to add a bevel to the cylinder’s vertices. This can be done by dragging the “Bevel” slider right under the extrusion slider.&lt;/p&gt;

&lt;p&gt;Next, it’s time to add some text. This is delightfully easy in Spline, and I wish other 3D editors had the same ease-of-use. Select the “T” icon in the toolbar, and click on one of the surfaces of the cylinder. Each surface should highlight in red as you hover over it, indicating that the text will snap to it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--w050nh-p--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/07/Screenshot-2023-07-03-at-11.49.27-AM-1024x799.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--w050nh-p--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/07/Screenshot-2023-07-03-at-11.49.27-AM-1024x799.png" alt="" width="800" height="624"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Each surface highlights when you hover it with the text tool.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Type your text and don’t worry if you don’t see anything – Every object you add to the scene has a default dull gray color – we’ll tweak the colors next!&lt;/p&gt;

&lt;p&gt;Select the ellipse and add the color you want in the “Material” section of the object editor. I decided to go with my site’s pastel orange. I colored the text in black in the same way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--lBOAfE8P--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/07/Screenshot-2023-07-03-at-12.00.07-PM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lBOAfE8P--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/07/Screenshot-2023-07-03-at-12.00.07-PM.png" alt="" width="800" height="630"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;My colored cylinder and text&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I wanted to change the font to reflect my site’s wordmark. This was extremely easy to do: select the text, go to the “font” menu. You can even upload a custom font!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--aTvKMI4u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/07/Screenshot-2023-07-03-at-12.04.14-PM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--aTvKMI4u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/07/Screenshot-2023-07-03-at-12.04.14-PM.png" alt="" width="800" height="664"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;My cylinder with cusomized font, rescaled&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Next, to make everything 3D, I also extruded the text.&lt;/p&gt;

&lt;p&gt;The last part is lighting. Since we’ll be exporting the design in a file format that lacks support for lighting, you don’t have to worry about this part, but I still believe this deserves mentioning. In the physical world, multiple cues exist to give us information about an object’s physical location: The stereoscopic information from our eyes, the texture of the object and the shadows it casts. Since we don’t get stereoscopic information on screens, we need to rely more heavily on other cues such as the reflectivity of the object and the shadows it casts. That’s where different lights come in play.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jnagSVkq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/07/Screenshot-2023-07-03-at-2.40.21-PM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jnagSVkq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/07/Screenshot-2023-07-03-at-2.40.21-PM.png" alt="" width="416" height="202"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The three different light types in Spline&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I won’t dive into the specifics of each light as it’s beyond the scope of this article, but you can play around with the different lights and see which one looks best. Most of the time I use a point light which is a single source of omnidirectional light. I like to position it so that it casts nice shadows – this is why I extruded the “Felix” lettering. When you’re in the editor, lights are represented by wireframes. Below you can see my model with a light in the top left corner casting shadows from my lettering.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VDvutlMR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/07/Screenshot-2023-07-03-at-2.45.08-PM-1024x702.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VDvutlMR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/07/Screenshot-2023-07-03-at-2.45.08-PM-1024x702.png" alt="" width="800" height="548"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;My point light&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Exporting
&lt;/h3&gt;

&lt;p&gt;There are multiple formats you can export your 3D file in, some are free and others aren’t (for instance, you can export in formats suited for 3D printing or augmented reality). The easiest way for displaying the 3D model on the web is to use the embedded viewer or the code export, which provides you with code to import the model and display it in your web application.&lt;/p&gt;

&lt;p&gt;However, I try to be careful about the technology I use, so I don’t get locked up in a proprietary “ecosystem”, and as Spline is closed-source, I feel more comfortable exporting the model to an open-source file format, even though it does complicate things a bit. To combat this, I’ll show you how you can do this in a React/Node.js project for free. After much trial and error, here’s the workflow I came up with:&lt;/p&gt;

&lt;p&gt;Export the model as GLTF or GLB (It’s best to use GLB in production as it’s a compressed version of GLTF, GLTF being just JSON). It’s one of the free exports and is supported by Three.js. Save that to the public directory of your web project. The issue with spline exports to GLTF/GLB is that it doesn’t retain lighting or materials, only geometries are exported, however there is a workaround for that.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
I use Next.js for my projects, this is what I found for that but a lot of it applies to other frameworks!&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;In order to see the different export options, click the “Exports” button on the middle toolbar.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--WfOOucfZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/07/image-869x1024.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--WfOOucfZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/07/image-869x1024.png" alt="" width="800" height="943"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The exports panel&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you go back to the “Exports” panel of your spline file and select the “Code export” tab, Spline automatically generates the positioning as well as the lighting in Three.js format. You can select which flavor of Three.js you use (I use &lt;code&gt;react-three-fiber&lt;/code&gt;), and copy the code. For instance, here’s the code for my model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/*
  Auto-generated by Spline
*/

import useSpline from ‘@splinetool/r3f-spline’
import { PerspectiveCamera } from ‘@react-three/drei’

export default function Scene({ …props }) {
  const { nodes, materials } = useSpline(‘https://[external spline URL]’)
  return (
    &amp;amp;lt;&amp;amp;gt;
      &amp;lt;color attach="background" args="{['#74757a']}"&amp;gt;&amp;lt;/color&amp;gt;
      &amp;lt;group dispose="{null}"&amp;gt;
        &amp;lt;pointlight name="Point Light" castshadow intensity="{0.82}" distance="{1379}" shadow-mapsize-width="{1024}" shadow-mapsize-height="{1024}" shadow-camera-near="{100}" shadow-camera-far="{100000}" position="{[-254.65,"&amp;gt;&amp;lt;/pointlight&amp;gt;
        &amp;lt;group name="Group"&amp;gt;
          &amp;lt;mesh name="Text" geometry="{nodes.Text.geometry}" material="{materials['Text" castshadow receiveshadow position="{[-46.56," rotation="{[0,"&amp;gt;&amp;lt;/mesh&amp;gt;
          &amp;lt;mesh name="Ellipse" geometry="{nodes.Ellipse.geometry}" material="{materials['Ellipse" castshadow receiveshadow position="{[-8.73," rotation="{[0," scale="{1}"&amp;gt;&amp;lt;/mesh&amp;gt;
        &amp;lt;/group&amp;gt;
        &amp;lt;perspectivecamera name="1" makedefault="{true}" far="{100000}" near="{70}" fov="{45}" position="{[-949.88," rotation="{[-0.46," scale="{1}"&amp;gt;&amp;lt;/perspectivecamera&amp;gt;
        &amp;lt;hemispherelight name="Default Ambient Light" intensity="{0.75}" color="#eaeaea"&amp;gt;&amp;lt;/hemispherelight&amp;gt;
      &amp;lt;/group&amp;gt;
    &amp;amp;gt;
  )
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the code provided from your Spline file as a new component (make sure to add the code provided by your project and not the one above as it’s project-specific!). You’ll notice that in the code there’s an external link to your project hosted on Spline servers, but don’t worry about that, we’ll get to it.&lt;/p&gt;

&lt;p&gt;Next, you’ll want to install the following repositories to your project using &lt;code&gt;npm&lt;/code&gt; or &lt;code&gt;yarn&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@react-three/fiber&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;three&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@react-three/drei&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’re going to alter the code provided by Spline to import the self-hosted GLTF file. Replace the line with useSpline with the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//Replace ‘/felix_roundel.glb’ with the path to your gltf/glb file
const gltf = useLoader(GLTFLoader, ‘/felix_roundel.glb’, loader =&amp;amp;gt; {
const dracoLoader = new DRACOLoader();
  dracoLoader.setDecoderPath(‘/draco/gltf/’);
  loader.setDRACOLoader(dracoLoader);
})
const { nodes, materials } = gltf;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This uses a “loader” which is a function that loads your 3D file. We use &lt;a href="https://threejs.org/docs/#examples/en/loaders/DRACOLoader"&gt;DRACOLoader&lt;/a&gt; as well, as it’s an optimization library for importing point clouds and meshes.&lt;/p&gt;

&lt;p&gt;Next, you’ll want to head to the following path of your project: &lt;code&gt;/node_modules/three/examples&lt;/code&gt;, and copy the &lt;code&gt;draco&lt;/code&gt; file to the public folder of your project. This is one of the quirks of the dracoLoader: It not only needs a library import, but it needs to import a decoder using the &lt;code&gt;setDecoderPath&lt;/code&gt; method (see &lt;a href="https://stackoverflow.com/questions/56071764/how-to-use-dracoloader-with-gltfloader-in-reactjs"&gt;https://stackoverflow.com/questions/56071764/how-to-use-dracoloader-with-gltfloader-in-reactjs&lt;/a&gt;). You can supply it with a local decoder hosted in your project’s public folder as I describe above, or alternatively you can link it to an externally-hosted library.&lt;/p&gt;

&lt;p&gt;Remove the default export in the Scene function, and wrap it around a &lt;code&gt;&amp;lt;Canvas&amp;gt;&lt;/code&gt; element. This is needed to prevent errors with paths (see &lt;a href="https://stackoverflow.com/questions/75950437/nextjs-react-three-drei-typeerror-failed-to-parse-url"&gt;https://stackoverflow.com/questions/75950437/nextjs-react-three-drei-typeerror-failed-to-parse-url&lt;/a&gt;). I know, there’s a lot of quirks to navigate around, but it’ll be worth it in the end! This is what your code should look like for now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import { Canvas } from “@react-three/fiber”;
import { GLTFLoader } from “three/examples/jsm/loaders/GLTFLoader”;
import { DRACOLoader } from ‘three/examples/jsm/loaders/DRACOLoader’
import { PerspectiveCamera } from ‘@react-three/drei’
import { useLoader } from “@react-three/fiber”;

export function Object(){
  const gltf = useLoader(GLTFLoader, ‘/felix_roundel.glb’, loader =&amp;amp;gt; {
  const dracoLoader = new DRACOLoader();
    dracoLoader.setDecoderPath(‘/draco/gltf/’);
    loader.setDRACOLoader(dracoLoader);
   })
   const { nodes, materials } = gltf;
  return (
    &amp;amp;lt;&amp;amp;gt;
      &amp;lt;color attach="background" args="{['#74757a']}"&amp;gt;&amp;lt;/color&amp;gt;
      &amp;lt;group dispose="{null}"&amp;gt;
        &amp;lt;pointlight name="Point Light" castshadow intensity="{0.82}" distance="{1379}" shadow-mapsize-width="{1024}" shadow-mapsize-height="{1024}" shadow-camera-near="{100}" shadow-camera-far="{100000}" position="{[-254.65,"&amp;gt;&amp;lt;/pointlight&amp;gt;
        &amp;lt;group name="Group"&amp;gt;
          &amp;lt;mesh name="Text" geometry="{nodes.Text.geometry}" material="{materials['Text" castshadow receiveshadow position="{[-46.56," rotation="{[0,"&amp;gt;&amp;lt;/mesh&amp;gt;
          &amp;lt;mesh name="Ellipse" geometry="{nodes.Ellipse.geometry}" material="{materials['Ellipse" castshadow receiveshadow position="{[-8.73," rotation="{[0," scale="{1}"&amp;gt;&amp;lt;/mesh&amp;gt;
        &amp;lt;/group&amp;gt;
        &amp;lt;perspectivecamera name="1" makedefault="{true}" far="{100000}" near="{70}" fov="{45}" position="{[-949.88," rotation="{[-0.46," scale="{1}"&amp;gt;&amp;lt;/perspectivecamera&amp;gt;
        &amp;lt;hemispherelight name="Default Ambient Light" intensity="{0.75}" color="#eaeaea"&amp;gt;&amp;lt;/hemispherelight&amp;gt;
      &amp;lt;/group&amp;gt;
    &amp;amp;gt;
  )
}

export default function Scene(){
  return (
    &amp;lt;canvas&amp;gt;
      &amp;lt;object&amp;gt;&amp;lt;/object&amp;gt;
    &amp;lt;/canvas&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run the code, you’ll notice that everything looks a bit sad and gray. This is because react-three-fiber can’t find the materials in the GLTF/GLB file (exporting textures is a premium feature). We’ll add our own materials for the meshes. There are different materials available in Three.js, the most useful one I find is &lt;code&gt;MeshPhongMaterial&lt;/code&gt;. It’s realistic and allows for shininess and shadows, and responds well to different types of lights. This is how you can define a material:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const material = new THREE.MeshPhongMaterial({
    color: 0x000000,
    transparent: false, opacity: 0.5,
    specular: 0x050505,
    shininess: 100
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The color is provided in hexadecimal format, for instance if one wanted a bright red one would type &lt;code&gt;0xff0000&lt;/code&gt;. I had a few issues with color fidelity in my model (my oranges looked yellow, many colors appeared washed out), to combat this I used &lt;code&gt;THREE.Color&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const color = new THREE.Color(‘#FF9A03’).convertSRGBToLinear();
const material = new THREE.MeshPhongMaterial({
    color: color.getHex(),
    transparent: false, opacity: 0.5,
    specular: 0x050505,
    shininess: 100
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create as many materials as you need and replace add them to the materials attribute of each mesh. As I wanted my model to have a transparent background, I also decided to remove the &lt;code&gt;&amp;lt;color&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;

&lt;p&gt;You should be done! This is the final code for my model. You can view the output it generates &lt;a href="https://felixrunquist.com/posts/creating-3d-models-spline-three-js"&gt;here&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import { Canvas } from “@react-three/fiber”;
import { GLTFLoader } from “three/examples/jsm/loaders/GLTFLoader”;
import { DRACOLoader } from ‘three/examples/jsm/loaders/DRACOLoader’
import { OrbitControls, PerspectiveCamera } from ‘@react-three/drei’
import { useLoader, } from “@react-three/fiber”;
import * as THREE from ‘three’

export function Object(){
  const gltf = useLoader(GLTFLoader, ‘https://felixrunquist.com/felix_roundel_2.glb’, loader =&amp;amp;gt; {
  const dracoLoader = new DRACOLoader();
    dracoLoader.setDecoderPath(‘https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/js/libs/draco/’);
    loader.setDRACOLoader(dracoLoader);
   })
   const { nodes, materials } = gltf;
   const color = new THREE.Color(‘#FF9A03’).convertSRGBToLinear();
   const ellipseMaterial = new THREE.MeshPhongMaterial({
        color: color.getHex(),
        transparent: false, opacity: 0.5,
        specular: 0x050505,
        shininess: 100
   });
   const textMaterial = new THREE.MeshPhongMaterial({
        color: 0x000000,
        transparent: false, opacity: 0.5,
        specular: 0x050505,
        shininess: 100
   });
  return (
    &amp;amp;lt;&amp;amp;gt;
      &amp;lt;group dispose="{null}"&amp;gt;
        &amp;lt;pointlight name="Point Light" castshadow shadow-mapsize-width="{1024}" shadow-mapsize-height="{1024}" shadow-camera-near="{100}" shadow-camera-far="{100000}" position="{[-254.65,"&amp;gt;&amp;lt;/pointlight&amp;gt;
        &amp;lt;group name="Group"&amp;gt;
          &amp;lt;mesh name="Text" geometry="{nodes.Text.geometry}" material="{textMaterial}" castshadow receiveshadow position="{[-46.56," rotation="{[0,"&amp;gt;&amp;lt;/mesh&amp;gt;
          &amp;lt;mesh name="Ellipse" geometry="{nodes.Ellipse.geometry}" material="{ellipseMaterial}" castshadow receiveshadow position="{[-8.73," rotation="{[0," scale="{1}"&amp;gt;&amp;lt;/mesh&amp;gt;
        &amp;lt;/group&amp;gt;
        &amp;lt;perspectivecamera name="1" makedefault="{true}" far="{100000}" near="{70}" fov="{45}" position="{[-949.88," rotation="{[-0.46," scale="{1}"&amp;gt;&amp;lt;/perspectivecamera&amp;gt;
        &amp;lt;hemispherelight name="Default Ambient Light" intensity="{0.75}" color="#eaeaea"&amp;gt;&amp;lt;/hemispherelight&amp;gt;
      &amp;lt;/group&amp;gt;
    &amp;amp;gt;
  )
}

export default function Scene(){
  return (
    &amp;lt;div style="{{width:" height:&amp;gt;
      &amp;lt;canvas shadows&amp;gt;
        &amp;lt;orbitcontrols&amp;gt;&amp;lt;/orbitcontrols&amp;gt;
        &amp;lt;object&amp;gt;&amp;lt;/object&amp;gt;
      &amp;lt;/canvas&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
If you aren’t seeing any shadows, add the shadows attribute to your \&amp;lt;\Canvas\&amp;gt; tag. 
&lt;/blockquote&gt;

&lt;p&gt;All done! I hope this helps you get started with Spline and Three.js. &lt;a href="https://twitter.com/intent/tweet?via=felixrunquist"&gt;Let me know if you have any issues&lt;/a&gt; and what 3D projects you make!&lt;/p&gt;

&lt;p&gt;This article was retrieved from &lt;a href="//felixrunquist.com"&gt;felixrunquist.com&lt;/a&gt;. &lt;/p&gt;

</description>
    </item>
    <item>
      <title>Adding search to a React/Next.js blog</title>
      <dc:creator>Felix</dc:creator>
      <pubDate>Sun, 02 Jul 2023 11:15:09 +0000</pubDate>
      <link>https://dev.to/felixrunquist/adding-search-to-a-reactnextjs-blog-1d95</link>
      <guid>https://dev.to/felixrunquist/adding-search-to-a-reactnextjs-blog-1d95</guid>
      <description>&lt;p&gt;When I &lt;a href="https://felixrunquist.com/posts/how-i-built-my-website"&gt;built my Next.js-based&lt;/a&gt; website, one of the things I really wanted to implement was the ability to search through posts. I recently came across Kent C. Dodd’s &lt;code&gt;match-sorter&lt;/code&gt; package, a Node.js package which allows for speedy filtering through text arrays. It immediately made me think of its usage as a search algorithm.&lt;/p&gt;

&lt;p&gt;Here’s the idea: On the client, we can provide the page with an array of post metadata: titles, excerpts and slugs. We add an event listener to the search input which runs &lt;code&gt;matchSorter&lt;/code&gt; on the array with the content of the search input, and we display the resulting posts.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I had a look at &lt;a href="https://github.com/kentcdodds/kentcdodds.com"&gt;Kent C. Dodd’s website&lt;/a&gt; and it turns out that he uses match-sorter as well in his search algorithm!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The basic usage of &lt;code&gt;match-sorter&lt;/code&gt; is really simple: You provide it an array and a string to match with, and it returns an ordered array based on what ranks the highest. This makes for a really simple implementation below, with react functions to manage state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { matchSorter, rankings } from ‘match-sorter’
import {useState, useEffect} from ‘react’
const search = [‘Hi’, ‘Hello’, ‘Hi there’]
export default function App(){
  const [input, setInput] = useState(‘\’)
  const [results, setResults] = useState([])
  useEffect(() =&amp;amp;gt; {
    var res = matchSorter(search, input)
    setResults(res)
  }, [input])
  return (
    &amp;amp;lt;&amp;amp;gt;
      &amp;lt;input type="text" value="{input}" oninput="{e"&amp;gt; setInput(e.target.value)}/&amp;amp;gt;
      {results.map(i =&amp;amp;gt; &amp;lt;p key="{i}"&amp;gt;{i}&amp;lt;/p&amp;gt;)}
    &amp;amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, in the case of an article search system, we want to do more: we’d like to be able to search not only through an array of titles, but through other information, such as a short excerpt for example. This data will be fetched from the CMS and provided to the browser, resulting in an object like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const posts = [
  {title: ‘How I built my website’, excerpt: ‘Trying to figure out how to build my website was a challenge for me. As a web designer, I wanted it to be fully customisable, but I also wanted ease of use to reduce friction and make it easy to maintain.’, url: ‘https://felixrunquist.com/posts/how-i-built-my-website’},
  {title: ‘Implementing online payments with Stripe’, excerpt: ‘A tutorial on setting up a credit card payment form on your NextJS site. We will cover signing up and the Stripe SDK’, url: ‘https://felixrunquist.com/posts/online-payments-stripe’},
  {title: ‘Next.js HTTP Authentication with JWT and cookies’, excerpt: ‘I was recently trying to figure out a way to implement simple HTTP authentication for a personal Next.js project. I augmented the basic HTTP auth with JWT and cookies’, url: ‘https://felixrunquist.com/posts/next-js-http-authentication-jwt-cookies’},
  {title: ‘Creating and ordering a custom PCB’, excerpt: ‘As part of a home automation project, I wanted to house a thermometer, motion sensor and display in a small form factor. I decided to design and order my own PCB.’, url: ‘https://felixrunquist.com/posts/creating-and-ordering-custom-pcb’}
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The smart thing about the &lt;code&gt;matchSorter&lt;/code&gt; function is that it can sort through arrays of objects as well: You can tell which key you’d like it to sort through, and you can even provide specific criteria for each key: for example, you can choose to match exact case, or complete equality.&lt;/p&gt;

&lt;p&gt;Let’s see how one could implement this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const posts = [
  {title: ‘How I built my website’, excerpt: ‘Trying to figure out how to build my website was a challenge for me. As a web designer, I wanted it to be fully customisable, but I also wanted ease of use to reduce friction and make it easy to maintain.’, url: ‘https://felixrunquist.com/posts/how-i-built-my-website’},
  {title: ‘Implementing online payments with Stripe’, excerpt: ‘A tutorial on setting up a credit card payment form on your NextJS site. We will cover signing up and the Stripe SDK’, url: ‘https://felixrunquist.com/posts/online-payments-stripe’},
  {title: ‘Next.js HTTP Authentication with JWT and cookies’, excerpt: ‘I was recently trying to figure out a way to implement simple HTTP authentication for a personal Next.js project. I augmented the basic HTTP auth with JWT and cookies’, url: ‘https://felixrunquist.com/posts/next-js-http-authentication-jwt-cookies’},
  {title: ‘Creating and ordering a custom PCB’, excerpt: ‘As part of a home automation project, I wanted to house a thermometer, motion sensor and display in a small form factor. I decided to design and order my own PCB.’, url: ‘https://felixrunquist.com/posts/creating-and-ordering-custom-pcb’}
]//Retrieved by the CMS

import { matchSorter, rankings } from ‘match-sorter’
import {useState, useEffect} from ‘react’
export default function App(){
  const [input, setInput] = useState(‘\’)
  const [results, setResults] = useState([])
  useEffect(() =&amp;amp;gt; {
    var res = matchSorter(posts, input, {keys: [{key: ‘title’}, {key: ‘excerpt’}]})
    setResults(res)
  }, [input])
  return (
    &amp;amp;lt;&amp;amp;gt;
      &amp;lt;input type="text" value="{input}" oninput="{e"&amp;gt; setInput(e.target.value)}/&amp;amp;gt;
      {results.map(i =&amp;amp;gt; &amp;lt;a href="{i.url}" key="{i.title}"&amp;gt;&amp;lt;h2&amp;gt;{i.title}&amp;lt;/h2&amp;gt;
&amp;lt;p&amp;gt;{i.excerpt}&amp;lt;/p&amp;gt;&amp;lt;/a&amp;gt;)}
    &amp;amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;As the array to search through is being passed to the client, you probably don’t want to provide too large objects which could add to loading times, and cause performance issues. I’d limit myself to one thousand items!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We’ve now implemented a basic search function, that has an input, a function to display results, and a function that updates results when the input is written to. Let’s say the search page is at &lt;code&gt;www.example.com/search&lt;/code&gt;. What if one was to reload the page, or if one wanted to share the url to the exact results? One could store the current search state in the URL, with the query parameter. For instance, searching “Recipes” would dynamically update the url so that it points at &lt;code&gt;www.example.com/search?q=Recipes&lt;/code&gt;. Like that, refreshing or sharing the search results would bring you back to the exact same state.&lt;/p&gt;

&lt;p&gt;This can be done in React using &lt;code&gt;react-router-dom&lt;/code&gt; (it can also be done in Next.js using &lt;code&gt;useRouter&lt;/code&gt;), a library that manages URLs and paths. First you define a retrieval function, checking on page load whether a query string has been provided or not. When the text input is written to, you can dynamically update the query parameter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const posts = [
{title: ‘How I built my website’, excerpt: ‘Trying to figure out how to build my website was a challenge for me. As a web designer, I wanted it to be fully customisable, but I also wanted ease of use to reduce friction and make it easy to maintain.’, url: ‘https://felixrunquist.com/posts/how-i-built-my-website’},
{title: ‘Implementing online payments with Stripe’, excerpt: ‘A tutorial on setting up a credit card payment form on your NextJS site. We will cover signing up and the Stripe SDK’, url: ‘https://felixrunquist.com/posts/online-payments-stripe’},
{title: ‘Next.js HTTP Authentication with JWT and cookies’, excerpt: ‘I was recently trying to figure out a way to implement simple HTTP authentication for a personal Next.js project. I augmented the basic HTTP auth with JWT and cookies’, url: ‘https://felixrunquist.com/posts/next-js-http-authentication-jwt-cookies’},
{title: ‘Creating and ordering a custom PCB’, excerpt: ‘As part of a home automation project, I wanted to house a thermometer, motion sensor and display in a small form factor. I decided to design and order my own PCB.’, url: ‘https://felixrunquist.com/posts/creating-and-ordering-custom-pcb’}
]//Retrieved by the CMS

import { matchSorter, rankings } from ‘match-sorter’
import {useState, useEffect} from ‘react’
import { useSearchParams, BrowserRouter as Router} from “react-router-dom”;
export default function App(){
  return (
    &amp;lt;router&amp;gt;
      &amp;lt;search&amp;gt;&amp;lt;/search&amp;gt;
    &amp;lt;/router&amp;gt;
  )
}

function Search(){
  const [searchParams, setSearchParams] = useSearchParams();
  const [input, setInput] = useState(‘\’)
  const [results, setResults] = useState([])
  useEffect(() =&amp;amp;gt; {
    setInput(searchParams.get(‘search’))//Get search parameters on load
  },[])

  useEffect(() =&amp;amp;gt; {
    var res = matchSorter(posts, input, {keys: [{key: ‘title’}, {key: ‘excerpt’}]})
    setResults(res)
    if(input){//Update query parameters
      setSearchParams({ ‘search’: input });
    }
  }, [input])
  return (
    &amp;amp;lt;&amp;amp;gt;
      &amp;lt;input type="text" value="{input}" oninput="{e"&amp;gt; setInput(e.target.value)}/&amp;amp;gt;
      {results.map(i =&amp;amp;gt; &amp;lt;a href="{i.url}" key="{i.title}"&amp;gt;&amp;lt;h2&amp;gt;{i.title}&amp;lt;/h2&amp;gt;
&amp;lt;p&amp;gt;{i.excerpt}&amp;lt;/p&amp;gt;&amp;lt;/a&amp;gt;)}
    &amp;amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s all there is to it! Luckily, the &lt;code&gt;react-sorter&lt;/code&gt; package does most of the heavy lifting, we just did the content fetching and state implementation. Let me know if this was helpful by &lt;a href="https://twitter.com/intent/tweet?via=felixrunquist"&gt;reaching out to me on Twitter&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;External links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Match-sorter documentation: &lt;a href="https://github.com/kentcdodds/match-sorter"&gt;https://github.com/kentcdodds/match-sorter&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;URL-based state: &lt;a href="https://blog.logrocket.com/use-state-url-persist-state-usesearchparams/"&gt;https://blog.logrocket.com/use-state-url-persist-state-usesearchparams/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article was retrieved from &lt;a href="//felixrunquist.com"&gt;felixrunquist.com&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Web design inspiration: Ten perfect examples</title>
      <dc:creator>Felix</dc:creator>
      <pubDate>Sun, 25 Jun 2023 18:09:46 +0000</pubDate>
      <link>https://dev.to/felixrunquist/web-design-inspiration-ten-perfect-examples-1k0l</link>
      <guid>https://dev.to/felixrunquist/web-design-inspiration-ten-perfect-examples-1k0l</guid>
      <description>&lt;p&gt;In no particular order, here’s a collection of websites I find inspiring, both by their design and their functionality. For each website, I’ll dive into the specific details which made me want to mention it.&lt;/p&gt;

&lt;p&gt;While I do feature personal as well as corporate projects alike, I do my best to credit the original creator when possible. &lt;/p&gt;

&lt;h2&gt;
  
  
  1. Carl Hauser’s blog
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--MESTLLeP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://framerusercontent.com/modules/7PNMgDyVyfqJNJPq8jjk/iMldJIQl6nR2wARFNdpv/assets/aAJj18ZYaem8ZyGSvoFClgg1rY.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MESTLLeP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://framerusercontent.com/modules/7PNMgDyVyfqJNJPq8jjk/iMldJIQl6nR2wARFNdpv/assets/aAJj18ZYaem8ZyGSvoFClgg1rY.png" alt="" width="606" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.carlhauser.com/"&gt;theprocess&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the process. Every day sketches, brainfarts and ideas flying around in my backpack and apartment. I hope they can inspire or help someone out there.&lt;/p&gt;

&lt;p&gt;Carl Hauser’s website is a showcase of personal design products he has made. I love the minimalist aesthetic of the website: each element is separated into a rounded box, and the color combination of beige, black and yellow works really well. On the homescreen, you’re greeted by a page of rough sketches, the date and ID on top gives them an industrial feeling.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_cKyNcYW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-16-at-7.53.24-PM-1024x588.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_cKyNcYW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-16-at-7.53.24-PM-1024x588.png" alt="" width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The projects page lists more finished projects, with detailed design, color scheme and typography for each product. The intricate renders make you feel like you’re looking at a finished product, which could be the next product unveiled by Apple or Nothing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--opBw8HRC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-16-at-7.56.52-PM-1024x604.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--opBw8HRC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-16-at-7.56.52-PM-1024x604.png" alt="" width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Carl Hauser also has an &lt;a href="https://www.instagram.com/carlhauser/"&gt;Instagram page&lt;/a&gt; where he features his designs.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Sketch Blog
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ghc7Ehw9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://www.sketch.com/images/metadata/pages/beyond-the-canvas.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ghc7Ehw9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://www.sketch.com/images/metadata/pages/beyond-the-canvas.jpg" alt="" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.sketch.com/blog/"&gt;Beyond the Canvas · Sketch Blog&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Welcome to Beyond the Canvas from Sketch — get the latest design news, inspiration, resources and tutorials from a global team of design experts.&lt;/p&gt;

&lt;p&gt;I chose to feature the Sketch Blog not for the content, but for the design of the site – from its soft pastels to the well-chosen monospace heading font, the look and feel is stunning. The devil being in the details, I particularly like the way margins work out: the content margin is aligned with the header so they both start at the same position.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YiX_mfwd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-16-at-8.21.59-PM-899x1024.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YiX_mfwd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-16-at-8.21.59-PM-899x1024.png" alt="" width="800" height="911"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. JP Silva’s portfolio
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DHgmVHE4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-16-at-8.35.52-PM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DHgmVHE4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-16-at-8.35.52-PM.png" alt="" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.jp.works/"&gt;JP – Webflow Expert and Brand Designer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’m a skilled Webflow expert and brand designer specializing in creating premium templates for various industries. Please view my portfolio for examples of my work, and contact me to discuss your project.&lt;/p&gt;

&lt;p&gt;JP Silva’s website breaks the boundaries of web design and introduces interesting ways to navigate, from the rotating cubes that allude to Rubik’s cubes, to the scroll-based animations. The use of an uppercase font is interesting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7GfdwsSH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-16-at-8.35.52-PM-1024x589.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7GfdwsSH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-16-at-8.35.52-PM-1024x589.png" alt="" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Vita Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_ylfXTzH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://vitaarchitecture.com/share.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_ylfXTzH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://vitaarchitecture.com/share.jpg" alt="" width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vitaarchitecture.com/"&gt;Vita Architecture | High-End Residential Architects in London&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vita Architecture is a boutique architectural practice in London &amp;amp; Surrey. Our projects are bespokely designed to deliver innovative and crafted architecture.&lt;/p&gt;

&lt;p&gt;Vita Architecture is a great example of motion-first design. From the moment you hit “enter” in the search bar, you’re transported to a site with flowing visuals and extensive imagery. Anything from scrolling to clicking a link provides a contextual animation which makes the entire site feel fluid. Plus, this website has been previously featured by Apple in a product announcement!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SUJcELhO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-16-at-8.49.07-PM-1024x556.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SUJcELhO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-16-at-8.49.07-PM-1024x556.png" alt="" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The only critique I have is the customized scrolling behavior which is jarring – I don’t recommend changing the default scrolling behavior (the speed/direction) as it feels counter-intuitive for the user who is used to the way native scrolling works on their device. It can feel like wading through sludge!&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Outreach.space
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--IgoEBPvU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://www.outreach.space/assets/seo/oumuamua.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IgoEBPvU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://www.outreach.space/assets/seo/oumuamua.jpg" alt="" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.outreach.space/"&gt;Outreach. A Mystery from Beyond.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Inspired by the true events surrounding 1I/’Oumuamua, the first interstellar object ever observed by humankind passing through the solar system. At the time of the sighting, the object has already passed the closest point in its orbit to Earth; soon it will be out of telescopic range. Its explorati…&lt;/p&gt;

&lt;p&gt;Outreach tells the story of a space voyage, and the discovery of a weird asteroid. It makes you feel like being teleported through space, and the delightful visuals interspersed with&lt;/p&gt;

&lt;p&gt;I love the use of Garamond, reminiscent of the typeface Apple used in the 80s. Plus, this site is built on Next.js and hosted on Vercel, just like mine, so it’s nice to see what can be done with these frameworks – there really is no limit!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mZ22cO77--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-17-at-1.19.35-PM-1024x569.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mZ22cO77--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-17-at-1.19.35-PM-1024x569.png" alt="" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The only disturbing thing is the loading modal – it isn’t interactive and scrolling on isn’t disabled when the modal displays. Since it’s a content-heavy site and therefore takes a while to load, the loading modal was displayed for a while – I thought the site was broken – providing more intent as to the loading state, for example an animation, could help with this.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Tinkersynth by Josh W. Comeau
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---OQ2Y_MW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://storage.googleapis.com/tinkersynth-email-assets/og-sample-image.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---OQ2Y_MW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://storage.googleapis.com/tinkersynth-email-assets/og-sample-image.png" alt="" width="630" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tinkersynth.com/"&gt;Generative Art Machine&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tinkersynth is an experimental art project. Create unique designs by manipulating whimsical machines and making serendipitous discoveries.&lt;/p&gt;

&lt;p&gt;Tinkersynth is a website that lets you create visual art by using a waveform generator, much like you would use a synthesizer to generate sounds. It’s a fun tool, with many whimsical details built into it, like each button having a little animation on click.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GuwSNqpM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-16-at-8.06.18-PM-837x1024.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GuwSNqpM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-16-at-8.06.18-PM-837x1024.png" alt="" width="800" height="979"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The skeuomorphic control panel is a fun way visualize the effects that can be applied – the little animations as you click and press on the sliders make you want to tinker with it even more. I can’t imagine how complicated everything must’ve been to code – handling all the states, transitions and animations!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Tinkersynth was built by &lt;a href="https://www.joshwcomeau.com/"&gt;Josh W. Comeau&lt;/a&gt;, a React developer and course writer.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Atmos
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jhTywBc9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://atmos.leeroy.ca/social.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jhTywBc9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://atmos.leeroy.ca/social.jpg" alt="" width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://atmos.leeroy.ca/"&gt;Atmos&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Get on board and discover the most surreal facts about the aviation industry!&lt;/p&gt;

&lt;p&gt;Atmos is an interactive experience built on Three.js. From the grainy yet simple visuals to the relaxing sound effects, It was developed by &lt;a href="https://leeroy.ca/"&gt;Leeroy&lt;/a&gt;, a Canadian web design agency. I particularly like the choice of colors – soft blues and warm orange tones which make you feel like you’re up in the sky. The airy music is well chosen to add to the effect, it almost makes you feel like you’re in a meditation session.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3vd9ptrn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-25-at-1.19.05-PM-1024x568.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3vd9ptrn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-25-at-1.19.05-PM-1024x568.png" alt="" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.awwwards.com/case-study-atmos.html"&gt;This post&lt;/a&gt; on Awwwards delves into the design of the site and some of the choices that were made, from the path the airplane follows to the bubbly clouds. It’s truly amazing that such an immersive experience can be created using only web technologies.&lt;/p&gt;

&lt;p&gt;P.S: try to spot the easter egg!&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Rauno Freiburg’s portfolio
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--heDye8sQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/og.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--heDye8sQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/og.png" alt="" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://rauno.me/"&gt;Rauno Freiberg&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Creating software that makes people feel something.&lt;/p&gt;

&lt;p&gt;I love the shader effects, how it fades away to reveal the content when you scroll. The navigation is unconventional (it resembles the MacOS dock), but certainly functional! Rauno has rethought how users should interact with elements. The &lt;a href="https://rauno.me/craft"&gt;crafts page&lt;/a&gt; features a feed on what he is working on, and I love the interactive demos! He’s also built separate projects, &lt;a href="https://uiw.tf/"&gt;uiw.tf&lt;/a&gt; and &lt;a href="https://ui.gallery/"&gt;ui.gallery&lt;/a&gt;, which explore unconventional yet intuitive ways to interact with visual content.&lt;/p&gt;

&lt;p&gt;The more time you spend on the site, the more details you see – like sound effects or the “last visit” location indicator: it fills you with a moment of unbridled glee to see your location as last visited when you refresh the page (unless you use a VPN, of course).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--1nY4mUXA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-20-at-6.11.26-PM-1024x479.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1nY4mUXA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-20-at-6.11.26-PM-1024x479.png" alt="" width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Cobbler
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ITPknDrW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://cdn.sanity.io/images/wzmqt15u/production/32986d74a8aabafe3ce04b2f1d64fe07ed9f27b0-1200x630.png%3Fw%3D1200" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ITPknDrW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://cdn.sanity.io/images/wzmqt15u/production/32986d74a8aabafe3ce04b2f1d64fe07ed9f27b0-1200x630.png%3Fw%3D1200" alt="" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.cobbler.app/"&gt;Cobbler&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For reps, Cobbler is your producers’ best friend. For studios, Cobbler is your rep.&lt;/p&gt;

&lt;p&gt;Something very striking is the bold color palette the designers chose to go with this site. Surprisingly enough, the saturated blue and pale green work extremely well together! The bold, outlined shapes and grainy backgrounds add to the look, and it makes for a very polished site. I’m obsessed with the font they use, Helveesti, which has mini serifs and a slight tapers on vertical stems.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KLfkUDYf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-20-at-6.16.09-PM-1024x384.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KLfkUDYf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-20-at-6.16.09-PM-1024x384.png" alt="" width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If anything, the animated visuals and colorful elements &lt;em&gt;could&lt;/em&gt; be drawing the user away from the actual content (case in point: I’ve scrolled through the website, visited a few pages, yet I can’t remember what services Cobbler provides…).&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Cosmos.so
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KMjA8_ue--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://www.cosmos.so/wp-content/uploads/2023/06/manifesto-ogimg-1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KMjA8_ue--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://www.cosmos.so/wp-content/uploads/2023/06/manifesto-ogimg-1.jpg" alt="" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.cosmos.so/manifesto"&gt;Manifesto — Cosmos&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You are now entering the Cosmos. Where ideas are sacred, exploration is infinite and connection is cosmic. A future where free expression is possible for every curator in the universe.&lt;/p&gt;

&lt;p&gt;Cosmos is positioning itself as the new Pinterest alternative. To showcase this, they’ve built an interactive website. In particular: the manifesto page. It’s such a calming experience, from the music to the scroll-controlled timeline, which makes a flower bloom in the background.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bIpSNK22--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-25-at-1.26.59-PM-1024x569.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bIpSNK22--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-25-at-1.26.59-PM-1024x569.png" alt="" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The dark/light theme transition is very well thought-out, the entire experience feels delightfully intuitive. An issue to point-out accessibility-wise: the scroll bar is customized as well as the scrolling behavior – this can make the user feel unsafe from the native implementations.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. A bonus one: Bento layouts
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--IeoVZSh1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://framerusercontent.com/modules/71JbRbP7TjNqc6Oou7DT/eIlCIdQiqtHMH1TXuoEr/assets/tNv1ZABhaWDfKdCQrkAUT3AzoU.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IeoVZSh1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://framerusercontent.com/modules/71JbRbP7TjNqc6Oou7DT/eIlCIdQiqtHMH1TXuoEr/assets/tNv1ZABhaWDfKdCQrkAUT3AzoU.jpg" alt="" width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bentobox.framer.website"&gt;My Bento Box&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s a bonus one. This is less of a particular site, but more of a general design trend that has blew up lately on Twitter. After Apple has begun using groups of little cards in a grid to summarize what has been presented, people followed suit and the form has been termed “bento box”: A group of rectangular elements in a grid. The site I’ve linked is an excellent example of Bento Boxes, and I’ll share some other ones below.&lt;/p&gt;

&lt;p&gt;There’s even a site to make your link in bio as a bento: &lt;a href="https://bento.me/en/home"&gt;bento.me&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JS0GGk0h--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/image-1024x576.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JS0GGk0h--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/image-1024x576.png" alt="" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A classic: The bento layout from Apple&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--1uDDvmeP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-20-at-8.41.11-PM-1024x928.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1uDDvmeP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-20-at-8.41.11-PM-1024x928.png" alt="" width="800" height="725"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Another layout by Bolt&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--caCIyHdL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Redesign-1024x518.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--caCIyHdL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Redesign-1024x518.png" alt="" width="800" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A layout I came up with for my site redesign&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This article was retrieved from &lt;a href="https://felixrunquist.com"&gt;felixrunquist.com&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Website update: code blocks, search, lightbox and more!</title>
      <dc:creator>Felix</dc:creator>
      <pubDate>Sun, 18 Jun 2023 17:13:45 +0000</pubDate>
      <link>https://dev.to/felixrunquist/website-update-code-blocks-search-lightbox-and-more-3k7m</link>
      <guid>https://dev.to/felixrunquist/website-update-code-blocks-search-lightbox-and-more-3k7m</guid>
      <description>&lt;p&gt;Since I got my website to a relative production-ready stage last year, I wanted to release an update to my website to work on a few much-needed improvements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revamped post experience
&lt;/h2&gt;

&lt;p&gt;I’ve always felt like my posts layout has felt pretty bland, which doesn’t integrate with the rest of my website design. I started off building my website from Next.js’ &lt;a href="https://github.com/vercel/next.js/tree/canary/examples/cms-wordpress"&gt;WordPress example&lt;/a&gt;, which, although modern in its design, is very minimal. I decided to add a few features to build on my existing content:&lt;/p&gt;

&lt;h3&gt;
  
  
  Code blocks
&lt;/h3&gt;

&lt;p&gt;As I constantly write posts about programming, code blocks are an elemental part of my posts. The code blocks I started off with use &lt;a href="https://prismjs.com/"&gt;Prism.js&lt;/a&gt;, however I knew from the beginning that this was a temporary solution. While it does support a lot of languages for syntax highlighting, it lacks support for things such as file trees, and interactive code playgrounds. A package that has been &lt;a href="https://www.joshwcomeau.com/react/next-level-playground/"&gt;all&lt;/a&gt; &lt;a href="https://blog.maximeheckel.com/design/"&gt;the&lt;/a&gt; &lt;a href="https://bestofjs.org/projects/sandpack"&gt;rave&lt;/a&gt; in the dev environment as of late is &lt;a href="https://sandpack.codesandbox.io/"&gt;Sandpack&lt;/a&gt; by CodeSandbox. It’s a Javascript-oriented playground that features the same in-browser bundler used on CodeSandbox for running client-side node.js. It’s very performant and has excellent code formatting and file tree support. The only thing it lacks is support for languages other than Javascript, say if you want to use it not for the previews but just for code blocks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2JAwRjQ5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zpcg7khxuajgxmnowyb6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2JAwRjQ5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zpcg7khxuajgxmnowyb6.png" alt="Image description" width="800" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Full-bleed images and lightbox
&lt;/h3&gt;

&lt;p&gt;Most online reading experiences feature &lt;em&gt;full-bleed&lt;/em&gt; images, meaning that images in a post expand to the size of the browser content while the text remains within its container (it’s not a good practice to have text span across wide screens as it makes it hard to read). I implemented a CSS class for these kind of images.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--wRvRgtVN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Artboard.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wRvRgtVN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Artboard.png" alt="" width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another feature on sites such as Medium is that one can click on images in a post to expand them. This is called a Lightbox. You can test out the Lightbox functionality on my website at &lt;a href="https://felixrunquist.com/posts/a-website-update"&gt;the original article&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Post headers
&lt;/h3&gt;

&lt;p&gt;Something I didn’t like with Next.js’ wordpress example is the look of posts. Everything is huge, and while that might be the look they are going for, it’s tiring having to scroll everywhere. For instance, the cover image is positioned under the fold, which means that a visitor needs to scroll before deciding if they want to read the post based on the title and cover image. I decided on a responsive design to make sure the cover image is above the fold on all devices.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PZwwkVJ5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/chrome-1024x679.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PZwwkVJ5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/chrome-1024x679.png" alt="" width="800" height="530"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Next.js’ blog example with cover image under the fold&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZEBe3SMk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Chrome-2-1024x644.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZEBe3SMk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Chrome-2-1024x644.png" alt="" width="800" height="503"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;My updated design&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I also decided to include small flourishes such as drop caps, which felt authentic to my design.&lt;/p&gt;
&lt;h3&gt;
  
  
  Post categories and search
&lt;/h3&gt;

&lt;p&gt;Something I’ve wanted to do from day one is to implement a post search feature. I initially thought it would be quite time consuming, with hacky &lt;code&gt;fetch&lt;/code&gt;es and asynchronous calls to my CMS, but I realized that I could do all the searches client-side with Kent C. Dodd’s &lt;code&gt;match-sorter&lt;/code&gt; package. I also wanted to make post categories visible to users. Since they are already implemented in the CMS I use (WordPress) I only had to do half the work and implement it client-side. Since I have a lot of whitespace, I found a space for my search bar and categories elements.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qF0MTxdE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-10-at-9.19.47-PM-1024x894.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qF0MTxdE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-10-at-9.19.47-PM-1024x894.png" alt="" width="800" height="698"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The search bar being used&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4MiJ4c94--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-10-at-9.20.43-PM-1024x957.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4MiJ4c94--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-10-at-9.20.43-PM-1024x957.png" alt="" width="800" height="748"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Categories being displayed&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Header and footer
&lt;/h2&gt;

&lt;p&gt;I wasn’t very happy with the header transition I have – as you scroll and it collapses, it’s always felt a little jittery. I replaced my trigger-based animation (i.e. when you scroll past a certain point, the size transitions) with a scroll-based animation (i.e. the scroll controls the animation progress). Here’s the difference:&lt;/p&gt;

&lt;p&gt;/App.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import ‘./styles.css’
import { useState, useEffect } from ‘react’
export default function App(){
const [expanded, setExpanded] = useState(true);
useEffect(() =&amp;amp;gt; {
document.addEventListener(‘scroll’, scroll)
return ()=&amp;amp;gt;{
  document.removeEventListener(‘scroll’, scroll)
}
},[])
function scroll(){
setExpanded(window.scrollY &amp;amp;lt;= 50)
}
return (
&amp;lt;div classname="container"&amp;gt;
  &amp;lt;div classname="{'header'" expanded :&amp;gt;
    &amp;lt;h1&amp;gt;Header &amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;{expanded ? ‘Expanded’ : ‘Collapsed’}&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;/styles.css:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;body {margin: 0; }
* {font-family: sans-serif; }
.container {width: 100%; height: 200%; height: 200vh; }
.header {width: 100%; height: 4rem; background: #8CC0DE; transition: height .3s ease-out; position: fixed; }
.header p, .header h1 {margin: .5rem 0; display: inline-block; }
.header.expanded {height: 8rem; }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trigger-based animation&lt;/p&gt;

&lt;p&gt;/App.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import ‘./styles.css’
import { useState, useEffect } from ‘react’
export default function App(){
const [progress, setProgress] = useState(0);
useEffect(() =&amp;amp;gt; {
document.addEventListener(‘scroll’, scroll)
return ()=&amp;amp;gt;{
  document.removeEventListener(‘scroll’, scroll)
}
},[])
function scroll(){
setProgress(Math.max(Math.min(window.scrollY / 50, 1), 0))
}
return (
&amp;lt;div classname="container"&amp;gt;
  &amp;lt;div classname="{'header'}" style="{{'--progress':" progress&amp;gt;
    &amp;lt;h1&amp;gt;Header &amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;Progress: {progress}&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;styles.css:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;body {margin: 0; }
* {font-family: sans-serif; }
.container {width: 100%; height: 200%; height: 200vh; }
.header {width: 100%; height: 4rem; background: #8CC0DE; position: fixed; }
.header p, .header h1 {margin: .5rem 0; display: inline-block; }
.header {height: calc((1 – var(—progress)) * 8rem + var(—progress) * 4rem); }

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scroll-based animation&lt;/p&gt;

&lt;p&gt;Notice how in the first example, the header stays expanded, right until you scroll past a certain point? The scroll-based animation feels much more responsive as it adapts to your scrolling speed.&lt;/p&gt;

&lt;p&gt;When it comes to the footer, I never got to finishing it. I spent time positioning the links to social media and other items I wanted to feature.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gi-EqeZd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-11-at-2.33.21-PM-1024x498.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gi-EqeZd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-11-at-2.33.21-PM-1024x498.png" alt="" width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Design
&lt;/h2&gt;

&lt;p&gt;To make the colors and fonts I use on this website easily retrievable, I made a design page which groups common elements. You can find the page &lt;a href="https://felixrunquist.com/design"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SanFn66B--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-17-at-5.43.35-PM-1024x457.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SanFn66B--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-17-at-5.43.35-PM-1024x457.png" alt="" width="800" height="357"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A few elements featured on my design page&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A never-ending journey
&lt;/h2&gt;

&lt;p&gt;I believe a website is never fully finished, and there are always tweaks that can be made. I’ve compiled a list of things I’d like to change, and you can track my progress &lt;a href="https://felixrunquist.com/roadmap"&gt;here&lt;/a&gt;. As a bit of fun, I wanted to explore the current trend that are &lt;a href="https://bentogrids.com/"&gt;bento grids&lt;/a&gt; and create one featuring everything I’ve changed as part of the redesign.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jbCEJZUR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/img/bento-redesign.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jbCEJZUR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/img/bento-redesign.png" alt="" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This article was retrieved from &lt;a href="https://felixrunquist.com"&gt;felixrunquist.com&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Automatically cross-publishing posts to dev.to with RSS</title>
      <dc:creator>Felix</dc:creator>
      <pubDate>Thu, 15 Jun 2023 17:02:32 +0000</pubDate>
      <link>https://dev.to/felixrunquist/automatically-cross-publishing-posts-to-devto-with-rss-12em</link>
      <guid>https://dev.to/felixrunquist/automatically-cross-publishing-posts-to-devto-with-rss-12em</guid>
      <description>&lt;p&gt;While one might think that cross-publishing from your own site to other platform might diminish the reach of your own website, there’s actually a way to do this while boosting your site’s credibility. Plus, this can also be done (semi-)automatically! &lt;a href="https://dev.to"&gt;Dev.to&lt;/a&gt; is a developer-centric social network. It’s a place where people can publish and read articles about software development.&lt;/p&gt;

&lt;p&gt;Cross-publishing, or syndication, is the act of publishing a post on your website onto third-party sites, such as dev.to. This generally improves the visibility of your website, as long as it’s done right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Canonical links
&lt;/h2&gt;

&lt;p&gt;Crawlers rely on links to index websites. Used as a way to indicate duplicate content, canonical links also provide an indicator to crawlers to instead direct users to the provided url when they search for the page:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;link rel="canonical" href="https://felixrunquist.com" /&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Dev.to provides a setting to enable canonical links on articles you publish on the platforms. This means that crawlers will see that article as if it was published on your website, and you will benefit from the enhanced visibility that comes from publishing on a developer platform!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Beware! If you don’t provide a canonical link, the cross-published content could rank higher in search results than your website, which would drive traffic away!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Auto-publishing from your RSS feed
&lt;/h2&gt;

&lt;p&gt;In order for dev.to to automatically detect when you’ve posted on your website, you can provide it an RSS feed for it to fetch content from (if you don’t have an RSS feed, fret not, I explain what it is and how to set one up with Next.js in &lt;a href="https://felixrunquist.com/posts/generating-sitemap-rss-feed-with-next-js"&gt;this article&lt;/a&gt;). It’s a matter of heading to Settings &amp;gt; Extensions, in dev.to, and scrolling to the “Publishing to DEV Community from RSS” section. Here, you can provide the link to your RSS feed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LNd14dI1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-05-at-7.17.50-PM-1024x1000.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LNd14dI1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-05-at-7.17.50-PM-1024x1000.png" alt="" width="800" height="781"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The RSS section in dev.to settings&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You can click on the “Fetch Now” button for the site to retrieve the latest content. Content isn’t published automatically, but it is added into your drafts. I selected the option to mark the RSS source as canonical, which will let dev.to automatically handle the canonical links I mentioned earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing posts
&lt;/h2&gt;

&lt;p&gt;In order for the site to accurately fetch content from your website, you need to have a few fields configured in your RSS feed for each post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; – this will be used for your post’s title&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; will be used for cross-linking back to the post &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;description&amp;gt;&lt;/code&gt; contains a brief description of the post&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;content&amp;gt;&lt;/code&gt; has the actual post content. You can provide it as raw text, but you can also put html content in a &lt;code&gt;&amp;lt;cotnent:encoded&amp;gt;&lt;/code&gt; tag. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the post lands in your dev.to drafts (visible by going to your dashboard), all that’s left to do is to preview the article to make sure that everything is formatted correctly, as there could be a few issues especially if you used highly-customized elements. Next, change the &lt;code&gt;published&lt;/code&gt; attribute in the Markdown metadata to true, and it will be visible to others once you hit save!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you’re unfamiliar with Markdown and are unsure of the syntax, the dropdown under “Editor Basics” in the editor is extremely helpful!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ca-48aAX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-05-at-7.26.52-PM-1024x264.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ca-48aAX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-05-at-7.26.52-PM-1024x264.png" alt="" width="800" height="206"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The markdown metadata of a published post&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Generating a sitemap and RSS feed with Next.js</title>
      <dc:creator>Felix</dc:creator>
      <pubDate>Sun, 04 Jun 2023 14:33:25 +0000</pubDate>
      <link>https://dev.to/felixrunquist/generating-a-sitemap-and-rss-feed-with-nextjs-mga</link>
      <guid>https://dev.to/felixrunquist/generating-a-sitemap-and-rss-feed-with-nextjs-mga</guid>
      <description>&lt;p&gt;There are many options for generating content with Next.js. These different methods can be used for automatically generating sitemaps and RSS feeds based on content fetched from a CMS. I’ll explore the different options available, their pros and cons.&lt;/p&gt;

&lt;h2&gt;
  
  
  The difference between Statically and Dynamically rendered content
&lt;/h2&gt;

&lt;p&gt;Next.js terminology can be a little befuddling. To most people, SSR, SSG and ISR are nothing but meaningless acronyms. Fundamentally, Next.js projects can serve content in three different ways: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static content&lt;/strong&gt; : These are files put in the /public directory and are served by next.js as they are. For example, an image stored at /public/image.png will be served at &lt;a href="http://www.example.com/image.png"&gt;www.example.com/image.png&lt;/a&gt;. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build-time content&lt;/strong&gt; : This content is created at built time. On a page, this is characterized by exporting a getStaticProps function. In Next.js jargon, they call it &lt;a href="https://nextjs.org/docs/pages/building-your-application/rendering/static-site-generation"&gt;Static Site Generation (SSG)&lt;/a&gt;, a name I don’t find very appropriate as the content isn’t completely static (it’s &lt;em&gt;built&lt;/em&gt;), and the entire site doesn’t have to use this. It would be more appropriate to call this Build Generation. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic content&lt;/strong&gt; : This content is created every time someone visits a page. For example, this is what a PHP-based site does for every page written in PHP. On a Next.js page, this is characterized by exporting a getServerSideProps function. It’s commonly called &lt;a href="https://nextjs.org/docs/pages/building-your-application/rendering/server-side-rendering"&gt;Server Side Rendering (SSR)&lt;/a&gt;, which isn’t very appropriate as it implies that other forms of generating content don’t involve any action from the server. I’ll call this Dynamic Generation. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As for &lt;a href="https://nextjs.org/docs/pages/building-your-application/rendering/incremental-static-regeneration"&gt;Incremental Static Regeneration (ISR)&lt;/a&gt;, it’s a balance between build-time and dynamic content: The content is generated at build time, but is regenerated in the background on every request (meaning that the visitor doesn’t get disrupted), in a specified window.&lt;/p&gt;

&lt;p&gt;This is done by specifying a revalidate time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export async function getStaticProps() {
  const res = await fetch(‘https://…/posts’);
  const posts = await res.json();

  // If the request was successful, return the posts
  // and revalidate every 10 seconds.
  return {
    props: {
      posts,
    },
    revalidate: 10, //This is the revalidate time in seconds
  };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Phew, that was a lot of information! Now we can finally get to the bottom of sitemaps and rss feeds. For each content serving method, one can serve files such as sitemaps or RSS feeds in an equal way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bS159H1g--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-04-at-3.50.13-PM-1024x749.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bS159H1g--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/06/Screenshot-2023-06-04-at-3.50.13-PM-1024x749.png" alt="" width="800" height="585"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Methods of serving built XML files&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What is a sitemap?
&lt;/h2&gt;

&lt;p&gt;A sitemap is an XML-formatted file that is basically an index of your site: It provides a way for crawlers (search engine robots) to see the pages available on your site, and index them properly. This improves visibility to search engines.&lt;/p&gt;
&lt;h2&gt;
  
  
  Static sitemaps
&lt;/h2&gt;

&lt;p&gt;The first, most obvious option is to hand-code a sitemap and have it live in your /public folder. Here’s an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;
&amp;lt;urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"&amp;gt;
&amp;lt;url&amp;gt;
&amp;lt;loc&amp;gt;https://felixrunquist.com&amp;lt;/loc&amp;gt;
&amp;lt;/url&amp;gt;
&amp;lt;url&amp;gt;
&amp;lt;loc&amp;gt;https://felixrunquist.com/posts&amp;lt;/loc&amp;gt;
&amp;lt;/url&amp;gt;
&amp;lt;url&amp;gt;
&amp;lt;loc&amp;gt;https://felixrunquist.com/contact&amp;lt;/loc&amp;gt;
&amp;lt;/url&amp;gt;
&amp;lt;url&amp;gt;
&amp;lt;/urlset&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, the obvious issue here is that it every time you add a new page to your site, or if you fetch content via CMS (Content Management System), you need to remember to hand-code the changes yourself. This can get quite tricky and time-consuming. &lt;/p&gt;

&lt;h2&gt;Building dynamic sitemaps&lt;/h2&gt;

&lt;p&gt;The next example is to add a &lt;code&gt;sitemap.xml.js&lt;/code&gt; file in pages and use Dynamic Generation (or SSR if you prefer) using getServerSideProps. Since that function is run on every request, a req and res object is passed to the function, like an API, the &lt;code&gt;res&lt;/code&gt; object is used to specify that the file type is XML and it is used to end the request before the main function is run. This is a method mentioned in &lt;a href="https://nextjs.org/learn/seo/crawling-and-indexing/xml-sitemaps"&gt;Next.JS documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;/pages/sitemap.xml.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const URL = 'https://felixrunquist.com'
function generateSiteMap(posts) {
  return \`&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;
   &amp;lt;urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"&amp;gt;
     &amp;lt;url&amp;gt;
       &amp;lt;loc&amp;gt;https://felixrunquist.com&amp;lt;/loc&amp;gt;
     &amp;lt;/url&amp;gt;
     &amp;lt;url&amp;gt;
       &amp;lt;loc&amp;gt;https://felixrunquist.com/posts&amp;lt;/loc&amp;gt;
     &amp;lt;/url&amp;gt;
     \${posts
       .map(({ id }) =&amp;gt; {
         return \`
       &amp;lt;url&amp;gt;
           &amp;lt;loc&amp;gt;\${\`\${URL}/\${id}\`}&amp;lt;/loc&amp;gt;
       &amp;lt;/url&amp;gt;
     \`;
       })
       .join('')}
   &amp;lt;/urlset&amp;gt;
 \`;
}

export default function SiteMap() {//This function is there just to avoid compile errors
}

export async function getServerSideProps({ res }) {
  const posts = await fetchPosts();//External function to fetch posts

  const sitemap = generateSiteMap(posts);// generate the XML sitemap with the posts data

  res.setHeader('Content-Type', 'text/xml');// send the XML to the browser
  res.write(sitemap);
  res.end();

  return {
    props: {},
  };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, if you’re fetching data from a CMS, such as custom posts, this means that for every request to your Next.js site, there will also be one to the CMS which greatly increases overhead. This also affects loading times, as the sitemap won’t be served until all the content is fetched and built. &lt;/p&gt;

&lt;h2&gt;The best of both worlds&lt;/h2&gt;

&lt;p&gt;For my sitemap, I didn’t like the idea of having to manually update it every time, nor did I like the added overhead with Dynamic Generating. I found a way (&lt;a href="https://github.com/vercel/next.js/blob/canary/examples/with-sitemap/scripts/generate-sitemap.js"&gt;advocated by Next.js&lt;/a&gt;) to generate a &lt;code&gt;sitemap.xml&lt;/code&gt; file in the &lt;code&gt;/public&lt;/code&gt; folder at build time. However, their method modifies the &lt;code&gt;next.config.js&lt;/code&gt; file to run the function as a script. &lt;/p&gt;

&lt;p&gt;Instead, I decided to call the function in the getStaticProps function of my &lt;code&gt;index.js&lt;/code&gt; page. This means that the sitemap will be generated at build time, but I can also add a revalidate time in order to use incremental regeneration if I wish. &lt;/p&gt;

&lt;p&gt;This is the idea: At build-time (or on regeneration), fetch the Next.js page content as well as any CMS page content, then compile XML sitemap code. Using the &lt;code&gt;fs&lt;/code&gt; module, this will be written to pages/sitemap.xml.&lt;/p&gt;

&lt;p&gt;/public/robots.txt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-agent: *
Allow: /

Sitemap: https://felixrunquist.com/sitemap.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;/pages/index.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import {generateSitemap} from 'lib/generate.js'
export function getStaticProps(){
  generateSitemap();
  return {
    props: {}
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;/lib/generate.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import fs from 'fs'
const SITE_PATH = 'https://felixrunquist.com';

function getSitemapXML(posts) {
  return \`&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;
   &amp;lt;urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"&amp;gt;
     &amp;lt;url&amp;gt;
       &amp;lt;loc&amp;gt;\${SITE_PATH}&amp;lt;/loc&amp;gt;
     &amp;lt;/url&amp;gt;
     &amp;lt;url&amp;gt;
       &amp;lt;loc&amp;gt;\${SITE_PATH}/posts&amp;lt;/loc&amp;gt;
     &amp;lt;/url&amp;gt;
     &amp;lt;url&amp;gt;
       &amp;lt;loc&amp;gt;\${SITE_PATH}/contact&amp;lt;/loc&amp;gt;
     &amp;lt;/url&amp;gt;
     &amp;lt;url&amp;gt;
       &amp;lt;loc&amp;gt;\${SITE_PATH}/showcases&amp;lt;/loc&amp;gt;
     &amp;lt;/url&amp;gt;
     \${posts
       .map(({ node }) =&amp;gt; {
         return \`
       &amp;lt;url&amp;gt;
           &amp;lt;loc&amp;gt;\${SITE_PATH + '/posts/' + node.slug}&amp;lt;/loc&amp;gt;
       &amp;lt;/url&amp;gt;
     \`;
       })
       .join('')}
   &amp;lt;/urlset&amp;gt;
 \`;
}

export default async function generateSitemap(){
  const posts = await getPosts()//Fetch posts via API


  const sitemap = getSitemapXML(allPosts.edges);// Generate the XML sitemap with post data
  fs.writeFileSync('public/sitemap.xml', sitemap);//Write file as a statically-served file
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To indicate to search engines where your sitemap file is, you also need to add a robots.txt file. Since this will be changed rarely, I added this as a static asset. &lt;/p&gt;

&lt;h2&gt;What about RSS feeds? &lt;/h2&gt;

&lt;p&gt;An RSS (Really Simple Syndication) feed is a file that lists the posts and pages of your website in a computer-readable XML format, which makes a standardized format for notifying other sites and services that you’ve published new content. This is most useful if you publish blog posts or other dynamic content regularly. &lt;/p&gt;

&lt;p&gt;To serve this file, I’m going to use the example using Build Generation, which is the best option, in my opinion.&lt;/p&gt;

&lt;p&gt;I used the &lt;a href="https://www.npmjs.com/package/feed"&gt;feed&lt;/a&gt; package which makes exporting RSS data as XML really easy. &lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import fs from 'fs'
import { Feed } from 'feed'; //Uses the RSS feed generator library
import {SITE_PATH, SITE_DESC} from './constants';

export default async function generateRSS() {
   const feedOptions = {
    title: 'Posts | RSS Feed',
    description: SITE_DESC,
    id: SITE_PATH,
    link: SITE_PATH,
    image: \`\${SITE_PATH}/og-image.png\`,
    favicon: \`\${SITE_PATH}/favicon/favicon-32x32.png\`,
    copyright: \`All rights reserved \${new Date().getFullYear()}\`,
    generator: 'Feed for Node.js'
   };

   const feed = new Feed(feedOptions);
   const posts = await getPosts();//Fetch posts from API
   posts.edges.forEach(({node}) =&amp;gt; {
     feed.addItem({
       title: node.title,
       id: \`\${SITE_PATH}/posts/${node.slug}\`,
       link: \`\${SITE_PATH}/posts/${node.slug}\`,
       description: node.excerpt,
       content: node.content,
       date: new Date(node.date),
      });
   });
   fs.writeFileSync('public/feed.xml', feed.rss2());//Write as static asset

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also add a &lt;code&gt;link&lt;/code&gt; tag to your header to indicate the presence of an RSS feed to crawlers and RSS readers: &lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;link rel="alternate" href="feed.xml" type="application/rss+xml" title="RSS feed for posts" /&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The RSS can also used to cross-publish to different platforms such as &lt;a href="https://dev.to" rel="noreferrer noopener"&gt;dev.to&lt;/a&gt;, which I will explain in a future post. &lt;/p&gt;



</description>
    </item>
    <item>
      <title>Customizing WordPress GraphQL with custom fields</title>
      <dc:creator>Felix</dc:creator>
      <pubDate>Wed, 31 May 2023 15:47:08 +0000</pubDate>
      <link>https://dev.to/felixrunquist/customizing-wordpress-graphql-with-custom-fields-598m</link>
      <guid>https://dev.to/felixrunquist/customizing-wordpress-graphql-with-custom-fields-598m</guid>
      <description>&lt;p&gt;If you’ve used WordPress for a while, and have ever wanted to add custom metadata to a post, such as a caption/subtitle for example, this can be done with something called “custom fields”. However, enabling them in WordPress doesn’t mean they’re visible to a static site using GraphQL to fetch data from WordPress.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a static website?
&lt;/h2&gt;

&lt;p&gt;A “static” site is a site made of code that doesn’t change once it’s built. Think of it as a book: after printing, the content doesn’t change. This has many advantages over dynamic sites such as WordPress:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Speed: Since the content doesn’t change, multiple copies of the site can be stored on multiple servers, so that the user gets served from the closest server, reducing latency.&lt;/li&gt;
&lt;li&gt;Better SEO: As the content can be served served quicker without having to use javascript, your website can be higher up in search results&lt;/li&gt;
&lt;li&gt;Security: The site doesn’t contain databases visible to users which they potentially gain access to, and other vulnerabilities. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a while, having a static site meant that whenever you want to change anything, you would have to manually go through each HTML file and edit each change manually. This has changed with the arrival of services such as &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; and &lt;a href="https://www.gatsbyjs.com/" rel="noopener noreferrer"&gt;Gatsby&lt;/a&gt;, which can retrieve external data and create HTML files from them at build time.&lt;/p&gt;

&lt;p&gt;I personally have a Next.js website with WordPress as the CMS. They interface with &lt;a href="https://www.wpgraphql.com/" rel="noopener noreferrer"&gt;WPGraphQL&lt;/a&gt;, a plugin for WordPress creates a GraphQL API. &lt;a href="https://graphql.org/" rel="noopener noreferrer"&gt;GraphQL&lt;/a&gt; is an open source data query language originally developed by Facebook.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to add custom fields in WordPress
&lt;/h2&gt;

&lt;p&gt;Enabling custom fields is fairly simple. From the post editor, click on the three dots on the top right and head to Preferences &amp;gt; Panels.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ffelixrunquist.com%2Fapi%2Fget-image%3Fimage%3D%2Fwp-content%2Fuploads%2F2023%2F05%2FScreenshot-2023-05-17-at-6.42.55-PM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ffelixrunquist.com%2Fapi%2Fget-image%3Fimage%3D%2Fwp-content%2Fuploads%2F2023%2F05%2FScreenshot-2023-05-17-at-6.42.55-PM.png"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The post editor preference pane&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once you do this, you need to reload the page to apply the changes, and the custom fields appear underneath the post content (I spent quite a while trying to find them in the sidebar, to no avail). There may already be a custom field or two, such as mine which has a &lt;code&gt;classic-editor-remember&lt;/code&gt; field, to remember if I last used the WordPress block editor or the custom editor on my post, I believe.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ffelixrunquist.com%2Fapi%2Fget-image%3Fimage%3D%2Fwp-content%2Fuploads%2F2023%2F05%2FScreenshot-2023-05-17-at-6.45.23-PM-1024x359.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ffelixrunquist.com%2Fapi%2Fget-image%3Fimage%3D%2Fwp-content%2Fuploads%2F2023%2F05%2FScreenshot-2023-05-17-at-6.45.23-PM-1024x359.png"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Custom fields on the bottom of the page&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Making custom fields visible to GraphQL
&lt;/h2&gt;

&lt;p&gt;Once you’ve added your custom fields, it’s time to make them visible to the GraphQL API. In a nutshell, GraphQL serves data through a type: each type has a set number of fields (these fields aren’t the same as WordPress custom fields!) that can be populated so that you know what to expect in the data. For example, a post type could have the following fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Title &lt;/li&gt;
&lt;li&gt;Content&lt;/li&gt;
&lt;li&gt;Description&lt;/li&gt;
&lt;li&gt;Author&lt;/li&gt;
&lt;li&gt;Published date&lt;/li&gt;
&lt;li&gt;Last modified date&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In order to make the custom fields appear, the post type has to be changed to reflect the new field.&lt;/p&gt;

&lt;p&gt;To do this, there’s a bit of PHP involved to add this as a field type in GraphQL.&lt;/p&gt;

&lt;p&gt;I found that the best way to do this was via the WordPress admin panel, by editing the theme PHP files with the Theme File Editor. To do this, go to Themes&amp;gt;Theme File Editor. I had the Hello Elementor theme preinstalled with my version of WordPress, which is of no importance since I only use the WordPress website as a CMS, the website part visible to users is implemented with Next.js.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ffelixrunquist.com%2Fapi%2Fget-image%3Fimage%3D%2Fwp-content%2Fuploads%2F2023%2F05%2FScreenshot-2023-05-19-at-11.33.40-AM-1024x574.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ffelixrunquist.com%2Fapi%2Fget-image%3Fimage%3D%2Fwp-content%2Fuploads%2F2023%2F05%2FScreenshot-2023-05-19-at-11.33.40-AM-1024x574.png"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The theme file editor&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Next, go to the relevant &lt;code&gt;functions.php&lt;/code&gt; file (in my theme it’s called &lt;code&gt;elementor-functions.php&lt;/code&gt;). This is where we’re going to add the code relevant to adding a GraphQL field. I wanted to modify the &lt;code&gt;post&lt;/code&gt; type to add a &lt;code&gt;caption&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;This is the code I added to do so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
/**
 * 
 * Custom fields for WP Graph QL
 * 
 **/
add_action( ‘graphql_register_types’, function() {
  register_graphql_field( ‘Post’, ‘caption’, [
     ‘type’ =&amp;gt; ‘String’,
     ‘description’ =&amp;gt; __( ‘The caption of the post’, ‘wp-graphql’ ),
     ‘resolve’ =&amp;gt; function( $post ) {
       $caption = get_post_meta( $post-&amp;gt;ID, ‘caption’, true );
       return ! empty( $caption ) ? $caption : ”;
     }
  ] );
} );

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Be careful modifying WordPress PHP files! If you do something wrong, it could muck up the whole site and prevent even the admin panel from showing, even something as small as a syntax error, such as a forgotten semicolon. Make sure you have a backup of your WordPress site before making any changes!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The function runs when GraphQL is compiling its types, and we’re registering a new &lt;code&gt;caption&lt;/code&gt; field to the &lt;code&gt;post&lt;/code&gt; type. To fetch the data, we’re querying the &lt;code&gt;caption&lt;/code&gt; field on a WordPress post with a known ID, using the built-in function &lt;code&gt;get-post-meta&lt;/code&gt;. Finally, if there is no caption on a given post, we return an empty string.&lt;/p&gt;

&lt;p&gt;In order to test that the new field works on the post type, WPGraphQL comes with a built-in IDE to test queries. In the WordPress admin panel, head to GraphQL &amp;gt; GraphiQL IDE, and input the following query in the left pane:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query AllPosts {
  posts(first: 20, where: {orderby: {field: DATE, order: DESC}}) {
    edges {
      node {
        title
        excerpt
        date
        caption
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above code fetches the last 20 post ordered by date, and it fetches the title, excerpt, date and &lt;code&gt;caption&lt;/code&gt; custom field. If everything works, it should retrieve the data and display any captions you’ve added to posts on the left pane when you click the “Play” button. If there is an error mentioning an unknown field type, it means the PHP code you entered for the custom field is not being recognized. This might mean that the &lt;code&gt;functions.php&lt;/code&gt; file is the wrong file. I had two &lt;code&gt;-functions.php&lt;/code&gt; files in my theme files, and the first file I added the code into resulted in an error, so I added it to the second file.&lt;/p&gt;

&lt;p&gt;Sources:&lt;br&gt;&lt;br&gt;
PHP Code – &lt;a href="https://master--wpgraphql-docs.netlify.app/getting-started/custom-fields-and-meta/" rel="noopener noreferrer"&gt;https://master–wpgraphql-docs.netlify.app/getting-started/custom-fields-and-meta/&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Creating and ordering a custom PCB</title>
      <dc:creator>Felix</dc:creator>
      <pubDate>Mon, 29 May 2023 16:39:46 +0000</pubDate>
      <link>https://dev.to/felixrunquist/creating-and-ordering-a-custom-pcb-4fol</link>
      <guid>https://dev.to/felixrunquist/creating-and-ordering-a-custom-pcb-4fol</guid>
      <description>&lt;p&gt;I’ve always found something thrilling about circuitboards. I remember the excitement of finding a discarded motherboard on the side of the road as a child, imagining the tiny little electrons flowing in their ordered, angled city. &lt;/p&gt;

&lt;p&gt;As part of a home automation project, I wanted to house a thermometer, motion sensor and display in a small form factor. A problem I came across was fitting all the modules, board and cables in such a small box!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--c0Bnc4o8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/IMG_2051-767x1024.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--c0Bnc4o8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/IMG_2051-767x1024.jpeg" alt="" width="767" height="1024"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The box I made out of MDF, along with an ESP32 DevKit V1 for comparison&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The box I made using &lt;a href="https://www.festi.info/boxes.py/"&gt;boxes.py&lt;/a&gt; and an online laser cutting service was only 6x6x2cm on the inside, barely enough to fit in my sensors and screen, let alone free-form dupont cables. It was absolute mayhem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--wBMaV-4O--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://europe1.discourse-cdn.com/arduino/original/4X/a/4/f/a4fc23372c77e51ac4affa2aa7f2cfd386e00325.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wBMaV-4O--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3Dhttps://europe1.discourse-cdn.com/arduino/original/4X/a/4/f/a4fc23372c77e51ac4affa2aa7f2cfd386e00325.jpeg" alt="" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;This image came from an Arduino forum titled Breadboard problem – am I going mad? We’ve all been there…&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I decided it was time to try my hand at creating a custom PCB. I’ve seen people online create their own through online PCB services, and researched the best way to design one.&lt;/p&gt;
&lt;h2&gt;
  
  
  Software
&lt;/h2&gt;

&lt;p&gt;There is EDA (Electronics Design Automation) software such as Autodesk EAGLE, but the free version being quite limited and the paid version requiring a Fusion 360 subscription, which is upwards of 560 € / year or $630 USD (I &lt;em&gt;know&lt;/em&gt;!), that option was quickly thrown out.&lt;/p&gt;

&lt;p&gt;I then came across &lt;a href="https://www.kicad.org/"&gt;KiCad&lt;/a&gt;, which has checked all the boxes for me. Honestly, it’s a godsend. It’s an open source PCB design suite that has a wide range of tools from designing circuits to drawing the actual PCB to viewing the model in 3D, to even converting images to PCB-ready footprints! If it even is possible to find something where it lacks, it definitely makes up for it with the vast amount of add-on packages available to install.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--lZ0zVyao--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-06-at-7.32.11-PM-990x1024.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lZ0zVyao--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-06-at-7.32.11-PM-990x1024.png" alt="" width="800" height="827"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The wide range of tools offered by KiCad&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As I haven’t ever done any digital electronic design, there definitely was a learning curve, but it wasn’t very steep.&lt;/p&gt;

&lt;p&gt;The first thing I did was to create a new project in KiCad, which initializes files for circuitry and PCB layout. This step is important if you want to create schematics and then design them onto a PCB, which you can’t do if you create a separate schematic or PCB file. I then opened up schematic editor. For a newbie, the amount buttons and information may be a little overwhelming at first but it quickly starts to feel comfortable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nkZH5Ue7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-06-at-7.33.32-PM-1024x602.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nkZH5Ue7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-06-at-7.33.32-PM-1024x602.png" alt="" width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After a bit of googling, I found the “insert component” button on the top of the right sidebar, which seemed like a good place to start. The software then prompted me to select the component I wanted among its huge library (I never once had to fetch a component from an external library). For example, you can insert a 1×4 connector by typing “01×04” in the search bar.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--a8NWlKsT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-06-at-7.37.07-PM-1024x344.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--a8NWlKsT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-06-at-7.37.07-PM-1024x344.png" alt="" width="800" height="269"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After adding all the components I needed for my project (mostly connectors to join all my modules up), it was time for me to specify the wiring between the components. There are two ways of doing this: The first way is to select the “add wire” icon (“W”) on the right, and join two or more pins, but this is tedious and the shapes you define won’t make it to the final PCB as this is just a schematic. The other way is to label pins, and KiCad automatically joins up pins with the same label. There is a scary amount of label types that can be added, but I just selected the first option and hoped for the best, which worked in my favor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--dHN5yT7A--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-06-at-7.41.43-PM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--dHN5yT7A--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-06-at-7.41.43-PM.png" alt="" width="68" height="240"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;4 (four!) different labels one can add!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Something I had to look out for was the fact that the labels have to properly join up with the pins for them to be recognized.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VnkSerAT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-06-at-7.47.11-PM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VnkSerAT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-06-at-7.47.11-PM.png" alt="" width="756" height="418"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;An improperly connected pin vs a properly connected one – the square and the circle vanish&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I wanted to build a circuit to fit an ESP32 D1 mini, an I2C RTC/Temp sensor, an I2C oled screen, PIR and IR receiver/emitters so I could use a remote with my device. I added components with the right number of pins while making sure that the pin order was correct.&lt;/p&gt;

&lt;p&gt;Once I had my circuit done, there were a few things left to do: KiCad gets upset if you don’t define power symbols, so I selected “Add Power Symbol” (“P”) and added the power and ground pins for my circuit (3.3V and GND in my case). Once you do that, you get a symbol to add to a pin just like any label. This defines that pin, and all the ones with the same label, as belonging to that power symbol.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--EvdH3Lbc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-06-at-7.52.58-PM-1024x872.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EvdH3Lbc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-06-at-7.52.58-PM-1024x872.png" alt="" width="800" height="681"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;My finished schematic&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Another thing to do is to define footprints for all the components – this gives the software an idea of how the component looks like in the real world. To do this, click on the “Run footprint assignment tool” button on the top bar, and it opens a list of the components in the schematic in the middle and a list of available footprints to assign on the left. Unassigned components are highlighted in yellow, and then it’s a matter of selecting the right footprints. For connectors, you can choose a “connector” or a “header” configuration. The default header/connector size is 2.54mm (This works with dupont cables and Arduino/Espressif boards). For example, if I wanted a 1×4 pin connector to map out to a 1×4 header, I’d select the &lt;code&gt;Connector_PinHeader_2.54mm&lt;/code&gt; family on the left, and search “1×04” on the top bar (Note: don’t type “01×04” as connector footprints don’t have a leading “0” like components do). Once you find the right configuration, double click on it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--M0pqWmIB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-06-at-8.00.38-PM-1024x262.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--M0pqWmIB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-06-at-8.00.38-PM-1024x262.png" alt="" width="800" height="205"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Searching for 1×4 headers&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Building the PCB
&lt;/h2&gt;

&lt;p&gt;After assigning all the footprints on my circuit, I went to the “Tools” tab bar and selected “Update PCB from Schematic”. This sends the circuit to the PCB builder, ready to start building a physical circuit. At first the components are haphazardly placed out onto the screen, and the circuits jumbled about (don’t worry – the PCB isn’t going to come out like that and I will get to the tracing later) but you can move them about to place them. This is where the measuring tools and grid comes in handy: I went to Settings&amp;gt;Display Options&amp;gt;Full window crosshair so that I could make sure components were aligned when I moved them. I would select each component, align it, and measure by pressing the space bar to reset the “dx” and “dy” properties on the bottom of the screen: these let you see how much your cursor moves to properly space out components. Another tool is the Dimension tool: after selecting the User.Comments layer on the right, you can click between two points to show the distance between them, this is useful for later reference.&lt;/p&gt;

&lt;p&gt;Something that seems trivial but that is important to make sure your PCB comes out the way you envisioned it is drawing diagrams. This helps make sure you get your spacing, number of components and orientation right.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ijDzOXc0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/IMG_2052-1024x946.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ijDzOXc0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/IMG_2052-1024x946.jpeg" alt="" width="800" height="739"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;My messy but functional diagram&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once everything is positioned to taste, it’s time to create a shape for the PCB. This is known as &lt;em&gt;edge tracing&lt;/em&gt;. I clicked on the “Edge.Cuts” layer (in the layer list on the right) and then selected the line tool on the right toolbar to create a square around my PCB. There are other tools such as the arc tool to create rounded edges.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
A note on pads: &lt;br&gt;&lt;br&gt;
There are two main types of pads (the plated surfaces on which components are mounted) in KiCad: &lt;br&gt;
Through-hole which is a hole that penetrates the entire thickness of the board and SMD (Surface Mount Device) which is just a plated surface that doesn’t come with a drilled hole. &lt;br&gt;
&lt;/p&gt;

&lt;p&gt;It’s time for the last building stage: creating traces, or wires, between components: The easiest way to do this, especially for a simple PCB with few components, is to use an auto tracer. This algorithmically determines the traces to join up pins the same way the schematic is laid out. I downloaded a plugin called “Freerouting” that does precisely this. You may need to install/update Java to make this work like I did. In the PCB editor, select Tools&amp;gt;External Plugins&amp;gt;Freerouting from the menu bar. This launches the tool and automatically traces on the front and back PCB layer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PuFPyHlL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-06-at-8.22.58-PM-1024x738.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PuFPyHlL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-06-at-8.22.58-PM-1024x738.png" alt="" width="800" height="577"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;My traced PCB – it’s finally coming to shape!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There were a few traces I wasn’t happy with, so I modified them by selecting the layer on which the trace is on (either F.Cu – front or B.Cu – back), deleting the traces I want to change, and selecting the “Route Tracks” tool (“X”). This handy little tool avoids traces which aren’t part of the circuit and does things like 45º angles (This is important for high-frequency signals which bounce back off the corner of 90º angles). There are a few things to consider when tracing: The front and back are obviously electrically isolated so front and back traces can intersect but not traces on the same side. Pins traverse the entire board so this is a useful place to join traces from different sides to the same circuit, and if you want to do this without a pin you can make a hole, called a “&lt;a href="https://en.wikipedia.org/wiki/Via_(electronics)#In_PCB"&gt;via&lt;/a&gt;” which does this (This adds holes to drill so avoid if you want to keep costs down).&lt;/p&gt;

&lt;p&gt;One can also add labels to components that will be printed out onto the PCB, this is known as a “Silkscreen”. Select the silkscreen layer (front or back) in the layers list and you can place text or shapes onto it. Make sure that the silkscreen doesn’t overlap with any pads as they will get clipped. The silkscreen is printed on top of the traces so having them overlap isn’t an issue.&lt;/p&gt;

&lt;p&gt;Another thing I found tricky was to switch the side of components: You can mount components on either side, and although for through-holes it doesn’t change much (As the pads which go through the thickness of the board are symmetric on both sides), for surface-mounted components it makes a huge difference: By double clicking a component to make the footprint properties dialog come up, you can select the side it’s mounted on in the “position” section.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--S5E4DXrK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-07-at-12.53.46-PM-1024x888.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--S5E4DXrK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-07-at-12.53.46-PM-1024x888.png" alt="" width="800" height="694"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A component positioned on the front side&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The next step is to make sure there aren’t any issues with the design: PCBs must follow a DRC (Design Rule Check) to make sure board edges are properly defined, different traces/components don’t intersect… This is done by clicking on the “DRC window” icon on the top bar, and selecting “Run DRC”. If it finds any issue, it will put arrows in the PCB diagram and selecting the error in the window highlights the component and lists the error. I had a few issues with overlapping labels and unconnected pads, but they were quickly resolved.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vxhAcel4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-07-at-12.52.08-PM-1024x836.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vxhAcel4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-07-at-12.52.08-PM-1024x836.png" alt="" width="800" height="653"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;My DRC issues&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After everything is in order, select View&amp;gt;3D viewer (Option-3) to get a rendered view of the PCB. I find that this helps with mentally representing all the components in space, and making sure everything is how you envisioned it. Seeing the front and back traces separately also helps to make sure the pads are wired optimally.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--1q7e1rrq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-07-at-12.57.35-PM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1q7e1rrq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-07-at-12.57.35-PM.png" alt="" width="800" height="596"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;My 3D model&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Ordering
&lt;/h2&gt;

&lt;p&gt;It’s time to order the PCB! To get the file ready for export, choose File&amp;gt;Plot…. There are 4 things to export: Traces (front and back), Masks (font and back), Silkscreen (Front and back), and the drill holes. To export drill holes, go to “Generate Drill Files…”. I found it best to select the “PTH and NPTH in single file” so that you get one simple file for all the drill holes. Select “Generate drill File” and “Plot” on the dialogs, and it should export the gerber files in the same folder as the KiCad project.&lt;/p&gt;

&lt;p&gt;I researched a few online PCB ordering services: &lt;a href="https://www.pcbway.com/"&gt;PCBWay&lt;/a&gt;, &lt;a href="https://jlcpcb.com/"&gt;JLCPCB&lt;/a&gt; and &lt;a href="https://www.allpcb.com/"&gt;AllPCB&lt;/a&gt; are based in China, while &lt;a href="https://oshpark.com/"&gt;OSHPark&lt;/a&gt; is USA-based. After looking at reviews and comparing prices, I found that the best way to go was PCB way, especially for a hobby project. To upload the KiCad model, I zipped a folder containing all the gerber files and uploaded it on the website. It automatically populated the fields for me, and it was just a matter of selecting the PCB color I wanted (I went with purple) as well as the delivery service. As the minimum order for most PCB manufacturing services is 5 boards, I got the idea of modifying my board so that it could fit an ESP32 DevKit as well, and so that I could use the I2C and various pin support as well for other projects, unfortunately I got that idea after I had ordered my PCB and it was too late to change.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PrI0VSfk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-08-at-12.07.49-AM-1024x393.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PrI0VSfk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-08-at-12.07.49-AM-1024x393.png" alt="" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The ordering process was quick, and I could see on my order history the stage at which my board was, from drilling to plating to cutting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--tNhKazcq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-17-at-7.04.09-PM-945x1024.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tNhKazcq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/Screenshot-2023-05-17-at-7.04.09-PM-945x1024.png" alt="" width="800" height="867"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The board came within 10 days from ordering, I was very impressed with the speed! I got a pack of 5 boards in purple, and I’m quite pleased with the quality of the board, from the thickness to the screen printing resolution. I was warned by KiCad that the font used for my name wasn’t of uniform thickness, but JLCPCB handled it very well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--l3sQdP9Z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/IMG_2198-1024x711.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l3sQdP9Z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://felixrunquist.com/api/get-image%3Fimage%3D/wp-content/uploads/2023/05/IMG_2198-1024x711.jpeg" alt="" width="800" height="555"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;My finished creation!&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The verdict
&lt;/h2&gt;

&lt;p&gt;Overall, I’m quite happy with the design process with KiCad, as well as the quality of service offered by JLCPCB. Although I do feel bad about not supporting local businesses and ordering PCBs from across the globe, the price, especially for a hobby project, made it a no-brainer for the quality of the finished product. Receiving your own creation in the mail is such a rewarding feeling!&lt;/p&gt;

&lt;p&gt;If I was to do anything differently next time, I would definitely add drill holes which would have made mounting it easier, I would also take the time to think about what other project the board could be used for given the minimum order amount of 5.&lt;/p&gt;

&lt;p&gt;KiCad, FTW!&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
