I spend a lot of time in DevTools, and I’m sure you do too. Sometimes I even bounce between them, especially when I’m debugging cross-browser issues. DevTools is a lot like browsers themselves — not all of the features in one browser’s DevTools will be the same or supported in another browser’s DevTools.
But there are quite a few DevTools features that are interoperable, even some lesser-known ones that I’m about to share with you.
For the sake of brevity, I use “Chromium” to refer to all Chromium-based browsers, like Chrome, Edge, and Opera, in the article. Many of the DevTools in them offer the exact same features and capabilities as one another, so this is just my shorthand for referring to all of them at once.
Search nodes in the DOM tree
Sometimes the DOM tree is full of nodes nested in nodes that are nested in other nodes, and so on. That makes it pretty tough to find the exact one you’re looking for, but you can quickly search the DOM tree using Cmd + F (macOS) or Ctrl + F (Windows).
Additionally, you can also search using a valid CSS selector, like .red, or using an XPath, like //div/h1.
Searching text in Chrome DevTools (left), selectors in Firefox DevTools (center), and XPath in Safari DevTools (right)
In Chromium browsers, the focus automatically jumps to the node that matches the search criteria as you type, which could be annoying if you are working with longer search queries or a large DOM tree. Fortunately, you can disable this behavior by heading to Settings (F1) → Preferences → Global → Search as you type → Disable.
After you have located the node in the DOM tree, you can scroll the page to bring the node within the viewport by right-clicking on the nod, and selecting “Scroll into view”.
Access nodes from the console
DevTools provides many different ways to access a DOM node directly from the console.
For example, you can use $0 to access the currently selected node in the DOM tree. Chromium browsers take this one step further by allowing you to access nodes selected in the reverse chronological order of historic selection using, $1, $2, $3, etc.
Another thing that Chromium browsers allow you to do is copy the node path as a JavaScript expression in the form of document.querySelector by right-clicking on the node, and selecting Copy → Copy JS path, which can then be used to access the node in the console.
Here’s another way to access a DOM node directly from the console: as a temporary variable. This option is available by right-clicking on the node and selecting an option. That option is labeled differently in each browser’s DevTools:
Chromium: Right click → “Store as global variable”
Firefox: Right click → “Use in Console”
Safari: Right click → “Log Element”
Access a node as a temporary variable in the console, as shown in Chrome (left), Firefox (center), and Safari (right)
Visualize elements with badges
DevTools can help visualize elements that match certain properties by displaying a badge next to the node. Badges are clickable, and different browsers offer a variety of different badges.
In Safari, there is a badge button in the Elements panel toolbar which can be used to toggle the visibility of specific badges. For example, if a node has a display: grid or display: inline-grid CSS declaration applied to it, a grid badge is displayed next to it. Clicking on the badge will highlight grid areas, track sizes, line numbers, and more, on the page.
Grid overlay with badges in Safari DevTools
The badges that are currently supported in Firefox’s DevTools are listed in the Firefox source docs. For example, a scroll badge indicates a scrollable element. Clicking on the badge highlights the element causing the overflow with an overflow badge next to it.
In Chromium browsers, you can right-click on any node and select “Badge settings…” to open a container that lists all of the available badges. For example, elements with scroll-snap-type will have a scroll-snap badge next to it, which on click, will toggle the scroll-snap overlay on that element.
Taking screenshots
We’ve been able to take screenshots from some DevTools for a while now, but it’s now available in all of them and includes new ways to take full-page shots.
The process starts by right-clicking on the DOM node you want to capture. Then select the option to capture the node, which is labeled differently depending on which DevTools you’re using.
Chrome (left), Safari (middle), and Firefox (right)
Repeat the same steps on the html node to take a full-page screenshot. When you do, though, it’s worth noting that Safari retains the transparency of the element’s background color — Chromium and Firefox will capture it as a white background.
Comparing screenshots in Safari (left) and Chromium (right)
There’s another option! You can take a “responsive” screenshot of the page, which allows you to capture the page at a specific viewport width. As you might expect, each browser has different ways to get there.
Chromium: Cmd + Shift + M (macOS) or Ctrl + Shift + M (Windows). Or click the “Devices” icon next to the “Inspect” icon.
Launching responsive design mode in Safari (left), Firefox (right), and Chromium (bottom)
Chrome tip: Inspect the top layer
Chrome lets you visualize and inspect top-layer elements, like a dialog, alert, or modal. When an element is added to the #top-layer, it gets a top-layer badge next to it, which on click, jumps you to the top-layer container located just after the </html> tag.
The order of the elements in the top-layer container follows the stacking order, which means the last one is on the top. Click the reveal badge to jump back to the node.
Firefox tip: Jump to ID
Firefox links the element referencing the ID attribute to its target element in the same DOM and highlights it with an underline. Use CMD + Click (macOS) or CTRL + Click (Windows) )to jump to the target element with the identifier.
Wrapping up
Quite a few things, right? It’s awesome that there are some incredibly useful DevTools features that are supported in Chromium, Firefox, and Safari alike. Are there any other lesser-known features supported by all three that you like?
There are a few resources I keep close by to stay on top of what’s new. I thought I’d share them with here:
DevTools Tips (Patrick Brosset): A curated collection of helpful cross-browser DevTools tips and tricks.
Dev Tips (Umar Hansa): DevTools tips sent to your inbox!
There are many considerations when building a calendar component — far more than what is covered in the articles I linked up. If you think about it, calendars are fraught with nuance, from handling timezones and date formats to localization and even making sure dates flow from one month to the next… and that’s before we even get into accessibility and additional layout considerations depending on where the calendar is displayed and whatnot.
Many developers fear the Date() object and stick with older libraries like moment.js. But while there are many “gotchas” when it comes to dates and formatting, JavaScript has a lot of cool APIs and stuff to help out!
I don’t want to re-create the wheel here, but I will show you how we can get a dang good calendar with vanilla JavaScript. We’ll look into accessibility, using semantic markup and screenreader-friendly <time> -tags — as well as internationalization and formatting, using the Intl.Locale, Intl.DateTimeFormat and Intl.NumberFormat-APIs.
In other words, we’re making a calendar… only without the extra dependencies you might typically see used in a tutorial like this, and with some of the nuances you might not typically see. And, in the process, I hope you’ll gain a new appreciation for newer things that JavaScript can do while getting an idea of the sorts of things that cross my mind when I’m putting something like this together.
First off, naming
What should we call our calendar component? In my native language, it would be called “kalender element”, so let’s use that and shorten that to “Kal-El” — also known as Superman’s name on the planet Krypton.
Let’s create a function to get things going:
function kalEl(settings = {}) { ... }
This method will render a single month. Later we’ll call this method from [...Array(12).keys()] to render an entire year.
Initial data and internationalization
One of the common things a typical online calendar does is highlight the current date. So let’s create a reference for that:
const today = new Date();
Next, we’ll create a “configuration object” that we’ll merge with the optional settings object of the primary method:
We check, if the root element (<html>) contains a lang-attribute with locale info; otherwise, we’ll fallback to using en-US. This is the first step toward internationalizing the calendar.
We also need to determine which month to initially display when the calendar is rendered. That’s why we extended the config object with the primary date. This way, if no date is provided in the settings object, we’ll use the today reference instead:
const date = config.date ? new Date(config.date) : today;
We need a little more info to properly format the calendar based on locale. For example, we might not know whether the first day of the week is Sunday or Monday, depending on the locale. If we have the info, great! But if not, we’ll update it using the Intl.Locale API. The API has a weekInfo object that returns a firstDay property that gives us exactly what we’re looking for without any hassle. We can also get which days of the week are assigned to the weekend:
if (!config.info) config.info = new Intl.Locale(config.locale).weekInfo || {
firstDay: 7,
weekend: [6, 7]
};
Again, we create fallbacks. The “first day” of the week for en-US is Sunday, so it defaults to a value of 7. This is a little confusing, as the getDay method in JavaScript returns the days as [0-6], where 0 is Sunday… don’t ask me why. The weekends are Saturday and Sunday, hence [6, 7].
Before we had the Intl.Locale API and its weekInfo method, it was pretty hard to create an international calendar without many **objects and arrays with information about each locale or region. Nowadays, it’s easy-peasy. If we pass in en-GB, the method returns:
The last one is a Boolean that checks whether today exists in the month we’re about to render.
Semantic markup
We’re going to get deeper in rendering in just a moment. But first, I want to make sure that the details we set up have semantic HTML tags associated with them. Setting that up right out of the box gives us accessibility benefits from the start.
Calendar wrapper
First, we have the non-semantic wrapper: <kal-el>. That’s fine because there isn’t a semantic <calendar> tag or anything like that. If we weren’t making a custom element, <article> might be the most appropriate element since the calendar could stand on its own page.
Month names
The <time> element is going to be a big one for us because it helps translate dates into a format that screenreaders and search engines can parse more accurately and consistently. For example, here’s how we can convey “January 2023” in our markup:
The row above the calendar’s dates containing the names of the days of the week can be tricky. It’s ideal if we can write out the full names for each day — e.g. Sunday, Monday, Tuesday, etc. — but that can take up a lot of space. So, let’s abbreviate the names for now inside of an <ol> where each day is a <li>:
<ol>
<li><abbr title="Sunday">Sun</abbr></li>
<li><abbr title="Monday">Mon</abbr></li>
<!-- etc. -->
</ol>
We could get tricky with CSS to get the best of both worlds. For example, if we modified the markup a bit like this:
…we get the full names by default. We can then “hide” the full name when space runs out and display the title attribute instead:
@media all and (max-width: 800px) {
li abbr::after {
content: attr(title);
}
}
But, we’re not going that way because the Intl.DateTimeFormat API can help here as well. We’ll get to that in the next section when we cover rendering.
Day numbers
Each date in the calendar grid gets a number. Each number is a list item (<li>) in an ordered list (<ol>), and the inline <time> tag wraps the actual number.
<li>
<time datetime="2023-01-01">1</time>
</li>
And while I’m not planning to do any styling just yet, I know I will want some way to style the date numbers. That’s possible as-is, but I also want to be able to style weekday numbers differently than weekend numbers if I need to. So, I’m going to include data-* attributes specifically for that: data-weekend and data-today.
Week numbers
There are 52 weeks in a year, sometimes 53. While it’s not super common, it can be nice to display the number for a given week in the calendar for additional context. I like having it now, even if I don’t wind up not using it. But we’ll totally use it in this tutorial.
We’ll use a data-weeknumber attribute as a styling hook and include it in the markup for each date that is the week’s first date.
Let’s get the calendar on a page! We already know that <kal-el> is the name of our custom element. First thing we need to configure it is to set the firstDay property on it, so the calendar knows whether Sunday or some other day is the first day of the week.
We’ll be using template literals to render the markup. To format the dates for an international audience, we’ll use the Intl.DateTimeFormat API, again using the locale we specified earlier.
The month and year
When we call the month, we can set whether we want to use the long name (e.g. February) or the short name (e.g. Feb.). Let’s use the long name since it’s the title above the calendar:
For weekdays displayed above the grid of dates, we need both the long (e.g. “Sunday”) and short (abbreviated, ie. “Sun”) names. This way, we can use the “short” name when the calendar is short on space:
And finally, the days, wrapped in an <ol> element:
${[...Array(numOfDays).keys()].map(i => {
const cur = new Date(year, month, i + 1);
let day = cur.getDay(); if (day === 0) day = 7;
const today = renderToday && (config.today.day === i + 1) ? ' data-today':'';
return `
<li data-day="${day}"${today}${i === 0 || day === config.info.firstDay ? ` data-weeknumber="${new Intl.NumberFormat(locale).format(getWeek(cur))}"`:''}${config.info.weekend.includes(day) ? ` data-weekend`:''}>
<time datetime="${year}-${(pad(month))}-${pad(i)}" tabindex="0">
${new Intl.NumberFormat(locale).format(i + 1)}
</time>
</li>`
}).join('')}
Let’s break that down:
We create a “dummy” array, based on the “number of days” variable, which we’ll use to iterate.
We create a day variable for the current day in the iteration.
We fix the discrepancy between the Intl.Locale API and getDay().
If the day is equal to today, we add a data-* attribute.
Finally, we return the <li> element as a string with merged data.
tabindex="0" makes the element focusable, when using keyboard navigation, after any positive tabindex values (Note: you should never add positive tabindex-values)
To “pad” the numbers in the datetime attribute, we use a little helper method:
const pad = (val) => (val + 1).toString().padStart(2, '0');
Week number
Again, the “week number” is where a week falls in a 52-week calendar. We use a little helper method for that as well:
I didn’t write this getWeek-method. It’s a cleaned up version of this script.
And that’s it! Thanks to the Intl.Locale, Intl.DateTimeFormat and Intl.NumberFormat APIs, we can now simply change the lang-attribute of the <html> element to change the context of the calendar based on the current region:
de-DE
fa-IR
zh-Hans-CN-u-nu-hanidec
Styling the calendar
You might recall how all the days are just one <ol> with list items. To style these into a readable calendar, we dive into the wonderful world of CSS Grid. In fact, we can repurpose the same grid from a starter calendar template right here on CSS-Tricks, but updated a smidge with the :is() relational pseudo to optimize the code.
Notice that I’m defining configurable CSS variables along the way (and prefixing them with ---kalel- to avoid conflicts).
The seven-column grid works fine when the first day of the month is also the first day of the week for the selected locale). But that’s the exception rather than the rule. Most times, we’ll need to shift the first day of the month to a different weekday.
Remember all the extra data-* attributes we defined when writing our markup? We can hook into those to update which grid column (--kalel-li-gc) the first date number of the month is placed on:
In this case, we’re spanning from the first grid column to the fourth grid column — which will automatically “push” the next item (Day 2) to the fifth grid column, and so forth.
Let’s add a little style to the “current” date, so it stands out. These are just my styles. You can totally do what you’d like here.
I like the idea of styling the date numbers for weekends differently than weekdays. I’m going to use a reddish color to style those. Note that we can reach for the :not() pseudo-class to select them while leaving the current date alone:
Oh, and let’s not forget the week numbers that go before the first date number of each week. We used a data-weeknumber attribute in the markup for that, but the numbers won’t actually display unless we reveal them with CSS, which we can do on the ::before pseudo-element:
We’re technically done at this point! We can render a calendar grid that shows the dates for the current month, complete with considerations for localizing the data by locale, and ensuring that the calendar uses proper semantics. And all we used was vanilla JavaScript and CSS!
But let’s take this one more step…
Rendering an entire year
Maybe you need to display a full year of dates! So, rather than render the current month, you might want to display all of the month grids for the current year.
Well, the nice thing about the approach we’re using is that we can call the render method as many times as we want and merely change the integer that identifies the month on each instance. Let’s call it 12 times based on the current year.
as simple as calling the render-method 12 times, and just change the integer for month — i:
[...Array(12).keys()].map(i =>
render(
new Date(date.getFullYear(),
i,
date.getDate()),
config.locale,
date.getMonth()
)
).join('')
It’s probably a good idea to create a new parent wrapper for the rendered year. Each calendar grid is a <kal-el> element. Let’s call the new parent wrapper <jor-el>, where Jor-El is the name of Kal-El’s father.
<jor-el id="app" data-year="true">
<kal-el data-firstday="7">
<!-- etc. -->
</kal-el>
<!-- other months -->
</jor-el>
We can use <jor-el> to create a grid for our grids. So meta!
I figured we could do something similar without changing anything in the HTML or JavaScript. I’ve taken the liberty to include full names for months, and numbers instead of day names, to make it more readable. Enjoy!
You know what it’s like to pick up a new language or framework. Sometimes there’s great documentation to help you find your way through it. But even the best documentation doesn’t cover absolutely everything. And when you work with something that’s new, you’re bound to find a problem that doesn’t have a written solution.
That’s how it was for me the first time I created a React project — and React is one of those frameworks with remarkable documentation, especially now with the beta docs. But I still struggled my way through. It’s been quite a while since that project, but the lessons I gained from it are still fresh in my mind. And even though there are a lot of React “how-to” tutorials in out there, I thought I’d share what I wish I knew when I first used it.
So, that’s what this article is — a list of the early mistakes I made. I hope they help make learning React a lot smoother for you.
Using create-react-app to start a project
TL;DR Use Vite or Parcel.
Create React App (CRA) is a tool that helps you set up a new React project. It creates a development environment with the best configuration options for most React projects. This means you don’t have to spend time configuring anything yourself.
As a beginner, this seemed like a great way to start my work! No configuration! Just start coding!
CRA uses two popular packages to achieve this, webpack and Babel. webpack is a web bundler that optimizes all of the assets in your project, such as JavaScript, CSS, and images. Babel is a tool that allows you to use newer JavaScript features, even if some browsers don’t support them.
Both are good, but there are newer tools that can do the job better, specifically Vite and Speedy Web Compiler (SWC).
These new and improved alternatives are faster and easier to configure than webpack and Babel. This makes it easier to adjust the configuration which is difficult to do in create-react-app without ejecting.
To use them both when setting up a new React project you have to make sure you have Node version 12 or higher installed, then run the following command.
npm create vite
You’ll be asked to pick a name for your project. Once you do that, select React from the list of frameworks. After that, you can select either Javascript + SWC or Typescript + SWC
Then you’ll have to change directory cd into your project and run the following command;
npm i && npm run dev
This should run a development server for your site with the URL localhost:5173
Data can be passed to React components through something called props. These are added to a component just like attributes in an HTML element and can be used in a component’s definition by taking the relevant values from the prop object passed in as an argument.
With modern JavaScript, it is possible to destructure the props object and assign a default value to it all in the function argument.
// Card.jsx
function Card({title = "Default title", description= "Desc"}) {
return (
<div>
<h1>5 Mistakes I Made When Starting My First React Project</h1>
<p>{description}</p>
</div>
)
}
export default Card;
This is more favorable as the code that can be read by modern browsers without the need for extra transformation.
Unfortunately, defaultProps do require some transformation to be read by the browser since JSX (JavaScript XML) isn’t supported out of the box. This could potentially affect the performance of an application that is using a lot of defaultProps.
In React, the propTypes property can be used to check if a component is being passed the correct data type for its props. They allow you to specify the type of data that should be used for each prop such as a string, number, object, etc. They also allow you to specify if a prop is required or not.
This way, if a component is passed the wrong data type or if a required prop is not being provided, then React will throw an error.
TypeScript provides a level of type safety in data that’s being passed to components. So, sure, propTypes were a good idea back when I was starting. However, now that TypeScript has become the go-to solution for type safety, I would highly recommend using it over anything else.
TypeScript is a programming language that builds on top of JavaScript by adding static type-checking. TypeScript provides a more powerful type system, that can catch more potential bugs and improves the development experience.
Class components in React are created using JavaScript classes. They have a more object-oriented structure and as well as a few additional features, like the ability to use the this keyword and lifecycle methods.
I prefer writing components with classes over functions, but JavaScript classes are more difficult for beginners to understand and this can get very confusing. Instead, I’d recommend writing components as functions:
Function components are simply JavaScript functions that return JSX. They are much easier to read, and do not have additional features like the this keyword and lifecycle methods which make them more performant than class components.
Function components also have the advantage of using hooks. React Hooks allow you to use state and other React features without writing a class component, making your code more readable, maintainable and reusable.
TL;DR: There’s no need to do it, unless you need hooks.
Since React 17 was released in 2020, it’s now unnecessary to import React at the top of your file whenever you create a component.
import React from 'react'; // Not needed!
export default function Card() {}
But we had to do that before React 17 because the JSX transformer (the thing that converts JSX into regular JavaScript) used a method called React.createElement that would only work when importing React. Since then, a new transformer has been release which can transform JSX without the createElement method.
You will still need to import React to use hooks, fragments, and any other functions or components you might need from the library:
import { useState } from 'react';
export default function Card() {
const [count, setCount] = useState(0);
// ...
}
Those were my early mistakes!
Maybe “mistake” is too harsh a word since some of the better practices came about later. Still, I see plenty of instances where the “old” way of doing something is still being actively used in projects and other tutorials.
To be honest, I probably made way more than five mistakes when getting started. Anytime you reach for a new tool it is going to be more like a learning journey to use it effectively, rather than flipping a switch. But these are the things I still carry with me years later!
If you’ve been using React for a while, what are some of the things you wish you knew before you started? It would be great to get a collection going to help others avoid the same struggles.
I used to have this boss who loved, loved, loved, loved to emphasize words. This was way back before we used a WYSIWYG editors and I’d have to handcode that crap.
<p>
I used to have this boss who <em>loved</em>, <strong>loved</strong>,
<strong><em>loved</em></strong>, <strong><em><u>loved</u></em></strong>
to emphasize words.
</p>
(Let’s not go into the colors he used for even MOAR emphasis.)
Writing all that markup never felt great. The effort it took, sure, whatever. But is it even a good idea to add overload content with double — or more! — emphases?
Different tags convey different emphasis
For starters, the <strong> and <em> tags are designed for different uses. We got them back in HTML5, where:
So, <strong> gives the content more weight in the sense it suggests that the content in it is important or urgent. Think of a warning:
Warning: The following content has been flagged for being awesome.
It might be tempting to reach for <em> to do the same thing. Italicized text can be attention-grabbing after all. But it’s really meant as a hint to use more emphasis when readingt the content in it. For example, here are two versions of the same sentence with the emphasis in different locations:
<p>I ate the <em>entire</em> plate of burritos.</p>
<p>I ate the entire <em>plate</em> of burritos.</p>
Both examples stress emphasis, but on different words. And they would sound different if you were to read them out loud. That makes <em> a great way to express tone in your writing. It changes the meaning of the sentence in a way that <strong> does not.
Visual emphasis vs. semantic emphasis
Those are two things you gotta weigh when emphasizing content. Like, there are plenty of instances where you may need to italicize content without affecting the meaning of the sentence. But those can be handled with other tags that render italics:
<i>: This is the classic one! Before HTML5, this was used to stress emphasis with italics all over the place. Now, it’s purely used to italicize content visually without changing the semantic meaning.
<cite>: Indicating the source of a fact or figure. (“Source: CSS-Tricks“)
<address>: Used to mark up contact information, not only physical addresses, but things like email addresses and phone numbers too. (
howdy@example.com
)
It’s going to he the same thing with <strong>. Rather than using it for styling text you want to look heavier, it’s a better idea to use the classic <b> tag for boldfacing to avoid giving extra signficance to content that doesn’t need it. And remember, some elements like headings are already rendered in bold, thanks to the browser’s default styles. There’s no need to add even more strong emphasis.
Using italics in emphasized content (and vice versa)
There are legitimate cases where you may need to italicize part of a line that’s already emphasized. Or maybe add emphasis to a bit of text that’s already italicized.
A blockquote might be a good example. I’ve seen plenty of times where they are italicized for style, even though default browser styles don’t do it:
blockquote {
font-style: italic;
}
What if we need to mention a movie title in that blockquote? That should be italicized. There’s no stress emphasis needed, so an <i> tag will do. But it’s still weird to italicize something when it’s already rendered that way:
<blockquote>
This movie’s opening weekend performance offers some insight in
to its box office momentum as it fights to justify its enormous
budget. In its first weekend, <i>Avatar: The Way of Water</i> made
$134 million in North America alone and $435 million globally.
</blockquote>
In a situation where we’re italicizing something within italicized content like this, we’re supposed to remove the italics from the nested element… <i> in this case.
This little snippet evaluates the blockquote to see if it’s font-style is set to italic. If it is, then it’ll make sure the <em>, <i>, <cite>, and <address> elements are rendered as normal text, while retaining the semantic meaning if there is one.
But back to emphasis within emphasis
I wouldn’t nest <strong> inside <em> like this:
<p>I ate the <em><strong>entire</strong></em> plate of burritos.</p>
…or nest <em> inside <strong> instead:
<p>I ate the <em><strong>entire</strong></em> plate of burritos.</p>
The rendering is fine! And it doesn’t matter what order they’re in… at least in modern browsers. Jennifer Kyrnin mentions that some browsers only render the tag nearest to the text, but I didn’t bump into that anywhere in my limited tests. But something to watch for!
The reason I wouldn’t nest one form of emphasis in another is because it simply isn’t needed. There is no grammar rule that calls for it. Like exclamation points, one form of emphasis is enough, and you ought to use the one that matches what you’re after whether it’s visual, weight, or announced emphasis.
And even though some screen readers are capable of announcing emphasized content, they won’t read the markup with any additional importance or emphasis. So, no additional accessibility perks either, as far as I can tell.
But I really want all the emphasis!
If you’re in the position where your boss is like mine and wants ALL the emphasis, I’d reach for the right HTML tag for the type of emphasis, then apply the rest of the styles with a mix of tags that don’t affect semantics with CSS to help account for anything browser styles won’t handle.
<style>
/* If `em` contains `b` or `u` tags */
em:has(b, u) {
color: #f8a100;
}
</style>
<p>
I used to have this boss who <em>loved</em>, <strong>loved</strong>,
<strong><em>loved</em></strong>, <strong><em><u>loved</u></em></strong>
to emphasize words.
</p>
I might even do it with the <strong> tag too as a defensive measure:
/* If `em` contains `b` or `u` tags */
em:has(b, u),
/* If `strong` contains `em` or `u` tags */
strong:has(i, u) {
color: #f8a100;
}
As long as we’re playing defense, we can identify errors where emphases are nested within emphases by highlighting them in red or something:
Then I’d probably use that snippet from the last section that removes the default italic styling from an element when it is nested in another italiczed element.
Anything else?
Mayyyyybe:
Make sure your webfont includes bold and italic variations — otherwise, you’ll be relying on the browser to try to bold or italicize text for you. But limit your font files to just the weights and styles you need for better performance.
Check your analytics for the browsers your visitors use and test accordingly. Even though I didn’t run into a browser that balked at <strong> in <em> or the other way around, there may be a browser or several that will.
Do you know that kind of effect where someone’s head is poking through a circle or hole? The famous Porky Pig animation where he waves goodbye while popping out of a series of red rings is the perfect example, and Kilian Valkhof actually re-created that here on CSS-Tricks a while back.
I have a similar idea but tackled a different way and with a sprinkle of animation. I think it’s pretty practical and makes for a neat hover effect you can use on something like your own avatar.
CodePen Embed Fallback
See that? We’re going to make a scaling animation where the avatar seems to pop right out of the circle it’s in. Cool, right? Don’t look at the code and let’s build this animation together step-by-step.
The HTML: Just one element
If you haven’t checked the code of the demo and you are wondering how many divs this’ll take, then stop right there, because our markup is nothing but a single image element:
<img src="" alt="">
Yes, a single element! The challenging part of this exercise is using the smallest amount of code possible. If you have been following me for a while, you should be used to this. I try hard to find CSS solutions that can be achieved with the smallest, most maintainable code possible.
I wrote a series of articles here on CSS-Tricks where I explore different hover effects using the same HTML markup containing a single element. I go into detail on gradients, masking, clipping, outlines, and even layout techniques. I highly recommend checking those out because I will re-use many of the tricks in this post.
An image file that’s square with a transparent background will work best for what we’re doing. Here’s the one I’m using if you want start with that.
I’m hoping to see lots of examples of this as possible using real images — so please share your final result in the comments when you’re done so we can build a collection!
Before jumping into CSS, let’s first dissect the effect. The image gets bigger on hover, so we’ll for sure use transform: scale() in there. There’s a circle behind the avatar, and a radial gradient should do the trick. Finally, we need a way to create a border at the bottom of the circle that creates the appearance of the avatar behind the circle.
We said that the background would be a radial gradient. That’s perfect because we can create hard stops between the colors of a radial gradient, which make it look like we’re drawing a circle with solid lines.
Note the CSS variable, --b, I’m using there. It represents the thickness of the “border” which is really just being used to define the hard color stops for the red part of the radial gradient.
CodePen Embed Fallback
The next step is to play with the gradient size on hover. The circle needs to keep its size as the image grows. Since we are applying a scale() transformation, we actually need to decrease the size of the circle because it otherwise scales up with the avatar. So, while the image scales up, we need the gradient to scale down.
Let’s start by defining a CSS variable, --f, that defines the “scale factor”, and use it to set the size of the circle. I’m using 1 as the default value, as in that’s the initial scale for the image and the circle that we transform from.
Here is a demo to illustrate the trick. Hover to see what is happening behind the scenes:
CodePen Embed Fallback
I added a third color to the radial-gradient to better identify the area of the gradient on hover:
Now we have to position our background at the center of the circle and make sure it takes up the full height. I like to declare everything directly on the background shorthand property, so we can add our background positioning and make sure it doesn’t repeat by tacking on those values right after the radial-gradient():
The background is placed at the center (50%), has a width equal to calc(100%/var(--f)), and has a height equal to 100%.
Nothing scales when --f is equal to 1 — again, our initial scale. Meanwhile, the gradient takes up the full width of the container. When we increase --f, the element’s size grows — thanks to the scale() transform — and the gradient’s size decreases.
Here’s what we get when we apply all of this to our demo:
CodePen Embed Fallback
We’re getting closer! We have the overflow effect at the top, but we still need to hide the bottom part of the image, so it looks like it is popping out of the circle rather than sitting in front of it. That’s the tricky part of this whole thing and is what we’re going to do next.
The bottom border
I first tried tackling this with the border-bottom property, but I was unable to find a way to match the size of the border to the size to the circle. Here’s the best I could get and you can immediately see it’s wrong:
CodePen Embed Fallback
The actual solution is to use the outline property. Yes, outline, not border. In a previous article, I show how outline is powerful and allows us to create cool hover effects. Combined with outline-offset, we have exactly what we need for our effect.
The idea is to set an outline on the image and adjust its offset to create the bottom border. The offset will depend on the scaling factor the same way the gradient size did.
CodePen Embed Fallback
Now we have our bottom “border” (actually an outline) combined with the “border” created by the gradient to create a full circle. We still need to hide portions of the outline (from the top and the sides), which we’ll get to in a moment.
Here’s our code so far, including a couple more CSS variables you can use to configure the image size (--s) and the “border” color (--c):
Since we need a circular bottom border, we added a border-radius on the bottom side, allowing the outline to match the curvature of the gradient.
The calculation used on outline-offset is a lot more straightforward than it looks. By default, outline is drawn outside of the element’s box. And in our case, we need it to overlap the element. More precisely, we need it to follow the circle created by the gradient.
When we scale the element, we see the space between the circle and the edge. Let’s not forget that the idea is to keep the circle at the same size after the scale transformation runs, which leaves us with the space we will use to define the outline’s offset as illustrated in the above figure.
Let’s not forget that the second element is scaled, so our result is also scaled… which means we need to divide the result by f to get the real offset value:
Offset = ((f - 1) * S/2) / f = (1 - 1/f) * S/2
We add a negative sign since we need the outline to go from the outside to the inside:
Offset = (1/f - 1) * S/2
Here’s a quick demo that shows how the outline follows the gradient:
CodePen Embed Fallback
You may already see it, but we still need the bottom outline to overlap the circle rather than letting it bleed through it. We can do that by removing the border’s size from the offset:
There is no particular logic to that top padding. The idea is to ensure the outline doesn’t touch the avatar’s head. I used the element’s size to define that space to always have the same proportion.
Note that I have added the content-box value to the background:
We need this because we added padding and we only want the background set to the content box, so we must explicitly tell the background to stop there.
Adding CSS mask to the mix
We reached the last part! All we need to do is to hide some pieces, and we are done. For this, we will rely on the mask property and, of course, gradients.
Here is a figure to illustrate what we need to hide or what we need to show to be more accurate
The left image is what we currently have, and the right is what we want. The green part illustrates the mask we must apply to the original image to get the final result.
We can identify two parts of our mask:
A circular part at the bottom that has the same dimension and curvature as the radial gradient we used to create the circle behind the avatar
A rectangle at the top that covers the area inside the outline. Notice how the outline is outside the green area at the top — that’s the most important part, as it allows the outline to be cut so that only the bottom part is visible.
Let’s break down that mask property. For starters, notice that a similar radial-gradient() from the background property is in there. I created a new variable, --_g, for the common parts to make things less cluttered.
This creates the rectangle part of the mask. Its width is equal to the radial gradient’s width minus twice the border thickness:
calc(100% / var(--f) - 2 * var(--b))
The rectangle’s height is equal to half, 50%, of the element’s size.
We also need the linear gradient placed at the horizontal center (50%) and offset from the top by the same value as the outline’s offset. I created another CSS variable, --_o, for the offset we previously defined:
One of the confusing things here is that we need a negative offset for the outline (to move it from outside to inside) but a positive offset for the gradient (to move from top to bottom). So, if you’re wondering why we multiply the offset, --_o, by -1, well, now you know!
Here is a demo to illustrate the mask’s gradient configuration:
CodePen Embed Fallback
Hover the above and see how everything move together. The middle box illustrates the mask layer composed of two gradients. Imagine it as the visible part of the left image, and you get the final result on the right!
Wrapping up
Oof, we’re done! And not only did we wind up with a slick hover animation, but we did it all with a single HTML <img> element. Just that and less than 20 lines of CSS trickery!
Sure, we relied on some little tricks and math formulas to reach such a complex effect. But we knew exactly what to do since we identified the pieces we needed up-front.
Could we have simplified the CSS if we allowed ourselves more HTML? Absolutely. But we’re here to learn new CSS tricks! This was a good exercise to explore CSS gradients, masking, the outline property’s behavior, transformations, and a whole bunch more. If you felt lost at any point, then definitely check out my series that uses the same general concepts. It sometimes helps to see more examples and use cases to drive a point home.
I will leave you with one last demo that uses photos of popular CSS developers. Don’t forget to show me a demo with your own image so I can add it to the collection!
My previous post was a broad overview of SvelteKit where we saw what a great tool it is for web development. This post will fork off what we did there and dive into every developer’s favorite topic: caching. So, be sure to give my last post a read if you haven’t already. The code for this post is available on GitHub, as well as a live demo.
This post is all about data handling. We’ll add some rudimentary search functionality that will modify the page’s query string (using built-in SvelteKit features), and re-trigger the page’s loader. But, rather than just re-query our (imaginary) database, we’ll add some caching so re-searching prior searches (or using the back button) will show previously retrieved data, quickly, from cache. We’ll look at how to control the length of time the cached data stays valid and, more importantly, how to manually invalidate all cached values. As icing on the cake, we’ll look at how we can manually update the data on the current screen, client-side, after a mutation, while still purging the cache.
This will be a longer, more difficult post than most of what I usually write since we’re covering harder topics. This post will essentially show you how to implement common features of popular data utilities like react-query; but instead of pulling in an external library, we’ll only be using the web platform and SvelteKit features.
Unfortunately, the web platform’s features are a bit lower level, so we’ll be doing a bit more work than you might be used to. The upside is we won’t need any external libraries, which will help keep bundle sizes nice and small. Please don’t use the approaches I’m going to show you unless you have a good reason to. Caching is easy to get wrong, and as you’ll see, there’s a bit of complexity that’ll result in your application code. Hopefully your data store is fast, and your UI is fine allowing SvelteKit to just always request the data it needs for any given page. If it is, leave it alone. Enjoy the simplicity. But this post will show you some tricks for when that stops being the case.
Speaking of react-query, it was just released for Svelte! So if you find yourself leaning on manual caching techniques a lot, be sure to check that project out, and see if it might help.
Setting up
Before we start, let’s make a few small changes to the code we had before. This will give us an excuse to see some other SvelteKit features and, more importantly, set us up for success.
First, let’s move our data loading from our loader in +page.server.js to an API route. We’ll create a +server.js file in routes/api/todos, and then add a GET function. This means we’ll now be able to fetch (using the default GET verb) to the /api/todos path. We’ll add the same data loading code as before.
import { json } from "@sveltejs/kit";
import { getTodos } from "$lib/data/todoData";
export async function GET({ url, setHeaders, request }) {
const search = url.searchParams.get("search") || "";
const todos = await getTodos(search);
return json(todos);
}
Next, let’s take the page loader we had, and simply rename the file from +page.server.js to +page.js (or .ts if you’ve scaffolded your project to use TypeScript). This changes our loader to be a “universal” loader rather than a server loader. The SvelteKit docs explain the difference, but a universal loader runs on both the server and also the client. One advantage for us is that the fetch call into our new endpoint will run right from our browser (after the initial load), using the browser’s native fetch function. We’ll add standard HTTP caching in a bit, but for now, all we’ll do is call the endpoint.
Yep, forms can target directly to our normal page loaders. Now we can add a search term in the search box, hit Enter, and a “search” term will be appended to the URL’s query string, which will re-run our loader and search our to-do items.
Let’s also increase the delay in our todoData.js file in /lib/data. This will make it easy to see when data are and are not cached as we work through this post.
We’ll look at manual invalidation shortly, but all this function says is to cache these API calls for 60 seconds. Set this to whatever you want, and depending on your use case, stale-while-revalidate might also be worth looking into.
And just like that, our queries are caching.
Note make sure you un-check the checkbox that disables caching in dev tools.
Remember, if your initial navigation on the app is the list page, those search results will be cached internally to SvelteKit, so don’t expect to see anything in DevTools when returning to that search.
What is cached, and where
Our very first, server-rendered load of our app (assuming we start at the /list page) will be fetched on the server. SvelteKit will serialize and send this data down to our client. What’s more, it will observe the Cache-Control header on the response, and will know to use this cached data for that endpoint call within the cache window (which we set to 60 seconds in put example).
After that initial load, when you start searching on the page, you should see network requests from your browser to the /api/todos list. As you search for things you’ve already searched for (within the last 60 seconds), the responses should load immediately since they’re cached.
What’s especially cool with this approach is that, since this is caching via the browser’s native caching, these calls could (depending on how you manage the cache busting we’ll be looking at) continue to cache even if you reload the page (unlike the initial server-side load, which always calls the endpoint fresh, even if it did it within the last 60 seconds).
Obviously data can change anytime, so we need a way to purge this cache manually, which we’ll look at next.
Cache invalidation
Right now, data will be cached for 60 seconds. No matter what, after a minute, fresh data will be pulled from our datastore. You might want a shorter or longer time period, but what happens if you mutate some data and want to clear your cache immediately so your next query will be up to date? We’ll solve this by adding a query-busting value to the URL we send to our new /todos endpoint.
Let’s store this cache busting value in a cookie. That value can be set on the server but still read on the client. Let’s look at some sample code.
We can create a +layout.server.js file at the very root of our routes folder. This will run on application startup, and is a perfect place to set an initial cookie value.
You may have noticed the isDataRequest value. Remember, layouts will re-run anytime client code calls invalidate(), or anytime we run a server action (assuming we don’t turn off default behavior). isDataRequest indicates those re-runs, and so we only set the cookie if that’s false; otherwise, we send along what’s already there.
The httpOnly: false flag is also significant. This allows our client code to read these cookie values in document.cookie. This would normally be a security concern, but in our case these are meaningless numbers that allow us to cache or cache bust.
Reading cache values
Our universal loader is what calls our /todos endpoint. This runs on the server or the client, and we need to read that cache value we just set up no matter where we are. It’s relatively easy if we’re on the server: we can call await parent() to get the data from parent layouts. But on the client, we’ll need to use some gross code to parse document.cookie:
export function getCookieLookup() {
if (typeof document !== "object") {
return {};
}
return document.cookie.split("; ").reduce((lookup, v) => {
const parts = v.split("=");
lookup[parts[0]] = parts[1];
return lookup;
}, {});
}
const getCurrentCookieValue = name => {
const cookies = getCookieLookup();
return cookies[name] ?? "";
};
Fortunately, we only need it once.
Sending out the cache value
But now we need to send this value to our /todos endpoint.
getCurrentCookieValue('todos-cache') has a check in it to see if we’re on the client (by checking the type of document), and returns nothing if we are, at which point we know we’re on the server. Then it uses the value from our layout.
Busting the cache
But how do we actually update that cache busting value when we need to? Since it’s stored in a cookie, we can call it like this from any server action:
It’s all downhill from here; we’ve done the hard work. We’ve covered the various web platform primitives we need, as well as where they go. Now let’s have some fun and write application code to tie it all together.
For reasons that’ll become clear in a bit, let’s start by adding an editing functionality to our /list page. We’ll add this second table row for each todo:
And, of course, we’ll need to add a form action for our /list page. Actions can only go in .server pages, so we’ll add a +page.server.js in our /list folder. (Yes, a +page.server.js file can co-exist next to a +page.js file.)
We’re grabbing the form data, forcing a delay, updating our todo, and then, most importantly, clearing our cache bust cookie.
Let’s give this a shot. Reload your page, then edit one of the to-do items. You should see the table value update after a moment. If you look in the Network tab in DevToold, you’ll see a fetch to the /todos endpoint, which returns your new data. Simple, and works by default.
Immediate updates
What if we want to avoid that fetch that happens after we update our to-do item, and instead, update the modified item right on the screen?
This isn’t just a matter of performance. If you search for “post” and then remove the word “post” from any of the to-do items in the list, they’ll vanish from the list after the edit since they’re no longer in that page’s search results. You could make the UX better with some tasteful animation for the exiting to-do, but let’s say we wanted to not re-run that page’s load function but still clear the cache and update the modified to-do so the user can see the edit. SvelteKit makes that possible — let’s see how!
First, let’s make one little change to our loader. Instead of returning our to-do items, let’s return a writeable store containing our to-dos.
return {
todos: writable(todos),
};
Before, we were accessing our to-dos on the data prop, which we do not own and cannot update. But Svelte lets us return our data in their own store (assuming we’re using a universal loader, which we are). We just need to make one more tweak to our /list page.
Instead of this:
{#each todos as t}
…we need to do this since todos is itself now a store.:
{#each $todos as t}
Now our data loads as before. But since todos is a writeable store, we can update it.
First, let’s provide a function to our use:enhance attribute:
This will run before a submit. Let’s write that next:
function executeSave({ data }) {
const id = data.get("id");
const title = data.get("title");
return async () => {
todos.update(list =>
list.map(todo => {
if (todo.id == id) {
return Object.assign({}, todo, { title });
} else {
return todo;
}
})
);
};
}
This function provides a data object with our form data. We return an async function that will run after our edit is done. The docs explain all of this, but by doing this, we shut off SvelteKit’s default form handling that would have re-run our loader. This is exactly what we want! (We could easily get that default behavior back, as the docs explain.)
We now call update on our todos array since it’s a store. And that’s that. After editing a to-do item, our changes show up immediately and our cache is cleared (as before, since we set a new cookie value in our editTodo form action). So, if we search and then navigate back to this page, we’ll get fresh data from our loader, which will correctly exclude any updated to-do items that were updated.
We can set cookies in any server load function (or server action), not just the root layout. So, if some data are only used underneath a single layout, or even a single page, you could set that cookie value there. Moreoever, if you’re not doing the trick I just showed manually updating on-screen data, and instead want your loader to re-run after a mutation, then you could always set a new cookie value right in that load function without any check against isDataRequest. It’ll set initially, and then anytime you run a server action that page layout will automatically invalidate and re-call your loader, re-setting the cache bust string before your universal loader is called.
Writing a reload function
Let’s wrap-up by building one last feature: a reload button. Let’s give users a button that will clear cache and then reload the current query.
In a real project you probably wouldn’t copy/paste the same code to set the same cookie in the same way in multiple places, but for this post we’ll optimize for simplicity and readability.
We could call this done and move on, but let’s improve this solution a bit. Specifically, let’s provide feedback on the page to tell the user the reload is happening. Also, by default, SvelteKit actions invalidate everything. Every layout, page, etc. in the current page’s hierarchy would reload. There might be some data that’s loaded once in the root layout that we don’t need to invalidate or re-load.
So, let’s focus things a bit, and only reload our to-dos when we call this function.
We’re setting a new reloading variable to true at the start of this action. And then, in order to override the default behavior of invalidating everything, we return an async function. This function will run when our server action is finished (which just sets a new cookie).
Without this async function returned, SvelteKit would invalidate everything. Since we’re providing this function, it will invalidate nothing, so it’s up to us to tell it what to reload. We do this with the invalidate function. We call it with a value of reload:todos. This function returns a promise, which resolves when the invalidation is complete, at which point we set reloading back to false.
Lastly, we need to sync our loader up with this new reload:todos invalidation value. We do that in our loader with the depends function:
export async function load({ fetch, url, setHeaders, depends }) {
depends('reload:todos');
// rest is the same
And that’s that. depends and invalidate are incredibly useful functions. What’s cool is that invalidate doesn’t just take arbitrary values we provide like we did. We can also provide a URL, which SvelteKit will track, and invalidate any loaders that depend on that URL. To that end, if you’re wondering whether we could skip the call to depends and invalidate our /api/todos endpoint altogether, you can, but you have to provide the exact URL, including the search term (and our cache value). So, you could either put together the URL for the current search, or match on the path name, like this:
invalidate(url => url.pathname == "/api/todos");
Personally, I find the solution that uses depends more explicit and simple. But see the docs for more info, of course, and decide for yourself.
If you’d like to see the reload button in action, the code for it is in this branch of the repo.
Parting thoughts
This was a long post, but hopefully not overwhelming. We dove into various ways we can cache data when using SvelteKit. Much of this was just a matter of using web platform primitives to add the correct cache, and cookie values, knowledge of which will serve you in web development in general, beyond just SvelteKit.
Moreover, this is something you absolutely do not need all the time. Arguably, you should only reach for these sort of advanced features when you actually need them. If your datastore is serving up data quickly and efficiently, and you’re not dealing with any kind of scaling problems, there’s no sense in bloating your application code with needless complexity doing the things we talked about here.
As always, write clear, clean, simple code, and optimize when necessary. The purpose of this post was to provide you those optimization tools for when you truly need them. I hope you enjoyed it!
But if you’re looking for gains on the CSS side of things, Patrick has a nice way of sniffing out your most expensive selectors using Edge DevTools:
Crack open DevTools.
Head to the Performance Tab.
Make sure you have the “Enable advanced rendering instrumentation” option enabled. This tripped me up in the process.
Record a page load.
Open up the “Bottom-Up” tab in the report.
Check out your the size of your recalculated styles.
From here, click on one of the Recalculated Style events in the Main waterfall view and you’ll get a new “Selector Stats” tab. Look at all that gooey goodness!
Now you see all of the selectors that were processed and they can be sorted by how long they took, how many times they matched, the number of matching attempts, and something called “fast reject count” which I learned is the number of elements that were easy and quick to eliminate from matching.
A lot of insights here if CSS is really the bottleneck that needs investigating. But read Patrick’s full post over on the Microsoft Edge Blog because he goes much deeper into the why’s and how’s, and walks through an entire case study.
But animating these grid properties only recently gained supported by all three major browsers. Shall we take a look at a few examples to get the creative juices flowing?
A simple two-column grid. Now, before, you might not have built this using CSS Grid because animations and transitions weren’t supported, but what if you wanted the left column — perhaps a sidebar navigation — to expand on hover? Well, now that’s possible.
I know what you’re thinking: “Animating a CSS property? Easy peasy, I’ve been doing it for years!” Me too. However, I ran into an interesting snag while experimenting with a particular use case.
So, we want to transition the grid itself (specifically grid-template-columns, which is set on the .grid class in the example). But the left column (.left) is the selector that requires the :hover pseudo-class. While JavaScript can solve this conundrum easily — thanks, but no thanks — we can accomplish it with CSS alone.
Let’s walk through the whole thing, starting with the HTML. Pretty standard stuff really… a grid with two columns.
Putting the cosmetic CSS aside, you’ll first need to set display: grid on the parent container (.grid).
.grid {
display: grid;
}
Next, we can define and size the two columns using the grid-template-columns property. We’ll make the left column super narrow, and later increase its width on hover. The right column takes up the rest of the remaining space, thanks to the auto keyword.
We know we’re going to animate this thing, so let’s go ahead and throw a transition in there while we’re at it so the change between states is smooth and noticeable.
That’s it for the .grid! All that’s left is to apply the hover state. Specifically, we’re going to override the grid-template-columns property so that the left column takes up a greater amount of space on hover.
This alone isn’t all that interesting, although it’s awesome that animations and transitions are supported now in CSS Grid. What’s more interesting is that we can use the relatively new :has()pseudo-class to style the parent container (.grid) while the child (.left) is hovered.
.grid:has(.left:hover) {
/* Hover styles */
}
In plain English this is saying, “Do something to the .grid container if it contains an element named .left inside of it that is in a hover state.” That’s why :has() is often referred to as a “parent” selector. We can finally select a parent based on the children it contains — no JavaScript required!
So, let’s increase the width of the .left column to 30% when it is hovered. The .right column will continue to take up all the leftover space:
We could use CSS variables as well, which may or may not look cleaner depending on your personal preferences (or you might be using CSS variables in your project anyway):
I love that CSS grids can be animated now, but the fact that we can build this particular example with just nine lines of CSS is even more astounding.
Here’s another example by Olivia Ng — similar concept, but with content (click on the nav icon):
CodePen Embed Fallback
Example 2: Expanding Panels
CodePen Embed Fallback
This example transitions the grid container (the column widths) but also the individual columns (their background colors). It’s ideal for providing more content on hover.
It’s worth remembering that the repeat() function sometimes produces buggy transitions, which is why I set the width of each column individually (i.e. grid-template-columns: 1fr 1fr 1fr).
Example 3: Adding Rows and Columns
CodePen Embed Fallback
This example animatedly “adds” a column to the grid. However — you guessed it — this scenario has a pitfall too. The requirement is that the “new” column mustn’t be hidden (i.e. set to display: none), and CSS Grid must acknowledge its existence while setting its width to 0fr.
So, for a three-column grid — grid-template-columns: 1fr 1fr 0fr (yes, the unit must be declared even though the value is 0!) transitions into grid-template-columns: 1fr 1fr 1fr correctly, but grid-template-columns: 1fr 1fr doesn’t. In hindsight, this actually makes perfect sense considering what we know about how transitions work.
Here’s another example by Michelle Barker — same concept, but with an extra column and lot more pizzazz. Make sure to run this one in full-screen mode because it’s actually responsive (no trickery, just good design!).
CodePen Embed Fallback
A few more examples
Because why not?
This “Animated Mondrian” is the original proof of concept for animated CSS grids by Chrome DevRel. The grid-row‘s and grid-column‘s utilize the span keyword to create the layout you see before you, and then the grid-template-row’s and grid-template-column‘s are animated using a CSS animation. It’s nowhere near as complex as it looks!
CodePen Embed Fallback
Same concept, but with more of that Michelle Barker pizzazz. Could make a nice loading spinner?
CodePen Embed Fallback
Wrapping up with a bit of nostalgia (showing my age here), the not-very-griddy animated CSS grid by Andrew Harvard. Again — same concept — it’s just that you can’t see the other grid items. But don’t worry, they’re there.
SvelteKit is the latest of what I’d call next-gen application frameworks. It, of course, scaffolds an application for you, with the file-based routing, deployment, and server-side rendering that Next has done forever. But SvelteKit also supports nested layouts, server mutations that sync up the data on your page, and some other niceties we’ll get into.
This post is meant to be a high-level introduction to hopefully build some excitement for anyone who’s never used SvelteKit. It’ll be a relaxed tour. If you like what you see, the full docs are here.
In some ways this is a challenging post to write. SvelteKit is an application framework. It exists to help you build… well, applications. That makes it hard to demo. It’s not feasible to build an entire application in a blog post. So instead, we’ll use our imaginations a bit. We’ll build the skeleton of an application, have some empty UI placeholders, and hard-coded static data. The goal isn’t to build an actual application, but instead to show you how SvelteKit’s moving pieces work so you can build an application of your own.
To that end, we’ll build the tried and true To-Do application as an example. But don’t worry, this will be much, much more about seeing how SvelteKit works than creating yet another To-Do app.
Spinning up a new SvelteKit project is simple enough. Run npm create svelte@latest your-app-name in the terminal and answer the question prompts. Be sure to pick “Skeleton Project” but otherwise make whatever selections you want for TypeScript, ESLint, etc.
Once the project is created, run npm i and npm run dev and a dev server should start running. Fire up localhost:5173 in the browser and you’ll get the placeholder page for the skeleton app.
Basic routing
Notice the routes folder under src. That holds code for all of our routes. There’s already a +page.svelte file in there with content for the root / route. No matter where in the file hierarchy you are, the actual page for that path always has the name +page.svelte. With that in mind, let’s create pages for /list, /details, /admin/user-settings and admin/paid-status, and also add some text placeholders for each page.
Your file layout should look something like this:
You should be able to navigate around by changing URL paths in the browser address bar.
Layouts
We’ll want navigation links in our app, but we certainly don’t want to copy the markup for them on each page we create. So, let’s create a +layout.svelte file in the root of our routes folder, which SvelteKit will treat as a global template for all pages. Let’s and add some content to it:
Some rudimentary navigation with some basic styles. Of particular importance is the <slot /> tag. This is notthe slot you use with web components and shadow DOM, but rather a Svelte feature indicating where to put our content. When a page renders, the page content will slide in where the slot is.
And now we have some navigation! We won’t win any design competitions, but we’re not trying to.
Nested layouts
What if we wanted all our admin pages to inherit the normal layout we just built but also share some things common to all admin pages (but only admin pages)? No problem, we add another +layout.svelte file in our root admin directory, which will be inherited by everything underneath it. Let’s do that and add this content:
<div>This is an admin page</div>
<slot />
<style>
div {
padding: 15px;
margin: 10px 0;
background-color: red;
color: white;
}
</style>
We add a red banner indicating this is an admin page and then, like before, a <slot /> denoting where we want our page content to go.
Our root layout from before renders. Inside of the root layout is a <slot /> tag. The nested layout’s content goes into the root layout’s <slot />. And finally, the nested layout defines its own <slot />, into which the page content renders.
If you navigate to the admin pages, you should see the new red banner:
Defining our data
OK, let’s render some actual data — or at least, see how we can render some actual data. There’s a hundred ways to create and connect to a database. This post is about SvelteKit though, not managing DynamoDB, so we’ll “load” some static data instead. But, we’ll use all the same machinery to read and update it that you’d use for real data. For a real web app, swap out the functions returning static data with functions connecting and querying to whatever database you happen to use.
Let’s create a dirt-simple module in lib/data/todoData.ts that returns some static data along with artificial delays to simulate real queries. You’ll see this lib folder imported elsewhere via $lib. This is a SvelteKit feature for that particular folder, and you can even add your own aliases.
A function to return a flat array of our to-do items, a lookup of our tags, and a function to fetch a single to-do (we’ll use that last one in our Details page).
Loading our data
How do we get that data into our Svelte pages? There’s a number of ways, but for now, let’s create a +page.server.js file in our list folder, and put this content in it:
import { getTodos, getTags } from "$lib/data/todoData";
export function load() {
const todos = getTodos();
const tags = getTags();
return {
todos,
tags,
};
}
We’ve defined a load() function that pulls in the data needed for the page. Notice that we are notawait-ing calls to our getTodos and getTags async functions. Doing so would create a data loading waterfall as we wait for our to-do items to come in before loading our tags. Instead, we return the raw promises from load, and SvelteKit does the necessary work to await them.
So, how do we access this data from our page component? SvelteKit provides a data prop for our component with data on it. We’ll access our to-do items and tags from it using a reactive assignment.
Before we move on to the Details page and mutate data, let’s take a peek at a really neat SvelteKit feature: layout groups. We’ve already seen nested layouts for all admin pages, but what if we wanted to share a layout between arbitrary pages at the same level of our file system? In particular, what if we wanted to share a layout between only our List page and our Details page? We already have a global layout at that level. Instead, we can create a new directory, but with a name that’s in parenthesis, like this:
We now have a layout group that covers our List and Details pages. I named it (todo-management) but you can name it anything you like. To be clear, this name will not affect the URLs of the pages inside of the layout group. The URLs will remain the same; layout groups allow you to add shared layouts to pages without them all comprising the entirety of a directory in routes.
We could add a +layout.svelte file and some silly <div> banner saying, “Hey we’re managing to-dos”. But let’s do something more interesting. Layouts can define load() functions in order to provide data for all routes underneath them. Let’s use this functionality to load our tags — since we’ll be using our tags in our details page — in addition to the list page we already have.
In reality, forcing a layout group just to provide a single piece of data is almost certainly not worth it; it’s better to duplicate that data in the load() function for each page. But for this post, it’ll provide the excuse we need to see a new SvelteKit feature!
First, let’s go into our list page’s +page.server.js file and remove the tags from it.
import { getTodos, getTags } from "$lib/data/todoData";
export function load() {
const todos = getTodos();
return {
todos,
};
}
Our List page should now produce an error since there is no tags object. Let’s fix this by adding a +layout.server.js file in our layout group, then define a load() function that loads our tags.
import { getTags } from "$lib/data/todoData";
export function load() {
const tags = getTags();
return {
tags,
};
}
And, just like that, our List page is rendering again!
We’re loading data from multiple locations
Let’s put a fine point on what’s happening here:
We defined a load() function for our layout group, which we put in +layout.server.js.
This provides data for all of the pages the layout serves — which in this case means our List and Details pages.
Our List page also defines a load() function that goes in its +page.server.js file.
SvelteKit does the grunt work of taking the results of these data sources, merging them together, and making both available in data.
Our Details page
We’ll use our Details page to edit a to-do item. First, let’s add a column to the table in our List page that links to the Details page with the to-do item’s ID in the query string.
<td><a href="/details?id={t.id}">Edit</a></td>
Now let’s build out our Details page. First, we’ll add a loader to grab the to-do item we’re editing. Create a +page.server.js in /details, with this content:
import { getTodo, updateTodo, wait } from "$lib/data/todoData";
export function load({ url }) {
const id = url.searchParams.get("id");
console.log(id);
const todo = getTodo(id);
return {
todo,
};
}
Our loader comes with a url property from which we can pull query string values. This makes it easy to look up the to-do item we’re editing. Let’s render that to-do, along with functionality to edit it.
SvelteKit has wonderful built-in mutation capabilities, so long as you use forms. Remember forms? Here’s our Details page. I’ve elided the styles for brevity.
We’re grabbing the tags as before from our layout group’s loader and the to-do item from our page’s loader. We’re grabbing the actual tag objects from the to-do’s list of tag IDs and then rendering everything. We create a form with a hidden input for the ID and a real input for the title. We display the tags and then provide a button to submit the form.
If you noticed the use:enhance, that simply tells SvelteKit to use progressive enhancement and Ajax to submit our form. You’ll likely always use that.
How do we save our edits?
Notice the action="?/editTodo" attribute on the form itself? This tells us where we want to submit our edited data. For our case, we want to submit to an editTodo “action.”
Let’s create it by adding the following to the +page.server.js file we already have for Details (which currently has a load() function, to grab our to-do):
Form actions give us a request object, which provides access to our formData, which has a get method for our various form fields. We added that hidden input for the ID value so we could grab it here in order to look up the to-do item we’re editing. We simulate a delay, call a new updateTodo() method, then redirect the user back to the /list page. The updateTodo() method merely updates our static data; in real life you’d run some sort of update in whatever datastore you’re using.
export async function updateTodo(id, newTitle) {
const todo = todos.find(t => t.id == id);
Object.assign(todo, { title: newTitle });
}
Let’s try it out. We’ll go to the List page first:
Now let’s click the Edit button for one of the to-do items to bring up the editing page in /details.
We’re going to add a new title:
Now, click Save. That should get us back to our /list page, with the new to-do title applied.
How did the new title show up like that? It was automatic. Once we redirected to the /list page, SvelteKit automatically re-ran all of our loaders just like it would have done regardless. This is the key advancement that next-gen application frameworks, like SvelteKit, Remix, and Next 13 provide. Rather than giving you a convenient way to render pages then wishing you the best of luck fetching whatever endpoints you might have to update data, they integrate data mutation alongside data loading, allowing the two to work in tandem.
A few things you might be wondering…
This mutation update doesn’t seem too impressive. The loaders will re-run whenever you navigate. What if we hadn’t added a redirect in our form action, but stayed on the current page? SvelteKit would perform the update in the form action, like before, but would still re-run all of the loaders for the current page, including the loaders in the page layout(s).
Can we have more targeted means of invalidating our data? For example, our tags were not edited, so in real life we wouldn’t want to re-query them. Yes, what I showed you is just the default forms behavior in SvelteKit. You can turn the default behavior off by providing a callback to use:enhance. Then SvelteKit provides manual invalidation functions.
Loading data on every navigation is potentially expensive, and unnecessary. Can I cache this data like I do with tools like react-query? Yes, just differently. SvelteKit lets you set (and then respect) the cache-control headers the web already provides. And I’ll be covering cache invalidation mechanisms in a follow-on post.
Everything we’ve done throughout this article uses static data and modifies values in memory. If you need to revert everything and start over, stop and restart the npm run dev Node process.
Wrapping up
We’ve barely scratched the surface of SvelteKit, but hopefully you’ve seen enough to get excited about it. I can’t remember the last time I’ve found web development this much fun. With things like bundling, routing, SSR, and deployment all handled out of the box, I get to spend more time coding than configuring.
Here are a few more resources you can use as next steps learning SvelteKit:
This article is not a definitive guide to :has(). It’s also not here to regurgitate what’s already been said. It’s just me (hi 👋) jumping on the bandwagon for a moment to share some of the ways I’m most likely to use :has() in my day-to-day work… that is, once browser support is good enough. (Firefox is the last holdout, but will support it, which is imminent.)
When that does happen, you can bet I’ll start using :has() all over the place. Here are some real-world examples of things I’ve built recently and thought to myself, “Gee, this’ll be so much nicer once :has() is fully supported.”
Avoid having to reach outside your JavaScript component
Have you ever built an interactive component that sometimes needs to affect styles somewhere else on the page? Take the following example, where <nav> is a mega menu, and opening it changes the colors of the <header> content above it.
I feel like I need to do this kind of thing all the time.
This particular example is a React component I made for a site. I had to “reach outside” the React part of the page with document.querySelector(...) and toggle a class on the <body>, <header>, or another component. That’s not the end of the world, but it sure feels a bit yuck. Even in a fully React site (a Next.js site, say), I’d have to choose between managing a menuIsOpen state way higher up the component tree, or do the same DOM element selection — which isn’t very React-y.
With :has(), the problem goes away:
header:has(.megamenu--open) {
/* style the header differently if it contains
an element with the class ".megamenu--open"
*/
}
No more fiddling with other parts of the DOM in my JavaScript components!
Better table striping UX
Adding alternate row “stripes” to your tables can be a nice UX improvement. They help your eyes keep track of which row you’re on as you scan the table.
But in my experience, this doesn’t work great on tables with just two or three rows. If you have, for example, a table with three rows in the <tbody> and you’re “striping” every “even” row, you could end up with just one stripe. That’s not really worth a pattern and might have users wondering what’s so special about that one highlighted row.
What to get fancier? You could also decide to only do this if the table has at least a certain number of columns, too:
table:has(:is(td, th):nth-child(3)) {
/* only do stuff if there are three or more columns */
}
Remove conditional class logic from templates
I often need to change a page layout depending on what’s on the page. Take the following Grid layout, where the placement of the main content changes grid areas depending on whether there’s a sidebar present.
That’s something that might depend on whether there are sibling pages set in the CMS. I’d normally do this with template logic to conditionally add BEM modifier classes to the layout wrapper to account for both layouts. That CSS might look something like this (responsive rules and other stuff omitted for brevity):
/* m = main content */
/* s = sidebar */
.standard-page--with-sidebar {
grid-template-areas: 's s s m m m m m m m m m';
}
.standard-page--without-sidebar {
grid-template-areas: '. m m m m m m m m m . .';
}
CSS-wise, this is totally fine, of course. But it does make the template code a little messy. Depending on your templating language it can get pretty ugly to conditionally add a bunch of classes, especially if you have to do this with lots of child elements too.
Contrast that with a :has()-based approach:
/* m = main content */
/* s = sidebar */
.standard-page:has(.sidebar) {
grid-template-areas: 's s s m m m m m m m m m';
}
.standard-page:not(:has(.sidebar)) {
grid-template-areas: '. m m m m m m m m m . .';
}
Honestly, that’s not a whole lot better CSS-wise. But removing the conditional modifier classes from the HTML template is a nice win if you ask me.
It’s easy to think of micro design decisions for :has() — like a card when it has an image in it — but I think it’ll be really useful for these macro layout changes too.
Better specificity management
If you read my previous article, you’ll know I’m a stickler for specificity. If, like me, you don’t want your specificity scores blowing out when adding :has() and :not() throughout your styles, be sure to use :where().
That’s because the specificity of :has() is based on the most specific element in its argument list. So, if you have something like an ID in there (why, I’m not sure!), your selector is going to be tough to override in the cascade.
/* specificity score: 0,1,0.
Same as a .standard-page--with-sidebar
modifier class
*/
.standard-page:where(:has(.sidebar)) {
/* etc */
}
The future’s bright
These are just a few things I can’t wait to be able to use in production. The CSS-Tricks Almanac has a bunch of examples, too. What are you looking forward to doing with :has()? What sort of some real-world examples have you run into where :has() would have been the perfect solution?