Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Thu, 01 Aug 2024 14:35:33 +0000 en-US hourly 1 https://wordpress.org/?v=6.6.1 225069128 So you think you know box shadows? https://frontendmasters.com/blog/so-you-think-you-know-box-shadows/ https://frontendmasters.com/blog/so-you-think-you-know-box-shadows/#respond Thu, 01 Aug 2024 14:35:32 +0000 https://frontendmasters.com/blog/?p=3322 David Gerrells has a bunch of fun rendering far too many CSS box-shadows for things that box-shadow was never meant to do.

I found out my m1 can render a stupid number of these bad boys and so I set out to see just how far you can push them and boy did I.

Because box-shadow mimics the shape of the original element, doesn’t have to have any blur at all, and can be colored any color, they can be a way to draw anything you want wherever you want with a single DOM element. Doing faux ray-tracing as David does at the end is not something I thought I’d ever see.

I found it fun looking at the DevTools while the demos were cooking.

]]>
https://frontendmasters.com/blog/so-you-think-you-know-box-shadows/feed/ 0 3322
Reading from the Clipboard in JavaScript https://frontendmasters.com/blog/reading-from-the-clipboard-in-javascript/ https://frontendmasters.com/blog/reading-from-the-clipboard-in-javascript/#respond Wed, 31 Jul 2024 14:22:50 +0000 https://frontendmasters.com/blog/?p=3136 Browsers have excellent support for reading and writing the user’s clipboard, and this opens up possibilities for better, and more native like experiences on the web. On websites that use these APIs for helpful features, it feels natural to the user. On sites where it isn’t supported, it almost feels like a bug. In this series of articles, I’m going to demonstrate how to work with the clipboard.

Article Series
  1. Reading from the Clipboard (You are here!)
  2. Writing to the Clipboard (Coming soon!)
  3. Handling Pasted Content from the Clipboard (Coming soon!)

Before We Begin…

Clipboard functionality on the web requires a “secure context”. So if you’re running an http site (as opposed to an https site), these features will not work. I’d highly encourage you to get your site on https. That being said, these features, and others like them that require secure contexts, will still work on http://localhost. There’s no need to set up a temporary certificate when doing local testing.

Also note that some browsers will manipulate the content of the clipboard when read. This is done for security reasons and you can disable it. I’ll demonstrate this later in the article.

The Clipboard API

The Clipboard API is the top-level object (navigator.clipboard) containing the methods to work with the clipboard. According to MDN, support is pretty much across the board:

In fact, the only outlier relates to being able to disable the security aspects mentioned above in Firefox and Safari. Outside of that, support is great.

Reading the Clipboard

Reading from the clipboard is handled by two methods:

  • read
  • readText

Which you use will depend on your particular use case. In theory, read would be the most flexible, handling any content, but if you know for a fact you only need to support text input from the clipboard, you should probably use the more specific readText format. I’m a fan of code that helps reduce my chance of mistakes so that’s what I’d recommend. Let’s start with an example of that.

First, I’ll use a bit of HTML:

<button id="readCB">Read Clipboard</button>
<div id="log"></div>

The button will be used to kick off the code to read from the clipboard and I’ll use the <div> to show the results. Now the JavaScript:

let $log = document.querySelector('#log');

document.querySelector('#readCB').addEventListener('click', async () => {
  let contents = await navigator.clipboard.readText();
  console.log(contents);
  $log.innerText += `From clipboard: ${contents}`;
});

When the button is clicked, I try to read text from the clip. Note I said try. The first time the button is clicked, the browser prompts for permission:

Chrome
Arc
If you need to revoke or change permissions…

There should be some UI in the browser bar area to do this. On Chrome it’s the little control icon which opens this:

Chrome controls panel for a website showing secure connection, and also the Clipboard access which can be turned off or reset.

The process of reading from the clipboard is async, so I’m using await to make that a bit simpler. Then, I log the contents to the console and write it out to the DOM.

Here’s a live demo, but please note that it may not work for you, because of the permissions prompts mentioned above. You can view this demo here and that will allow you to approve the permissions to make it work. It’s tricky with <iframe> demos — both domains need the permissions approved, but won’t prompt for them, and even then it doesn’t always want to work.

How well does it work?

If you have nothing on the clipboard, the code runs just fine, but the contents are an empty string. It was actually a bit difficult to figure out how to empty my clipboard. I initially tested by putting my cursor on an empty line in my editor and hitting CTRL+C, but that felt like a hack. This random article showed a command line prompt I could run on Windows that seemed to be more “official”. I used the suggested command and got the exact same result. No error, just an empty string.

Copying text works as expected, but note that if you copy HTML, and insert it into the DOM, it will be rendered as HTML. In my case, I’m using innerText so that’s not an issue.

Next, I tested “rendered” text, and by that I mean random text from a web page, ensuring I got styled text as well. As expected, I got just the text of the selection from the HTML. So for example:

Which ended up as:

CSS
MIXINS
STYLE QUERIES
Style Queries are Almost Like Mixins (But Mixins Would Be Better)
By CHRIS COYIER on July 12, 2024
Having a named block of styles to apply in CSS can be useful, and newfangled Style Queries are pretty close to that. We look at one use case here, how Sass did mixins better, and hope for a native solution.

I tried copying from Word and PDF and got the same type of result.

Cool — now let’s kick it up a notch!

Reading Multimedia Content from the Clipboard

As explained earlier, the read method can support any content, not just text. Switching to it is as simple as:

let contents = await navigator.clipboard.read();

This method returns an array of ClipboardItems representing the fact that a user may have selected multiple different items. A ClipboardItem consists of:

  • types: An array of MIME types associated with the item
  • presentationStyle: This was one of the rare times when MDN failed to be helpful and I had to actually go to the spec. This property represents whether the pasted content should be considered as appended content or an attachment. You can imagine how an email client works where if you paste text, it just adds to the current email, but binary data is usually handled as an attachment to the mail instead. You don’t really need to worry about it as it’s only supported in Firefox. That being said, you can make up your own mind depending on the mimetype being used.

To get the actual contents you need to use the getType method, which feels oddly named. It takes a mimetype as an argument and returns a blob.

Now things get a bit more complex. Your application has to figure out what makes sense to do based on the data in the clipboard. Let’s consider a more advanced version of the previous demo:

document.querySelector('#readCB').addEventListener('click', async () => {
  let contents = await navigator.clipboard.read();

  for (let item of contents) {

    console.log('Types for this item: ', item.types);

    if (item.types.includes('text/html')) {
      let blob = await item.getType('text/html');
      let html = await blob.text();
      console.log(html);
      $log.innerHTML += html.replaceAll('<','&lt;').replaceAll('>','&gt;');
      $log.innerHTML += '<hr>';
    }

    if (item.types.includes('text/plain')) {
      let blob = await item.getType('text/plain');
      let text = await blob.text();
      console.log(text);
      $log.innerHTML += text.replaceAll('<','&lt;').replaceAll('>','&gt;');
      $log.innerHTML += '<hr>';
    }

    if (item.types.includes('image/png')) {
      // modified from MDN sample
      const pngImage = new Image(); 
      pngImage.alt = "PNG image from clipboard";
      const blob = await item.getType("image/png");
      pngImage.src = URL.createObjectURL(blob);
      $log.appendChild(pngImage);
    }

  }
});

Remember: each item in the array has itself an array of mimetypes. This is very important because many places where you copy code (web pages, your IDE, etc.) may return both text/plain and text/html for an item. That’s… good, as it gives you options in terms of what you want to do. If you want to try to keep some of the original formatting, get the HTML. If you only care about the content, get just the plain text.

Here’s another live demo, this one handling multiple content types.

Copying from a PDF

Surprisingly, copying from a PDF will only return text/plain, even if you select an entire document and it has images. This makes some sense though. Check out the screenshot from Acrobat below, where I had run “Select All”:

As you can see above, the textual elements are highlighted, not the image. You can copy an image from a PDF, but you can have to click on it specifically.

Security Concerns and Sanitization

The MDN docs mention this in regards to reading from the clipboard:

Certain browsers may sanitize the clipboard data when it is read, to prevent malicious content from being pasted into the document. For example, Chrome (and other Chromium-based browsers) sanitizes HTML data by stripping <script> tags and other potentially dangerous content. Use the unsanitized array to specify a list of MIME types that should not be sanitized.

The way around this is to pass a formats object to the read method that specifies an array of mimetypes the browser should not sanitize. Given a clipboard with potentially dangerous content, that means you would expect a different result from

let contents = await navigator.clipboard.read();

versus:

let contents = await navigator.clipboard.read({ unsanitized: ['text/html'] });

This was fairly difficult to test if I selected code that includes the <script> tag, it only reported text/plain as a mimetype. The only way I was able to verify this was in the MDN example where they kinda “force” the issue by writing to the clipboard as HTML. Honestly, I can’t see a reason to use this particular feature unless more ‘sanitized’ things turn up. I’d recommend just letting the browser clear it out if it needs to. But to be clear, if you copy code that includes the <script> tag, it will come in as text/plain and be read just fine.

Caveats

Though a user may have a file from the filesystem selected, if you try to read it from the clipboard, you get an empty array of results. I found this surprising as you can paste files into the browser and support reading them. We’ll get into that in the article on pasting.

Examples Use Cases

How about some examples to give you ideas of how you would use this in the real world?

Addresses

Sites making use of client-side maps (like Google Maps or Leaflet) could read from the clipboard and attempt to parse the contents as an address, and if found, focus the map. This will probably require the use of a Geocoding API to translate a freeform address into longitude and latitude points.

Google Maps on iOS (while not technically a web app) behaves this way. If you’ve given permission, if you happen to have an address on your clipboard you’ll see it as a one-click option right up top.

QR Codes

How about a TamperMonkey script that lets you take any block of text in your clipboard and turn it into a QR code? My buddy Todd Sharp literally built this the day after I wrote the first draft of this article. His work could be used in a regular web page as well to product QR codes from the clipboard.

URLs

If you knew the user had a URL on their clipboard, your app could offer to do something with it. Perhaps automatically add useful URL params, shorten it, or otherwise.


Can you think of any other use cases for reading from the clipboard? Admittedly, writing to the clipboard is generally a more common and useful ability, and we’ll get to that next.

]]>
https://frontendmasters.com/blog/reading-from-the-clipboard-in-javascript/feed/ 0 3136
SVG triangle of compromise https://frontendmasters.com/blog/svg-triangle-of-compromise/ https://frontendmasters.com/blog/svg-triangle-of-compromise/#respond Tue, 30 Jul 2024 15:15:04 +0000 https://frontendmasters.com/blog/?p=3290 tag, it’s cached nicely, but you give up on CSS […]]]> I enjoyed Micah R Ledbetter’s SVG triangle of compromise and generally think it’s a fair analysis of how any-which-way you use SVG on a page, you’re giving up some kind of nice ability it could have. For instance, if you use SVG through an <img> tag, it’s cached nicely, but you give up on CSS reaching in there to style things. If you drop it in as <svg>, you can style, but then it’s not cached well for repeated uses.

Then Scott Jehl chimed in with a way to “have it all”. The crux of it is using the SVG <use> element to reference an SVG file (so you get caching and sizing) and you can set CSS --custom-properties that “pierce” the shadow DOM that <use> creates (that’s right, SVG can have a shadow DOM just like web components) and allow for styling.

This does solve all three angles, the caveats being 1) you can’t cache the SVG (“sprite”, it’s usually called when you combine icons into a single file) on a different domain. 2) it’s a manual pain to set up SVGs to be entirely styled in this way. Scott’s tool might help with 2, but browsers need to help with 1.

]]>
https://frontendmasters.com/blog/svg-triangle-of-compromise/feed/ 0 3290
Patterns for Memory Efficient DOM Manipulation with Modern Vanilla JavaScript https://frontendmasters.com/blog/patterns-for-memory-efficient-dom-manipulation/ https://frontendmasters.com/blog/patterns-for-memory-efficient-dom-manipulation/#comments Mon, 29 Jul 2024 12:47:16 +0000 https://frontendmasters.com/blog/?p=1551 Let’s continue the modern vanilla JavaScript series!

Memory Efficient DOM Manipulation

Article Series

I’ll discuss best practices to avoid excess memory usage when managing updating the DOM to make your apps blazingly fast™️.

DOM: Document Object Model – A Brief Overview

When you render HTML, the live view of those rendered elements in the browser is called the DOM. This is what you’ll see in your developer tools “Elements” inspector:

Elements panel in chrome dev tools

It’s essentially a tree, with each element inside of it being a leaf. There is an entire set of APIs specifically dealing with modifying this tree of elements.

Here’s a quick list of common DOM APIs:

  • querySelector()
  • querySelectorAll()
  • createElement()
  • getAttribute()
  • setAttribute()
  • addEventListener()
  • appendChild()

These are attached to the document, so you use them like const el = document.querySelector("#el");. They are also available on all other elements, so if you have an element reference you can use these methods and their abilities are scoped to that element.

const nav = document.querySelector("#site-nav");
const navLinks = nav.querySelectorAll("a");

These methods will be available in the browser to modify the DOM, but they won’t be available in server JavaScript (like Node.js) unless you use a DOM emulator like js-dom.

As an industry, we’ve offloaded most of this direct rendering to frameworks. All JavaScript frameworks (React, Angular, Vue, Svelte, etc) use these APIs under the hood. While I recognize that the productivity benefits of frameworks often outweigh the potential performance gains of manual DOM manipulation, I want to demystify what goes on under the hood in this article.

Why manipulate the DOM yourself in the first place?

The main reason is performance. Frameworks can add unnecessary data structures and re-renders leading to the dreaded stuttering / freezing behavior seen in many modern web apps. This is due to the Garbage Collector being put on overdrive having to handle all that code.

The downside is it is more code to handle DOM manipulation yourself. It can get complicated, which is why it a better developer experience to use frameworks and abstractions around the DOM rather than manipulating the DOM manually. Regardless, there are cases where you may need the extra performance. That is what this guide is for.

VS Code is Built on Manual DOM Manipulation

Visual Studio Code is one of those such cases. VS Code is written in vanilla JavaScript “to be as close to the DOM as possible.” Projects as large as VS Code need to have tight control over performance. Since much of the power is in the plugins ecosystem, the core needs to be as core and lightweight as possible and is responsible for its wide adoption.

Microsoft Edge also recently moved off of React for the same reason.

If you find yourself in this case where you need the performance of direct DOM manipulation – lower level programming than using a framework – hopefully this article will help!

Tips for More Efficient DOM Manipulation

Prefer hiding/showing over creating new elements

Keeping your DOM unchanged by hiding and showing elements instead of destroying and creating them with JavaScript is always going to be the more performant option.

Server render your element and hide/show it with a class (and appropriate CSS ruleset) like el.classList.add('show') or el.style.display = 'block' instead of creating and inserting the element dynamically with JavaScript. The mostly static DOM is much more performant due to the lack of garbage collection calls and complex client logic.

Don’t create DOM nodes on the client dynamically if you can avoid it.

But do remember assistive technology. If you want an element both visually hidden and hidden to assistive technology, display: none; should do it. But if you want to hide an element and keep it there for assistive technology, look at other methods for hiding content.

Prefer textContent over innerText for reading the content of an element

The innerText method is cool because it is aware of the current styles of an element. It knows if an element is hidden or not, and only gets text if something is actually displaying. The issue with it is that this process of checking styles forces reflow, and is slower.

Reading content with element.textContent is much faster than element.innerText, so prefer textContent for reading content of an element where possible.

Use insertAdjacentHTML over innerHTML

The insertAdjacentHTML method is much faster than innerHTML because it doesn’t have to destroy the DOM first before inserting. The method is flexible in where it places the new HTML, for example:

el.insertAdjacentHTML("afterbegin", html);
el.insertAdjacentHTML("beforeend", html);

The Fastest Approach is to use insertAdjacentElement or appendChild

Approach #1: Use the template tag to create HTML templates and appendChild to insert new HTML

These are the fastest methods are to append fully formed DOM elements. An established pattern for this is to create an HTML template with the <template> tag to create the elements, then insert them into the DOM with insertAdjacentElement or appendChild methods.

<template id="card_template">
  <article class="card">
    <h3></h3>
    <div class="card__body">
      <div class='card__body__image'></div>
      <section class='card__body__content'>
      </section>
    </div>
  </article>
</template>
function createCardElement(title, body) {
  const template = document.getElementById('card_template');
  const element = template.content.cloneNode(true).firstElementChild;
  const [cardTitle] = element.getElementsByTagName("h3");
  const [cardBody] = element.getElementsByTagName("section");
  [cardTitle.textContent, cardBody.textContent] = [title, body];
  return element;
}

container.appendChild(createCardElement(
  "Frontend System Design: Fundamentals",
  "This is a random content"
))

You can see this in action in the new, Front-End System Design course, where Evgenni builds an infinite scrolling social news feed from scratch!

Approach #2: Use createDocumentFragment with appendChild to Batch Inserts

DocumentFragment is a lightweight, “empty” document object that can hold DOM nodes. It’s not part of the active DOM tree, making it ideal for preparing multiple elements for insertion.

const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  fragment.appendChild(li);
}
document.getElementById('myList').appendChild(fragment);

This approach minimizes reflows and repaints by inserting all elements at once, rather than individually.

Manage References When Nodes are Removed

When you remove a DOM node, you don’t want references sitting around that prevent the garbage collector from cleaning up associated data. We can use WeakMap and WeakRef to avoid leaky references.

Associate data to DOM nodes with WeakMap

You can associate data to DOM nodes using WeakMap. That way if the DOM node is removed later, the reference to the data will be gone for good.

let DOMdata = { 'logo': 'Frontend Masters' };
let DOMmap = new WeakMap();
let el = document.querySelector(".FmLogo");
DOMmap.set(el, DOMdata);
console.log(DOMmap.get(el)); // { 'logo': 'Frontend Masters' }
el.remove(); // DOMdata is able to be garbage collected

Using weak maps ensures references to data doesn’t stick around if a DOM element is removed.

Clean up after Garbage Collection using WeakRef

In the following example, we are creating a WeakRef to a DOM node:

class Counter {
  constructor(element) {
    // Remember a weak reference to the DOM element
    this.ref = new WeakRef(element);
    this.start();
  }

  start() {
    if (this.timer) {
      return;
    }

    this.count = 0;

    const tick = () => {
      // get the element from the weak reference, if it still exists
      const element = this.ref.deref();
      if (element) {
        console.log("Element is still in memory, updating count.")
        element.textContent = `Counter: ${++this.count}`;
      } else {
        // The element doesn't exist anymore
        console.log("Garabage Collector ran and element is GONE – clean up interval");
        this.stop();
        this.ref = null;
      }
    };

    tick();
    this.timer = setInterval(tick, 1000);
  }

  stop() {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = 0;
    }
  }
}

const counter = new Counter(document.getElementById("counter"));
setTimeout(() => {
  document.getElementById("counter").remove();
}, 5000);

After removing the node, you can watch your console to see when the actual garbage collection happens, or you can force it to happen yourself using the Performance tab in your developer tools:

Then you can be sure all references are gone and timers are cleaned up.

Note: Try not to overuse WeakRef—this magic does come at a cost. It’s better for performance if you can explicitly manage references.

Cleaning up Event Listeners

Manually remove events with removeEventListener

function handleClick() {
  console.log("Button was clicked!");
  el.removeEventListener("click", handleClick);
}

// Add an event listener to the button
const el = document.querySelector("#button");
el.addEventListener("click", handleClick);

Use the once param for one and done events

This same behavior as above could be achieved with the “once” param:

el.addEventListener('click', handleClick, {
  once: true
});

Adding a third parameter to addEventListener with a boolean value indicating that the listener should be invoked at most once after being added. The listener is automatically removed when invoked.

Use event delegation to bind fewer events

If you are building up and replacing nodes frequently in a highly dynamic component, it’s more expensive to have to setup all their respective event listeners as you’re building the nodes.

Instead, you can bind an event closer to the root level. Since events bubble up the DOM, you can check the event.target (original target of the event) to catch and respond to the event.

Using matches(selector) only matches the current element, so it needs to be the leaf node.

const rootEl = document.querySelector("#root");
// Listen for clicks on the entire window
rootEl.addEventListener('click', function (event) {
  // if the element is clicked has class "target-element"
  if (event.target.matches('.target-element')) doSomething();
});

More than likely, you’ll have elements like <div class="target-element"><p>...</p></div> in this case you’d need to use the .closest(element) method.

const rootEl = document.querySelector("#root");
// Listen for clicks on the entire window
rootEl.addEventListener('click', function (event) {
  // if the element is clicked has a parent with "target-element"
  if (event.target.closest('.target-element')) doSomething();
});

This method allows you to not worry about attaching and removing listeners after dynamically injecting elements.

Use AbortController to unbind groups of events

const button = document.getElementById('button');
const controller = new AbortController();
const { signal } = controller;

button.addEventListener(
  'click', 
  () => console.log('clicked!'), 
  { signal }
);

// Remove the listener!
controller.abort();

You can use the AbortController to remove sets of events.

let controller = new AbortController();
const { signal } = controller;

button.addEventListener('click', () => console.log('clicked!'), { signal });
window.addEventListener('resize', () => console.log('resized!'), { signal });
document.addEventListener('keyup', () => console.log('pressed!'), { signal });

// Remove all listeners at once:
controller.abort();

Shout out to Alex MacArthur for the AbortController code example on this one.

Profiling & Debugging

Measure your DOM to make sure it is not too large.

Here’s a brief guide on using Chrome DevTools for memory profiling:

  1. Open Chrome DevTools
  2. Go to the “Memory” tab
  3. Choose “Heap snapshot” and click “Take snapshot”
  4. Perform your DOM operations
  5. Take another snapshot
  6. Compare snapshots to identify memory growth

Key things to look for:

  • Unexpectedly retained DOM elements
  • Large arrays or objects that aren’t being cleaned up
  • Increasing memory usage over time (potential memory leak)

You can also use the “Performance” tab to record memory usage over time:

  1. Go to the “Performance” tab
  2. Check “Memory” in the options
  3. Click “Record”
  4. Perform your DOM operations
  5. Stop recording and analyze the memory graph

This will help you visualize memory allocation and identify potential leaks or unnecessary allocations during DOM manipulation.

JavaScript Execution Time Analysis

In addition to memory profiling, the Performance tab in Chrome DevTools is invaluable for analyzing JavaScript execution time, which is crucial when optimizing DOM manipulation code.

Here’s how to use it:

  1. Open Chrome DevTools and go to the “Performance” tab
  2. Click the record button
  3. Perform the DOM operations you want to analyze
  4. Stop the recording
Excerpt from ThePrimeagen's 
Inspecting & Debugging Performance lesson

The resulting timeline will show you:

  • JavaScript execution (yellow)
  • Rendering activities (purple)
  • Painting (green)

Look for:

  • Long yellow bars, indicating time-consuming JavaScript operations
  • Frequent short yellow bars, which might indicate excessive DOM manipulation

To dive deeper:

  • Click on a yellow bar to see the specific function call and its execution time
  • Look at the “Bottom-Up” and “Call Tree” tabs to identify which functions are taking the most time

This analysis can help you pinpoint exactly where your DOM manipulation code might be causing performance issues, allowing for targeted optimizations.

Performance Debugging Resources

Articles by the Chrome dev team:

Courses that cover the memory and performance analysis and Chrome Dev Tools further:

Remember, efficient DOM manipulation isn’t just about using the right methods—it’s also about understanding when and how often you’re interacting with the DOM. Excessive manipulation, even with efficient methods, can still lead to performance issues.

Key Takeaways for DOM Optimization

Efficient DOM manipulation knowledge is important when creating performance-sensitive web apps. While modern frameworks offer convenience and abstraction, understanding and applying these low-level techniques can significantly boost your app’s performance, especially in demanding scenarios.

Here’s a recap:

  1. Prefer modifying existing elements over creating new ones when possible.
  2. Use efficient methods like textContent, insertAdjacentHTML, and appendChild.
  3. Manage references carefully, leveraging WeakMap and WeakRef to avoid memory leaks.
  4. Clean up event listeners properly to prevent unnecessary overhead.
  5. Consider techniques like event delegation for more efficient event handling.
  6. Use tools like AbortController for easier management of multiple event listeners.
  7. Employ DocumentFragments for batch insertions and understand concepts like the virtual DOM for broader optimization strategies.

Remember, the goal isn’t always to forgo frameworks and manually manipulate the DOM for every project. Rather, it’s to understand these principles so you can make informed decisions about when to use frameworks and when to optimize at a lower level. Tools like memory profiling and performance benchmarking can guide these decisions.

Article Series

]]>
https://frontendmasters.com/blog/patterns-for-memory-efficient-dom-manipulation/feed/ 1 1551
Blurring https://frontendmasters.com/blog/blurring/ https://frontendmasters.com/blog/blurring/#respond Fri, 26 Jul 2024 19:31:08 +0000 https://frontendmasters.com/blog/?p=3194 , to WebGL. I particularly like the idea of masking an element with a backdrop-filter with a gradient so, for instance, a header can fade out how much […]]]> This 9-minute video from Juxtopposed on blurring is a great watch. There are an awful lot of ways to blur things on the web, from filter, backdrop-filter, <feGaussianBlur>, to WebGL. I particularly like the idea of masking an element with a backdrop-filter with a gradient so, for instance, a header can fade out how much blur is being applied.

]]>
https://frontendmasters.com/blog/blurring/feed/ 0 3194
How to Get the Width/Height of Any Element in Only CSS https://frontendmasters.com/blog/how-to-get-the-width-height-of-any-element-in-only-css/ https://frontendmasters.com/blog/how-to-get-the-width-height-of-any-element-in-only-css/#respond Thu, 25 Jul 2024 14:14:28 +0000 https://frontendmasters.com/blog/?p=3119 Getting the dimension of an element using JavaScript is a trivial task. You barely even need to do anything. If you have a reference to an element, you’ve got the dimensions (i.e. el.offsetWidth / el.offsetHeight). But we aren’t so lucky in CSS. While we’re able to react to elements being particular sizes with @container queries, we don’t have access to a straight up number we could use to, for example, display on the screen.

It may sound impossible but it’s doable! There are no simple built-in functions for this, so get ready for some slightly hacky experimentation.

Note: At time of writing, only Chrome (and Edge) have the full support of the features we will be using so consider those browsers to read the article.

Let’s start with a demo:

This demo has a simple layout with elements that will all have different sizes. Each rectangular element displays it’s own width/height. You can resize the browser or adjust the content; the values will update automatically.

Don’t try to find the hidden JavaScript, it’s 100% CSS magic, powered mostly by scroll-driven animations.

Why Scroll-Driven Animations?

Scroll-Driven animations is one of the most popular new CSS features in 2024. It unlocked a lot of possibilities and solved some common problems.

How are these features relevant to this situation of figuring out an element’s dimensions, though?

The terms “scroll” and “animation” tend to bring to mind, uhh, animating stuff on scroll. To be fair, that is the main purpose:

It allows you to animate property values based on a progression along a scroll-based timeline instead of the default time-based document timeline. This means that you can animate an element by scrolling a scrollable element, rather than just by the passing of time.

MDN

But we can think about it differently and achieve more than a simple animation on scroll. If you keep reading the MDN page, it explains there are two types of “scroll-based timelines”. In our case, we will consider the “view progress timeline”.

You progress this timeline based on the change in visibility of an element (known as the subject) inside a scroller. The visibility of the subject inside the scroller is tracked as a percentage of progress.

MDN

With this type of scroll timeline, there are three relevant elements: the scroller which is the container having the scroll, the subject which is an element moving inside the container and the animation that will progress based on the position of the “subject” inside the “scroller”.

The three elements are linked with each other. To identify the progress of the animation we need to know the position of the subject inside the scroller and for this, we need to know the dimension of the scroller, the dimension of the subject, and the offset of the subject (the distance between the subject and the edges of the scroller).

So our equation contains four variables:

  1. Dimension of the scroller
  2. Dimension of the subject
  3. Progress of the animation
  4. Offset of the subject

If three variables are known, we can automatically find the missing one. In our case, the missing variable will be the “dimension of scroller” and that’s how we are going to find the width/height of any element (an element that will a be scroller).

How Does it Work?

Let’s dive into the theory and get to how scroll-driven animations are actually used to do this. It won’t be long and boring, I promise! I’ll be using width as the dimension being measured, but height would use the same logic just on the other axis.

Consider the following figure:

We have a container (the scroller) and an element inside it (the subject) placed on the left. There are two special positions within the container. The 0% position is when the element is at the right (inside the container) and the 100% position is when the element has exited the container from the left (outside the container).

The movement of the subject between 0% and 100% will define the percentage of the progression but our element will not move so the percentage will be fixed. Let’s call it P. We also know the width of the subject and we need to find the width of the scroller.

Remember the variables we talked about. Considering this configuration, we already know three of them: “the width of the subject”, “the offset of the subject” (fixed to the left edge), and the “progress of the animation” (since the subject is fixed). To make things easier, let’s consider that the width of the scroller is a multiplier of the width of the subject:

W = N * S.

The goal is to find the N or more precisely, we need to find the relation between the P and N. I said the P is fixed, but in reality it’s only fixed when the scroller width is fixed which is logical. But if the width of the scroller changes, the progress will also change, that’s why we need to find the formula between the progress and the width.

Let’s start with the case where the width of the scroller is equal to twice the width of the subject, we get the following:

The subject is in the middle between 0% and 100% so the progress in this case is 50%. For N = 2 we get P = 50%.

Let’s try for N = 3:

Now we have two extra slots in addition to the 0% and 100%. If we suppose that the subject can only be placed inside one of the 4 slots, we can have the following progress: 0%33.33%66.67%100%. But the subject is always placed at the before-the-last slot so the progress in this case is equal to 66.67% or, seen differently, it’s equal to 100% - 100%/3 (100%/3 is the progression step).

Are you seeing the pattern? If the width of the scroller is equal to N times the width of the subject we will have N+1 slots (including 0% and 100%) so the step between each slot is equal to 100%/N and the subject is located at the before-the-last slot so the progress is equal to 100% - 100%/N.

We have our equation: P = 100% - 100%/N so N = 100%/(100% - P).

If we convert the percentage to values between 0 and 1 we get N = 1/(1 - P) and the width we are looking for is equal to W = N * S = S/(1 - P).

Now If we consider a width for the subject equal to 1px, we get W = 1px/(1 - P) and without the unit, we have W = 1/(1 - P).

Let’s Write Some Code

Enough theory! Let’s transform all this into code. We start with this structure:

<div class="container"></div>
.container {
  overflow: auto;
  position: relative;
}
.container:before {
  content: "";
  position: absolute;
  left: 0;
  width: 1px;
}

The scroller element is the container and the subject element is a pseudo-element. I am using position: absolute so the subject doesn’t affect the width of the container (the value we need to calculate). Like described in the previous section, it’s placed at the left of the container with 1px of width.

Next, we define a named timeline linked to the pseudo-element (the subject)

.container {
  timeline-scope: --cx;
}
.container:before {
  view-timeline: --cx inline
}

The MDN description of the property:

The view-timeline CSS shorthand property is used to define a named view progress timeline, which is progressed through based on the change in visibility of an element (known as the subject) inside a scrollable element (scroller). view-timeline is set on the subject.

We consider the inline (horizontal) axis. We need to also use timeline-scope to give the container access to the view progress. By default, a named timeline is scoped to the element where it’s defined (and its descendants) but we can change this to make it available at any level.

Why not define the scope at the html level, then?

Enlarging the scope to all the elements may sound like a good idea, but it’s not. We may need to use the same code for different elements so limiting the scope allows us to reuse the same code and keep the same naming.

I won’t spend too much time detailing the scope feature but don’t forget about it. If the code doesn’t work as intended, it’s probably a scoping issue.

Now let’s define the animation:

@property --x {
  syntax: "<number>";
  inherits: true;
  initial-value: 0; 
}
.container {
  animation: x linear;
  animation-timeline: --cx;
  animation-range: entry 100% exit 100%; 
}
@keyframes x {
  0%   { --x: 0; }
  100% { --x: 1; }
}

We define a keyframes that animates a variable from 0 to 1. We have to register that variable with a number type to be able to animate it. We run the animation on the container with a linear easing and define the timeline using animation-timeline.

At this step, we told the browser to consider the named timeline defined on the pseudo-element (the subject) as the reference for the animation progress. And that progress will be stored in the --x variable. At 50%, we have --x: 0.5, at 70%, we have --x: 0.7, and so on.

The last step is to add the formula we identified earlier:

@property --w {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0; 
}
.container {
  --w: calc(1/(1 - var(--x)));
}

The --w variable will contain the width in pixel of the container as a unitless value. It’s important to notice the “unitless” part. It gives us a lot of flexibility as we can integrate it within any formula. If you are a CSS hacker like me, you know what I mean!

What about that animation-range: entry 100% exit 100%;?

In addition to using a named timeline to define which element control the progress, we can also control the range of the animation. In other words, we can explicitly define where the 0% and 100% progress are located within the timeline.

Let’s get back to the first figure where I am showing the 0% and 100% progress.

The 0% is when the subject has completely entered the scroller from the right. We can express this using animation-range-start: entry 100%.

The 100% is when the subject has completely exited the scroller from the left. We can express this using animation-range-end: exit 100%.

Or using the shorthand:

animation-range: entry 100% exit 100%;

If you are new to scroll-driven animations, this part is not easy to grasp, so don’t worry if you don’t fully understand it. It requires some practice to build a mental model for it. Here is a good online tool that can help you visualize the different values.

Now, we do the same for the height and we are done. Here is the first demo again so you can inspect the full code.

Notice that I am using another pseudo-element to show the values. Let’s consider this as our first use case. Being able to get the width/height of any element and show them using only CSS is super cool!

.size::after {
  content: counter(w) "x" counter(h);
  counter-reset: w var(--w) h var(--h);
}

Are There Any Drawbacks?

Even if it seems to work fine, I still consider this as a “hack” to be used with caution. I am pretty sure it will fail in many situations so don’t consider this as a robust solution.

I also said “any element” in the title but in reality not all of them. It’s mandatory to be able to have a child element (the subject) so we cannot apply this trick to elements like <img> for example.

You also need to add overflow: auto (or hidden) to the container to make it the scroller for the subject. If you plan to have overflowing content then this solution will give you some trouble.

The value you will get using this method will include the padding but not the border! Pay attention to this part and compare the values you get with the ones of the Dev tools. You may need to perform another calculation to get the real dimension of the element by adding or subtracting specific amounts.

Another drawback is related to the use of 1px as our unit. We assumed that the size is a multiplier of 1px (which is true in most cases) but if your element is having a size like 185.15px, this trick won’t work. We can overcome this by using a smaller width for the subject (something like 0.01px) but I don’t think it is worth making this hack more complex.

A Few Use Cases

The first use case we saw is to show the dimension of the element which is a cool feature and can be a good one for debugging purposes. Let’s dig into more use cases.

Getting the Screen Dimension

We already have the viewport units vh and vw that works fine but this method can give us the unitless pixel values. You may ask how to do this since the viewport is not a real element. The solution is to rely on position: fixed applied to any element on the page. A fixed element is positioned relative to the viewport so its scroller will the viewport.

If you check the code, you will see that I am relying on the HTML pseudo-element for the subject and I don’t need to define any overflow or position on the HTML element. Plus the values are available globally since they are defined inside the HTML element!

For this particular case, I also have another CSS trick to get the screen dimension with an easier method:

Calculating the Scrollbar Width

There is a slight difference between the two screen width calculating methods above. The first demo will not include the scrollbar width if the page has a lot of content but the second one will. This means that If we combine both methods we can get the width of scrollbar!

Cool right? In addition to the screen dimension, you can also have the width of the scrollbar. Both values are available at root level so you can use them anywhere on the page.

Counting Stuff

All the calculations we did were based on the 1px size of the subject. If we change this to something else we can do some interesting counting. For example, if we consider 1lh (the height of the line box) we can count the number of lines inside a text.

Here is the version where you can edit the content. The number of lines will adjust based on the content you will enter.

Note how I am playing with the scope in this example. I am making the variable available at a higher level to be able to show the count inside a different element. Not only we can count the numbers of lines but we can also show the result anywhere on the page.

Can you think about something else to count? Share your example in the comment section.

Transferring Sizes

Being able to control the scope means that we can transfer the size of an element to another one on the page.

Here is an example where resizing the left element will also resize the right one!

Another important part of this trick is being able to get the width/height values as integer. This allows us to use them within any formula and append any unit to them.

Here is an example, where resizing the left element will rotate/scale the right one.

I have mapped the width with the rotation and the height with the scaling. Cool right? We can get the width/height of an element, have them as an integer, and transfer them to another element to do whatever we want. CSS is magic!

Conclusion

I hope you enjoyed this funny experiment. I still insist on the fact that it’s a hacky workaround to do something that was not possible using CSS. Use it for fun, use it to experiment with more CSS-only ideas but think twice before including this into a real project. Using one line of JavaScript code to get the dimension of an element is safer. Not all CSS-only tricks are a good replacement for JavaScript.

This said, if you find an interesting use case or you have another CSS-only experimentation where this trick can be useful, share it in the comment section.

I will end this article with a last demo where I am transforming the native progress element into a circular one. Can you figure out how it works? I am using the same technique. This time, I know both the width of the scroller and the subject and the missing variable is the progress. Try to dissect the code as homework 😜.

]]>
https://frontendmasters.com/blog/how-to-get-the-width-height-of-any-element-in-only-css/feed/ 0 3119
Ladybird & Independent Browser Engines https://frontendmasters.com/blog/ladybird/ https://frontendmasters.com/blog/ladybird/#comments Wed, 24 Jul 2024 17:41:41 +0000 https://frontendmasters.com/blog/?p=3132 Web browsers are tens of millions of lines of code written over decades to adhere to long, complex standards specifications and defend against all manner of malicious behavior. I’ve long been convinced that an entirely new browser (not a fork or a skinning) would be impossible to build today. Just scratch pad math, maybe 100 developers making 100k/year could do it over 5 years, so call it a 50m investment on developer power alone. Who ponies that up and why?

Well, Ladybird is giving it a go. They should have 7 full timers soon, and it’s open source so they’ll get help there, and are taking donations. Plus they’ll use some third-party resources to get it done, which should trim down on requirements. It reads like the thing already runs. Their big why is that Google is just too influential — and of course there is already controversy.

There is also Flow as well as Servo, which I’m told is the furthest along of all of them. They should get together on all this if you ask me. I’m happy to admit I was wrong and that it seems like new browser engines aren’t the fiction I thought they were.

]]>
https://frontendmasters.com/blog/ladybird/feed/ 1 3132
Clip Pathing Color Changes https://frontendmasters.com/blog/clip-pathing-color-changes/ https://frontendmasters.com/blog/clip-pathing-color-changes/#respond Tue, 23 Jul 2024 17:29:41 +0000 https://frontendmasters.com/blog/?p=3103 This is a nice post from Emil Kowalski on usage of the clip-path property in CSS. I’ve always liked clip-path. Maybe it’s because it’s such a sharp knife. When you clip an element, it’s clipped, yo. There isn’t a lot of nuance to it, it does what it does. But moreso, I think I like the connection to SVG (oh, hey, by the way, I just made my old book Practical SVG entirely free). The value that you give clip-path is stuff like circle(), polygon(), path(), etc — the primitive shapes of SVG.

In Emil’s post, my favorite example is a navigation bar where a “pill” shape animates from one navigation item to another when they are clicked. The pill is a different background color, and so the text color also changes. (If you’re over 100 years old like me, we used to call this kind of thing “lava lamp” navigation 👴).

I would guess most people would assume what is happening here is an extra element set behind the links that moves position to underneath the newly active links. You could do it that way, but there is a minor aesthetic issue with it. Because the background-color is changing here, the text also needs to change appropriately (from dark to light here). You could change that color instantly, but that will look weird like it’s changing too early. You could set a transition on it, but you’ll never get the fade to look quite right, especially as it has to go through an awkward gray color.

Essentially, you’ll never get a state like this:

This ain’t gonna happen with an underlay element alone.

See how the text is half-light and half-dark mid-animation when the highlight blue pill moves from one to another? That’s a lovely effect that makes this feel very polished and smooth. This idea first came from a tweet by Paco. Like Emil says:

You might say that not everyone is going to notice the difference, but I truly believe that small details like this add up and make the experience feel more polished. Even if they go unnoticed.

Agreed.

In Emil’s post, it’s done with React. That’s totally fine, but I figured I’d make a vanilla one for y’all here:

Here’s how this works:

  1. There is one set of semantic HTML navigation.
  2. If JavaScript executes, it duplicates the nav (we’ll need two) but ensures the duplicate is hidden for screen readers.
  3. The duplicate is placed exactly on top of the original (it’s the “blue” one) and can’t directly be clicked (i.e. pointer-events: none;)
  4. A clip-path is set that highlights one of the navigation items in particular by clipping the entire duplicate except one link.
  5. As links are clicked, the clip-path is changed using positional math, highlighting the new one. Also high-five for the round keyword that can be used with inset() for rounded corners on inset rectangles.
  6. The clip-path animates, thanks to a basic CSS transition.

I think it’s cool as heck that it all comes together that cleanly.

It’s also a nice touch that the new clip-path positions are calculated based on their page position, meaning that there are really no magic numbers here. If we add navigation items or change them, this code will be resilient and it will all still work. And if none of this JavaScript runs at all, no big deal.

]]>
https://frontendmasters.com/blog/clip-pathing-color-changes/feed/ 0 3103
How we use DDEV, Vite and Tailwind with Craft CMS https://frontendmasters.com/blog/how-we-use-ddev-vite-and-tailwind-with-craft-cms/ https://frontendmasters.com/blog/how-we-use-ddev-vite-and-tailwind-with-craft-cms/#respond Mon, 22 Jul 2024 21:34:43 +0000 https://frontendmasters.com/blog/?p=3105 I love a good post from real-world developers building real-world websites for clients that share how they work and the tools they use. Here, Joshua Pease of Viget shares How we use DDEV, Vite and Tailwind with Craft CMS, which is a change from webpack and Docker.

Goodbye complex esoteric Webpack configuration, hello Vite. Goodbye complex esoteric Docker Compose configuration, hello DDEV. This small change in tooling has completely transformed our development experience.

Plus a repo.

]]>
https://frontendmasters.com/blog/how-we-use-ddev-vite-and-tailwind-with-craft-cms/feed/ 0 3105
Introducing Svelte 5 https://frontendmasters.com/blog/introducing-svelte-5/ https://frontendmasters.com/blog/introducing-svelte-5/#comments Fri, 19 Jul 2024 18:40:23 +0000 https://frontendmasters.com/blog/?p=3067 Svelte has always been a delightful, simple, and fun framework to use. It’s a framework that’s always prioritized developer experience (DX), while producing a light and fast result with minimal JavaScript. It achieves this nice DX by giving users dirt simple idioms and a required compiler that makes everything work. Unfortunately, it used to be fairly easy to break Svelte’s reactivity. It doesn’t matter how fast a website is if it’s broken.

These reliability problems with reactivity are gone in Svelte 5.

In this post, we’ll get into the exciting Svelte 5 release (in Beta at the time of this writing). Svelte is the latest framework to add signals to power their reactivity. Svelte is now every bit as capable of handling robust web applications, with complex state, as alternatives like React and Solid. Best of all, it achieved this with only minimal hits to DX. It’s every bit as fun and easy to use as it was, but it’s now truly reliable, while still producing faster and lighter sites.

Let’s jump in!

The Plan

Let’s go through various pieces of Svelte, look at the “old” way, and then see how Svelte 5 changes things for the better. We’ll cover:

  1. State
  2. Props
  3. Effects

If you find this helpful, let me know, as I’d love to cover snippets and Svelte’s exciting new fine-grained reactivity.

As of this writing, Svelte 5 is late in the Beta phase. The API should be stable, although it’s certainly possible some new things might get added.

The docs are also still in beta, so here’s a preview URL for them. Svelte 5 might be released when you read this, at which point these docs will be on the main Svelte page. If you’d like to see the code samples below in action, you can find them in this repo.

State

Effectively managing state is probably the most crucial task for any web framework, so let’s start there.

State used to be declared with regular, plain old variable declarations, using let.

let value = 0;

Derived state was declared with a quirky, but technically valid JavaScript syntax of $:. For example:

let value = 0;
$: doubleValue = value * 2;

Svelte’s compiler would (in theory) track changes to value, and update doubleValue accordingly. I say in theory since, depending on how creatively you used value, some of the re-assignments might not make it to all of the derived state that used it.

You could also put entire code blocks after $: and run arbitrary code. Svelte would look at what you were referencing inside the code block, and re-run it when those things changed.

$: {
  console.log("Value is ", value);
}

Stores

Those variable declarations, and the special $: syntax was limited to Svelte components. If you wanted to build some portable state you could define anywhere, and pass around, you’d use a store.

We won’t go through the whole API, but here’s a minimal example of a store in action. We’ll define a piece of state that holds a number, and, based on what that number is at anytime, spit out a label indicating whether the number is even or odd. It’s silly, but it should show us how stores work.

import { derived, writable } from "svelte/store";

export function createNumberInfo(initialValue: number = 0) {
  const value = writable(initialValue);

  const derivedInfo = derived(value, value => {
    return {
      value,
      label: value % 2 ? "Odd number" : "Even number",
    };
  });

  return {
    update(newValue: number) {
      value.set(newValue);
    },
    numberInfo: derivedInfo,
  };
}

Writable stores exist to write values to. Derived stores take one or more other stores, read their current values, and project a new payload. If you want to provide a mechanism to set a new value, close over what you need to. To consume a store’s value, prefix it with a $ in a Svelte component. It’s not shown here, but there’s also a subscribe method on stores, and a get import. If the store returns an object with properties, you can either “dot through” to them, or you can use a reactive assignment ($:) to get those nested values. The example below shows both, and this distinction will come up later when we talk about interoperability between Svelte 4 and 5.

<script lang="ts">
  import { createNumberInfo } from './numberInfoStore';

  let store = createNumberInfo(0);

  $: ({ numberInfo, update } = store);
  $: ({ label, value } = $numberInfo);
</script>

<div class="flex flex-col gap-2 p-5">
  <span>{$numberInfo.value}</span>
  <span>{$numberInfo.label}</span>
  <hr />
  <span>{value}</span>
  <span>{label}</span>

  <button onclick={() => update($numberInfo.value + 1)}>
    Increment count
  </button>
</div>

This was the old Svelte.

This is a post on the new Svelte, so let’s turn our attention there.

State in Svelte 5

Things are substantially simpler in Svelte 5. Pretty much everything is managed by something new called “runes.” Let’s see what that means.

Runes

Svelte 5 joins the increasing number of JavaScript frameworks that use the concept of signals. There is a new feature called runes and under the covers they use signals. These accomplish a wide range of features from state to props and even side effects. Here’s a good introduction to runes.

To create a piece of state, we use the $state rune. You don’t import it, you just use it — it’s part of the Svelte language.

let count = $state(0);

For values with non-inferable types, you can provide a generic

let currentUser = $state<User | null>(null);

What if you want to create some derived state? Before we did:

$: countTimes2 = count * 2;

In Svelte 5 we use the $derived rune.

let countTimes2 = $derived(count * 2);

Note that we pass in a raw expression. Svelte will run it, see what it depends on, and re-run it as needed. There’s also a $derived.by rune if you want to pass an actual function.

If you want to use these state values in a Svelte template, you just use them. No need for special $ syntax to prefix the runes like we did with stores. You reference the values in your templates, and they update as needed.

If you want to update a state value, you assign to it:

count = count + 1;
// or count++;

What about stores?

We saw before that defining portable state outside of components was accomplished via stores. Stores are not deprecated in Svelte 5, but there’s a good chance they’re on their way out of the framework. You no longer need them, and they’re replaced with what we’ve already seen. That’s right, the $state and $derived runes we saw before can be defined outside of components in top-level TypeScript (or JavaScript) files. Just be sure to name your file with a .svelte.ts extension, so the Svelte compiler knows to enable runes in these files. Let’s take a look!

Let’s re-implement our number / label code from before, in Svelte 5. This is what it looked like with stores:

import { derived, writable } from "svelte/store";

export function createNumberInfo(initialValue: number = 0) {
  const value = writable(initialValue);

  const derivedInfo = derived(value, value => {
    return {
      value,
      label: value % 2 ? "Odd number" : "Even number",
    };
  });

  return {
    update(newValue: number) {
      value.set(newValue);
    },
    numberInfo: derivedInfo,
  };
}

Here it is with runes:

export function createNumberInfo(initialValue: number = 0) {
  let value = $state(initialValue);
  let label = $derived(value % 2 ? "Odd number" : "Even number");

  return {
    update(newValue: number) {
      value = newValue;
    },
    get value() {
      return value;
    },
    get label() {
      return label;
    },
  };
}

It’s 3 lines shorter, but more importantly, much simpler. We declared our state. We computed our derived state. And we send them both back, along with a method that updates our state.

You may be wondering why we did this:

  get value() {
    return value;
  },
  get label() {
    return label;
  }

rather than just referencing those properties. The reason is that reading that state, at any given point in time, evaluates the state rune, and, if we’re reading it in a reactive context (like a Svelte component binding, or inside of a $derived expression), then a subscription is set up to update any time that piece of state is updated. If we had done it like this:

// this won't work
return {
  update(newValue: number) {
    value = newValue;
  },
  value,
  label,
};

That wouldn’t have worked because those value and label pieces of state would be read and evaluated right there in the return value, with those raw values getting injected into that object. They would not be reactive, and they would never update.

That’s about it! Svelte 5 ships a few universal state primitives which can be used outside of components and easily constructed into larger reactive structures. What’s especially exciting is that Svelte’s component bindings are also updated, and now support fine-grained reactivity that didn’t used to exist.

Props

Defining state inside of a component isn’t too useful if you can’t pass it on to other components as props. Props are also reworked in Svelte 5 in a way that makes them simpler, and also, as we’ll see, includes a nice trick to make TypeScript integration more powerful.

Svelte 4 props were another example of hijacking existing JavaScript syntax to do something unrelated. To declare a prop on a component, you’d use the export keyword. It was weird, but it worked.

// ChildComponent.svelte
<script lang="ts">
  export let name: string;
  export let age: number;
  export let currentValue: string;
</script>

<div class="flex flex-col gap-2">
  {name} {age}
  <input bind:value={currentValue} />
</div>

This component created three props. It also bound the currentValue prop into the <input>, so it would change as the user typed. Then to render this component, we’d do something like this:

<script lang="ts">
  import ChildComponent from "./ChildComponent.svelte";

  let currentValue = "";
</script>

Current value in parent: {currentValue}
<ChildComponent name="Bob" age={20} bind:currentValue />

This is Svelte 4, so let currentValue = '' is a piece of state that can change. We pass props for name and age, but we also have bind:currentValue which is a shorthand for bind:currentValue={currentValue}. This creates a two-way binding. As the child changes the value of this prop, it propagates the change upward, to the parent. This is a very cool feature of Svelte, but it’s also easy to misuse, so exercise caution.

If we type in the ChildComponent’s <input>, we’ll see currentValue update in the parent component.

Svelte 5 version

Let’s see what these props look like in Svelte 5.

<script lang="ts">
  type Props = {
    name: string;
    age: number;
    currentValue: string;
  };

  let { age, name, currentValue = $bindable() }: Props = $props();
</script>

<div class="flex flex-col gap-2">
  {name} {age}
  <input bind:value={currentValue} />
</div>

The props are defined via the $props rune, from which we destructure the individual values.

let { age, name, currentValue = $bindable() }: Props = $props();

We can apply typings directly to the destructuring expression. In order to indicate that a prop can be (but doesn’t have to be) bound to the parent, like we saw above, we use the $bindable rune, like this

 = $bindable()

If you want to provide a default value, assign it to the destructured value. To assign a default value to a bindable prop, pass that value to the $bindable rune.

let { age = 10, name = "foo", currentValue = $bindable("bar") }: Props = $props();

But wait, there’s more!

One of the most exciting changes to Svelte’s prop handling is the improved TypeScript integration. We saw that you can assign types, above. But what if we want to do something like this (in React)

type Props<T> = {
  items: T[];
  onSelect: (item: T) => void;
};
export const AutoComplete = <T,>(props: Props<T>) => {
  return null;
};

We want a React component that receives an array of items, as well as a callback that takes a single item (of the same type). This works in React. How would we do it in Svelte?

At first, it looks easy.

<script lang="ts">
  type Props<T> = {
    items: T[];
    onSelect: (item: T) => void;
  };

  let { items, onSelect }: Props<T> = $props();
  //         Error here _________^
</script>

The first T is a generic parameter, which is defined as part of the Props type. This is fine. The problem is, we need to instantiate that generic type with an actual value for T when we attempt to use it in the destructuring. The T that I used there is undefined. It doesn’t exist. TypeScript has no idea what that T is because it hasn’t been defined.

What changed?

Why did this work so easily with React? The reason is, React components are functions. You can define a generic function, and when you call it TypeScript will infer (if it can) the values of its generic types. It does this by looking at the arguments you pass to the function. With React, rendering a component is conceptually the same as calling it, so TypeScript is able to look at the various props you pass, and infer the generic types as needed.

Svelte components are not functions. They’re a proprietary bit of code thrown into a .svelte file that the Svelte compiler turns into something useful. We do still render Svelte components, and TypeScript could easily look at the props we pass, and infer back the generic types as needed. The root of the problem, though, is that we haven’t (yet) declared any generic types that are associated with the component itself. With React components, these are the same generic types we declare for any function. What do we do for Svelte?

Fortunately, the Svelte maintainers thought of this. You can declare generic types for the component itself with the generics attribute on the <script> tag at the top of your Svelte component:

<script lang="ts" generics="T">
  type Props<T> = {
    items: T[];
    onSelect: (item: T) => void;
  };

  let { items, onSelect }: Props<T> = $props();
</script>

You can even define constraints on your generic arg:

<script lang="ts" generics="T extends { name: string }">
  type Props<T> = {
    items: T[];
    onSelect: (item: T) => void;
  };

  let { items, onSelect }: Props<T> = $props();
</script>

TypeScript will enforce this. If you violate that constraint like this:

<script lang="ts">
  import AutoComplete from "./AutoComplete.svelte";

  let items = [{ name: "Adam" }, { name: "Rich" }];
  let onSelect = (item: { id: number }) => {
    console.log(item.id);
  };
</script>

<div>
  <AutoComplete {items} {onSelect} />
</div>

TypeScript will let you know:

Type '(item: { id: number; }) => void' is not assignable to type '(item: { name: string; }) => void'. Types of parameters 'item' and 'item' are incompatible.

Property 'id' is missing in type '{ name: string; }' but required in type '{ id: number; }'.

Effects

Let’s wrap up with something comparatively easy: side effects. As we saw before, briefly, in Svelte 4 you could run code for side effects inside of $: reactive blocks

$: {
  console.log(someStateValue1, someStateValue2);
}

That code would re-run when either of those values changed.

Svelte 5 introduces the $effect rune. This will run after state has changed, and been applied to the dom. It is for side effects. Things like resetting the scroll position after state changes. It is not for synchronizing state. If you’re using the $effect rune to synchronize state, you’re probably doing something wrong (the same goes for the useEffect hook in React).

The code is pretty anti-climactic.

$effect(() => {
  console.log("Current count is ", count);
});

When this code first starts, and anytime count changes, you’ll see this log. To make it more interesting, let’s pretend we have a current timestamp value that auto-updates:

let timestamp = $state(+new Date());
setInterval(() => {
  timestamp = +new Date();
}, 1000);

We want to include that value when we log, but we don’t want our effect to run whenever our timestamp changes; we only want it to run when count changes. Svelte provides an untrack utility for that

import { untrack } from "svelte";

$effect(() => {
  let timestampValue = untrack(() => timestamp);
  console.log("Current count is ", count, "at", timestampValue);
});

Interop

Massive upgrades where an entire app is updated to use a new framework version’s APIs are seldom feasible, so it should come as no surprise that Svelte 5 continues to support Svelte 4. You can upgrade your app incrementally. Svelte 5 components can render Svelte 4 components, and Svelte 4 components can render Svelte 5 components. The one thing you can’t do is mix and match within a single component. You cannot use reactive assignments $: in the same component that’s using Runes (the Svelte compiler will remind you if you forget).

Since stores are not yet deprecated, they can continue to be used in Svelte 5 components. Remember the createNumberInfo method from before, which returned an object with a store on it? We can use it in Svelte 5. This component is perfectly valid, and works.

<script lang="ts">
  import { createNumberInfo } from '../svelte4/numberInfoStore';

  const numberPacket = createNumberInfo(0);

  const store = numberPacket.numberInfo;
  let junk = $state('Hello');
</script>

<span>Run value: {junk}</span>
<div>Number value: {$store.value}</div>

<button onclick={() => numberPacket.update($store.value + 1)}>Update</button>

But the rule against reactive assignments still holds; we cannot use one to destructure values off of stores when we’re in Svelte 5 components. We have to “dot through” to nested properties with things like {$store.value} in the binding (which always works) rather than:

$: ({ value } = $store);

… which generates the error of:

$: is not allowed in runes mode, use $derived or $effect instead

The error is even clear enough to give you another alternative to inlining those nested properties, which is to create a $derived state:

let value = $derived($store.value);
// or let { value } = $derived($store);

Personally I’m not a huge fan of mixing the new $derived primitive with the old Svelte 4 syntax of $store, but that’s a matter of taste.

Parting thoughts

Svelte 5 has shipped some incredibly exciting changes. We covered the new, more reliable reactivity primitives, the improved prop management with tighter TypeScript integration, and the new side effect primitive. But we haven’t come closing to covering everything. Not only are there more variations on the $state rune, but Svelte 5 also updated it’s event handling mechanism, and even shipped an exciting new way to re-use “snippets” of HTML.

Svelte 5 is worth a serious look for your next project.

]]>
https://frontendmasters.com/blog/introducing-svelte-5/feed/ 1 3067