Generating (and Solving!) Sudokus in CSS

I love to make CSS do stuff it shouldn’t. It’s the type of problem-solving brain training you’d get building a calculator in Minecraft, except you probably won’t get a job working with Minecraft Redstone no matter how good you get at that, whereas CSS skills are worth actual money, and many generalist programmers are scared of CSS, so studying it can be a way to stand out from the pack. Also, when you’ve done the impossible with CSS, all normal CSS tasks seem easy.

I’ve read interesting discussions on the web about whether CSS is a Turing complete language and whether CSS and HTML qualify as programming languages. I haven’t decided, but I can say that in the quest to support common UI patterns in a standard way, some of the newer CSS features blur the line between styling and functionality.

Challenging ourselves to solve logical problems with only CSS and HTML can force us to spend quality time with some of the newish, programing-like features of CSS, such as custom properties and logical functions. It still wasn’t clear how these could be used to build a Sudoku solver using only CSS, but as crazy as the idea sounded, the constraint-based logic of Sudoku seemed like it might be compatible with the declarative nature of CSS, so I wasn’t shocked to find someone else claimed to have built a “CSS3 Sudoku solution solver.” As it turned out, this was more like a sudoku validator in CSS than a solver. It also used a tiny bit of JavaScript to work with textboxes.

After days of valiantly trying to build a full Sudoku solver and generator app in pure CSS, I learned three things.

  1. You can unit test Sass functions and mixins, which is awesome. If you’re heavily using and reusing these Sass features, which is what they are meant for, they become as mission-critical and as scary to change as any other part of your codebase. They deserve tests around them.
  2. Chrome DevTools shows an infinite spinner of death when you throw 50MB of Sass-generated CSS at it.
  3. Maybe it’s impossible to translate something like this Python script into pure CSS. Maybe.

However, we can achieve a Sudoku solver and generator app for 16-square Sudoku which you can play with below, then we’ll break down how its features work. Where is your god now, simple puzzle intended for young children?

The value selector

Since we’re experimenting with CSS, we are contractually obligated to include something visually interesting, though nothing too over-the-top as Sudoku players seem to appreciate a UI that stays out of the way. In my opinion, the way you select numbers on some of the Sudoku apps could be more intuitive, so I decided to apply the radial menu UI pattern, which dates all the way back to days of black and white Macintosh and is still popular in modern video games. Someone actually built a nice pure CSS library for radial menus, but I got a crush on React Planet as I love the way it captures both selecting an item with the circle around it, and how it attractively displays the available actions. I wanted to see if I could build a similar effect with just CSS.

I took some of the dashed circle code from this Pen and then made the numbers out of labels using the old border-radius: 50% trick, then I used absolute positioning to make the numbers “stick” to the correct point on the dashed circle even when the animation makes it change size.

.context {   background: green;   margin-left: auto;   margin-right: auto;   left: 0;   right: 0;   top: -12.5px; }  .context .number.left {   background: orange;   margin-top: auto;   margin-bottom: auto;   top: 0;   bottom: 0;   left: -12.5px; }

The animation fades the number picker in while making its z-index higher so it becomes clickable. We are also animating the top and left margin from 50% to zero so the circle expands from the center to fill the available space.

@keyframes bounce-out {   0% {     z-index: -1;     width: 35%;     height: 35%;     margin-left: 50%;     margin-top: 50%;     opacity: 0;   }   100% {     z-index: 2;     opacity: 1;     width: var(--circle-radius);     height: var(--circle-radius);   } }

Then, to simulate bouncy physics similar to React Planet, I use a cubic-bezier() function on the animation. The website was a big help in making easing functions easy.

.context {   animation: bounce-out cubic-bezier(.68,-0.6,.32, 2.5) .5s forwards;         }

Both the selection of values and the behavior of opening the value selector for the selected square operate using radio button hacks, to remember which values were selected and achieve mutual exclusivity. CSS-Tricks has an excellent article on checkbox and radio button hacks, so I won’t repeat that information here, but I will show how we set CSS variables at the level of the Sudoku CSS grid based on checkboxes, as it’s central to the way this experiment works.

As we are using variables, we can get the same behavior when a value is set, regardless of whether it’s the user checking a box to specify a value, or the puzzle generator setting that same value for the square. There are many combinations of squares and values, so we are using Sass rather than writing all the combinations out by hand. We are also creating separate bit values for each value-square combination, and another custom property to tell us whether the square is unsolved. That’s because CSS gives us limited ability to compare one value to another (it’s possible but can be tricky). We are defining these values in a way that might look a bit odd at first, but will make life easier for us when it comes to validating whether a set of Sudoku square values is solvable or not.

@for $ i from 1 through 16 {   @for $ j from 1 through 4 {     #select-#{$ j}-value-square-#{$ i}:checked ~ .sudoku  {        --square-#{$ i}-unsolved: 0;         --square-#{$ i}-equals-#{$ j}: 1;     }   } }

Validating the grid

Doctor Google tells us that even with only 16 squares, there are four billion possible combinations of four numbers. But a program that brute forces all these combinations and outputs the ones that are valid according to the rules of Sudoku shows that there are only 288 valid solutions in 4×4 Sudoku, which is a big difference from the number of possible valid solutions in a 9×9 grid. With only 288 possible solutions, this is where Sass can really come into its own. I’m still not sure if CSS is a Turing complete language, but Sass is, and it gives us some proper data structures, such as lists. With a bit of regex magic we can transform the list of valid 4×4 puzzles linked above into a Sass-powered two-dimensional list!

$ solutions: ((1,2,3,4,3,4,1,2,2,1,4,3,4,3,2,1),(3,1,2,4,2,4,1,3,1,3,4,2,4,2,3,1),(1,2,3,4,3,4,1,2,2,3,4,1,4,1,2,3),/*...many lines later...*/(2,4,3,1,3,1,4,2,4,2,1,3,1,3,2,4),(4,3,2,1,2,1,4,3,3,4,1,2,1,2,3,4)); 

Sweet! If our CSS hack were a multi-tier application, this would be our database. Validation could have used the same approach of checking row and column values like the 9×9 validator we saw in the introduction, but since we know the answer it seems like we shouldn’t need to bother checking blocks and columns and rows. Instead, we can check whether the entered numbers could still be a valid puzzle or not. In pseudocode this might look something like:

foreach (s in squares)  {   if (solutionsContains(s.value, s.index) or s.isUnsolved())   {     showValidationError();   }  } 

Remember we created those weird variables whenever a square value is selected?

--square-#{$ i}-unsolved: 0;         --square-#{$ i}-equals-#{$ j}: 1;

So now we have answers to both questions in the condition in line 3 of the pseudocode above, but how can we do a logical OR operator in CSS? There’s a great article on CSS-Tricks about using calc() to simulate logic operators in CSS, and I’m not sure I would have even thought of some of the code in my Sudoku solver without it, but some of the formulas explained in that article get a bit unwieldy, especially if you want to do nested ANDs and ORs with more than two operands. For example, we need the CSS equivalent of this pseudocode:

if ((squareOneEqualsOne and squareTwoEqualsTwo /*...*/ and squareSixteenEqualsOne) or (squareOneEqualsOne and squareTwoEqualsThree /*...*/ and squareSixteenEqualsOne))   {     sudokuIsValid();   }  }

Well, that article showing how to do logic using calc() was written in 2019. Nowadays, in addition to calc(), we have the well-supported min() and max() math functions which meet our needs even better. If you Google “CSS min, max and clamp” (the last of which is just convenient sugar for a combination of min() and max()), you’ll find many examples are showing how they can be used to simplify fluid typography. That’s one compelling use case, but you can use these math functions anywhere you’d use a number, which adds a lot of power to CSS. For example, if you pass bit flag variables to CSS min(), that’s equivalent to AND. If you pass the same flags to CSS max(), that’s equivalent to OR. We can prove this using the following truth tables.

A B A AND B min(A, B)
0 0 0 0
1 0 0 0
0 1 0 0
1 1 1 1
A B A OR B max(A, B)
0 0 0 0
1 0 1 1
0 1 1 1
1 1 1 1

We can get pretty sophisticated with that, especially when you add the helpful fact that we are allowed to do anything calc() can do within min() and max(). CSS just took a step closer to being its own weird scripting language. Now we can implement the condition in our validation pseudocode above in CSS. (In practice, we generate this from Sass since it’s very repetitive.)

.sudoku {    --square-1-matches-puzzle-1: max(var(--square-1-unsolved), var(--square-1-equals-1, 0));   --square-2-matches-puzzle-1: max(var(--square-2-unsolved), var(--square-2-equals-2, 0));   /*...*/   --square-16-matches-puzzle-1: max(var(--square-16-unsolved), var(--square-16-equals-1, 0));   --puzzle-1-found: min(var(--square-1-matches-puzzle-1),    /*...*/    var(--square-16-matches-puzzle-1));   --solution-found: max(var(--puzzle-1-found), /*...*/ var(--puzzle-288-found)); }

By checking if each square is either unsolved or has a value that exists in the same position in one of our pre-calculated solutions from the Sass 2D list, we can produce a variable that tells us whether the currently defined squares exist in a valid 4×4 sudoku puzzle. Now as long as we can find something numeric that will drive a behavior in CSS, we can base that CSS behavior on --solution-found. For example, to make our grid turn red when it’s invalid we can put this in each square:

.square {   color: rgb(calc(255 * (1 - var(--solution-found))), 0, 0); }

Not every CSS property can be driven by a number, but many can, and both z-index and opacity are especially versatile CSS properties for this usage. Other behaviors can be trickier but often achievable. For example, I was a bit stuck thinking about how to trigger the shake animation for an invalid grid with just a numeric bit flag property so that the grid would shake any time it became invalid, but this is a great example of how hacking CSS forces you to read the specs and understand the edge cases for each property. I found my solution on this page about animation-duration.

A value of 0s, which is the default value, indicates that no animation should occur.

So we can base animation duration of the shake animation on --solution-found and remove the animation each time a number is clicked using the :active pseudo-class to make the animation replay any time the solution becomes invalid, and do nothing otherwise.

#select-#{$ j}-value-square-#{$ i}:active  {    animation: none; }   #select-#{$ j}-value-square-#{$ i}:checked ~ .sudoku {   animation: shake cubic-bezier(.36,.07,.19,.97) calc((clamp(0, 1 - var(--solution-found), 1)) * 1s) forwards; }

A pure CSS Sudoku app would probably be impossible if we didn’t have CSS custom properties, and they are more powerful than they may seem at first glance. The way they get reevaluated and update the UI whenever a property they depend on changes is like a simpler version of the reactivity you get from a fancy JavaScript framework like Vue. It’s fair to say reactivity is built right into the browser in the form of CSS variables!

Now that we have this approach for validation and our stylesheet knows the solution in its subconscious any time we set valid values in our Sudoku, we are close to implementing the solver!

Solving every 4×4 Sudoku

Remember when we introduced these intermediate variables?

.sudoku {    --puzzle-1-found: min(var(--square-1-matches-puzzle-1),    /*...*/    var(--square-16-matches-puzzle-1)); }

That wasn’t only to make the validation code easier to write and understand. Knowing which of the 288 possible puzzle(s) are matched allows us to write the solver!

#no-solution {   z-index: 1;   color: red; } @for $ solution-index from 1 through 288 { label[for=solution-#{$ solution-index}] {   cursor: pointer;   z-index: calc(var(--puzzle-#{$ solution-index}-found) * #{$ solution-index}); }  #solution-#{$ solution-index}:checked ~ .sudoku  {   @for $ square from 1 through 16 {     --square-#{$ square}-solution:"#{nth(nth($ solutions, $ solution-index), $ square)}";     --square-#{$ square}-color: grey;     --auto-#{$ square}: 1;   }

I put the optional plural in the word “puzzle(s)” above because, if the user hasn’t filled out many squares, it’s possible there are multiple valid solutions. I dig solvers like this JavaScript one that can quickly produce a solution even if you haven’t specified enough values for a human to be able to solve it without guessing.

The trick in my CSS solver is that while the “solve” button looks like a single button, it’s actually 288 radio button labels stacked one on top of the other — but all of them look the same. Imagine a stack of cards: they all have the same design on the back, but different values on the front. The solver logic is putting the card with the solution on the top of the pile with z-index, so if you pick it up and read the other side of it, you will always have the correct solution. It still works if there are multiple correct solutions, because the solution that comes later in our list of valid answers will be placed on top, since we calculate the z-index by multiplying the flag by $ solution-index. If no solutions are matched, the z-index of all the solve buttons will be zero and, since the disabled version of the button with the “invalid puzzle” message has z-index of one, it will appear on top. If puzzle number one is the solution, we will still see the puzzle one button, since the invalid button comes earlier in the HTML.

Stacking context can behave unexpectedly if you haven’t read up on it, so this is is a nice illustration of one of the non-obvious stacking behaviors.

Generating puzzles

We can think of the generating puzzles as another version of the solver with extra requirements.

  1. Some random squares need to be left unsolved when the puzzle generator button is pressed.
  2. The combination of randomly unsolved squares and a correct solution should be different each time the generator button is pressed.
  3. Pressing the solve button should reveal the complete solution.
  4. If the user manually solves the generated puzzle, we’d like to reward them with a victory screen that gives feedback about how fast they solved it.

CSS has no random() function (though Sass does), so it might not be obvious how we can get a different behavior each time we push the same button. But the solver explanation above was a bit of a spoiler as it already does something similar with a button that looks like a single element but is actually different depending on the current valid solution.

The question with the “generate” button is how we can get an unpredictable result each time we click. Full credit to Alvaro Montoro for his article on CSS-Tricks about how to generate seemingly random values with just CSS. The combination of radio button hacks and animating the stacking order seems to work nicely. I tried hard to see if I could do it without extra markup, but I concluded that this approach is the best and the simplest. To reuse the deck of cards analogy from the solver explanation, it’s like the deck of puzzle cards is invisibly shuffling all the time so that whenever you take a card, you discover it has a different face.

We can combine this pseudo randomness with the actual randomness offered by the Sass random() function to give our Sudoku game replay value.

@for $ j from 0 through 287 {   label[for=generate#{$ j}] {      animation-delay: #{$ j * .35s};    }   label[for=generate#{$ j}]:active:after {     z-index: 300;     width: 100%;   }   #generate#{$ j}:checked ~ .sudoku {      $ blockCounts: (1: 2, 2: 2, 3: 3, 4: 2);     $ shuffleSquares: (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);       @for $ square from 1 through 16 {        $ index1: random(16);        $ index2: random(16);         $ temp: nth($ shuffleSquares, $ index1);        $ shuffleSquares: set-nth($ shuffleSquares, $ index1, nth($ shuffleSquares, $ index2));        $ shuffleSquares: set-nth($ shuffleSquares, $ index2, $ temp);      }       @each $ square in $ shuffleSquares {        $ row: ceil($ square/4);       $ column: 1 + ($ square - 1) % 4;         $ block: if($ row &lt; 3, 1, 3) + if($ column < 3, 0, 1);        $ count: map-get($ blockCounts, $ block);        $ val: nth(nth($ solutions, $ j + 1), $ square);         --square-#{$ square}-solution-#{$ val}: 1;         @if ($ count > 0) {          $ blockCounts: map-merge($ blockCounts, ($ block: $ count - 1));         --square-#{$ square}-unsolved: 0;          --square-#{$ square}-equals-#{$ val}: 1;          @for $ other-value from 1 through 4 {           @if ($ other-value != $ val) {             --square-#{$ square}-equals-#{$ other-value}: 0;              }         }          --square-#{$ square}-color: grey;         --auto-#{$ square}: 1;       }     }    } }

For each “block” (that’s Sudoku-speak for those 4×4 sections of the Sudoku grid with the thick border around them), we use Sass to randomly choose two out of four squares to solve, except for one “gimme” square which only has one unsolved square. Since the validation logic and solver logic uses the variables rather than being directly based on which values were checked using the value selector, the validation and solving logic behaves the same way. That means generated values are treated the same as if the user had individually selected each value.

The solving timer

Here’s the timer ticking through the first eleven seconds.

We’ll dive into the CSS for the solving timer in a moment, but let’s first show what one of the digits looks like without CSS overflow set to hidden, and with a green border around the element to show the part that would be visible to the user at each step of the animation.

We are using an infinitely repeating keyframes animation to shift the list of possible digits one character to the left at a desired interval (we use a monospaced font so that we can be sure each character will occupy the same exact width). The seconds digit will go from zero up to nine, and the next digit should only go up to five, increasing once per ten seconds before both digits of the seconds need to reset to zero.

Each digit is animating using the same technique you can use to animate a spritesheet in CSS, except instead of animatedly shifting an image background to achieve an animation effect, we are shifting a pseudo element containing the possible digits.

As with many tasks in CSS, there is more than one way to make an animated counter in CSS. But some don’t work cross-browser and really demand a preprocessor to keep the code succinct. I like my approach because it’s fairly short. CSS does the heavy lifting to figure out when and how to move to the next digit. All the markup needs to do is create a placeholder where each digit goes, giving us some freedom for how we present our timer.

Here’s the markup:

<div class="stopwatch">     <div class="symbol"></div>   <div class="symbol">:</div>   <div class="symbol"></div>   <div class="symbol"></div> </div>

…and the CSS:

.stopwatch {   text-align: center;   font-family: monospace;   margin-bottom: 10px; }  .symbol {   width: 1ch;   overflow: hidden;   display: inline-flex;   font-size: 5ch; }  .symbol:nth-child(1)::after {   animation: tens 3600s steps(6, end) infinite;   content: '012345'; }  .symbol:nth-child(2)::after {   animation: units 600s steps(10, end) infinite;   content: '0123456789'; }  .symbol:nth-child(4)::after {   animation: tens 60s steps(6, end) infinite;   content: '012345'; }  .symbol:nth-child(5)::after {   animation: units 10s steps(10, end) infinite;   content: '0123456789'; }  @keyframes units {   to {     transform: translateX(-10ch);   } }  @keyframes tens {   to {     transform: translateX(-6ch);   } }

You might notice that the counter start again from the beginning after an hour. That’s because all of the iteration counts are set to infinite . We could fix it, but I figure if someone spends an hour solving one of these, they have bigger problems than a children’s Sudoku puzzle. 😛

What would be unfair, though, is if we allowed the same timer to just keep ticking even when the user generates a fresh puzzle. Can we make it reset? It turns out we’ve solved that problem already in the first step in this article, where we removed, and conditionally re-added, the animation for our number selector using the :active pseudo-class. This time, it’s actually simpler because every time we hit the “generate” button, we want to remove the animation on all the digits to take them back to zero. Then the animation will start again when the radio button is no longer active. So, it’s only one line of CSS we need to make the timer reset each time we generate!

input[name=generate]:active ~ .stopwatch .symbol::after {   animation: none; }


Even when the puzzle is solved, I want to offer replay value by giving the player visual feedback on their time performance and challenging them to solve puzzles faster. I also want to reward you for making it this far through the article by giving you a minimalist circular gauge you could reuse in your own projects. Here’s a standalone Pen with the circular gauge for you to experiment:

We’re applying the same principles used in the win screen from the game except, in this Pen, the rating displayed is controlled with radio button hacks, whereas in the game it’s controlled by animation that slowly moves to a lower rating as time passes. The gauge in the game is hidden using zero opacity and is only displayed (and paused) when we detect that the puzzle has been manually solved.

Let’s explain how we create the illusion of a semi-circle that’s divided into two sides by color. It’s actually a full CSS circle with its bottom half hidden using overflow: hidden.

We apply the two colors using a pseudo-element that fills half of the <div>.

Then we cut a hole in the middle to make a donut, using another circle filled with the game’s background color, and center that inside the larger circle using flexbox.

Next, hide half of it by making the size of the container half as tall as the full circle and, again, using overflow: hidden.

Now, if we rotate our donut, it looks like the gauge is filling up with green or losing green, depending on whether we rotate our donut by negative or positive degrees!

We’d like to put labels on both ends of the gauge and a description in between them, and it turns out flexbox is an elegant solution:

#rating {   font-size: 30px;   display: flex;   width: 300px;   justify-content: space-between; }

Here’s the markup:

<div id="rating">   <div id="turtle">🐢</div>   <div id="feedback"></div>   <div id="rabbit">🐇</div> </div>

That’s all we need to position our labels. If the rating <div> is the width of the diameter of our circle, flexbox will position the emoji labels at the ends and the description in the center of the circle!

As for controlling what the description says, it’s similar to the trick we used for our timer, except this time we do it vertically rather than horizontally since the feedback descriptions are of variable length. But they are always the same height.


I opened this article with questions about whether CSS is a programming language. It’s hard to argue the logic we were able to implement using just CSS wasn’t programming, but some of it is unusual usage of CSS to say the least. As with many things in the tech world, the answer seems to be “it depends,” and as much as we learned about CSS through this experiment, we’ve also illustrated that programming is as much about the mindset as the tech.

No CSS hacking article is complete without the disclaimer that although we’ve shown we can implement sophisticated logic in CSS and learn a lot in the process, most of the time we probably shouldn’t do this in production, because of maintainability, accessibility, and some other words ending with “ility.”

But we also saw that some things — such as what I think of as the built-in reactivity you get with CSS variables — are quite convenient in CSS but might require us to go JavaScript hoops and probably use a framework. By pushing the limits of CSS we ended up creating a circular gauge that I believe could reasonably be used in a production app, and might even be the right thing compared to reaching for some JavaScript widget that might be heavy and do more than we really need.

On my wishlist for the CSS Sudoku app is a reset button. Currently, if you want to start a new Sudoku game, you have to refresh the page. That’s an inherent limitation of radio button hacks which makes CSS hacking different to conventional programming. At one stage, I believed I found a solution when I thought animations could be used to set CSS variables — but it turns out that is part of CSS Houdini and only supported in Chromium-based browsers. If and when that’s supported everywhere, it will open a Pandora’s box of hacks and be a lot of fun. In a future article, I may even explore why this innocuous feature we have in Chrome is a game changer for CSS hacking.

The jury is still out on whether a full 81-square Sudoku solver is possible in CSS, but if you’re curious to find out, leave your feedback in the comments. If enough people want it, we may go down that rabbit hole together and see what dark corners of CSS we can illuminate in the process.

The post Generating (and Solving!) Sudokus in CSS appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, ,

Building a Settings Component

This is a tremendous CSS-focused tutorial from Adam Argyle. I really like the “just for gap” concept here. Grid is extremely powerful, but you don’t have to use all its abilities every time you reach for it. Here, Adam reaches for it for very light reasons like using it as an in-between border alternative as well as more generic spacing. I guess he’s putting money where his mouth is in terms of gap superseding margin!

I also really like calling out Una Kravet’s awesome name for flexible grids: RAM. Perhaps you’ve seen the flexible-number-of-columns trick with CSS grid? The bonus trick here (which I first saw from Evan Minto) is to use min(). That way, not only are large layouts covered, but even the very smallest layouts have no hard-coded minimum (like if 100% is smaller than 10ch here):

.el {   display: grid;   grid-template-columns: repeat(auto-fit, minmax(min(10ch, 100%), 35ch)); }

There is a ton more trickery in the blog post. The “color pops” with :focus-within is fun and clever. So much practical CSS in building something so practical! 🧡 more blog posts like this, please. Fortunately, we don’t have to wait, as Adam has other component-focused posts like this one on Tabs and this one on Sidenav.

Direct Link to ArticlePermalink

The post Building a Settings Component appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, ,

Going “Meta GSAP”: The Quest for “Perfect” Infinite Scrolling

I‘m not sure how this one came about. But, it‘s a story. This article is more about grokking a concept, one that’s going to help you think about your animations in a different way. It so happens that this particular example features infinite scrolling — specifically the “perfect” infinite scroll for a deck of cards without duplicating any of them.

Why am I here? Well, this all started from a tweet. A tweet that got me thinking about layouts and side-scrolling content.

I took that concept and used it on my site. And it’s still there in action at the time of writing.

Then I got to thinking more about gallery views and side-scrolling concepts. We hopped on a livestream and decided to try and make something like the old Apple “Cover Flow” pattern. Remember it?

My first thoughts for making this assumed I‘d make this so it works without JavaScript, as it does in the demo above, in a way that uses “progressive enhancement.” I grabbed Greensock and ScrollTrigger, and off we went. I came away from that work pretty disappointed. I had something but couldn‘t quite get infinite scrolling to work how the way I wanted. The “Next” and “Previous” buttons didn’t want to play ball. You can see it here, and it requires horizontal scrolling.

So I opened up a new thread on the Greensock forum. Little did I know I was about to open myself up to some serious learning! We solved the issue with the buttons. But, being me, I had to ask whether something else was possible. Was there a “clean” way to do infinite scrolling? I‘d tried something on stream but had no luck. I was curious. I’d tried a technique like that used in this pen which I created for the ScrollTrigger release.

The initial answer was that it is kinda tricky to do:

The hard part about infinite things on scroll is that the scroll bar is limited while the effect that you’re wanting is not. So you have to either loop the scroll position like this demo (found in the ScrollTrigger demos section) or hook directly into the scroll-related navigation events (like the wheel event) instead of actually using the actual scroll position.

I figured that was the case and was happy to leave it “as-is.” A couple of days passed and Jack dropped a reply that kinda blew my mind when I started digging into it. And now, after a bunch of going through it, I’m here to share the technique with you.

Animate anything

One thing that is often overlooked with GSAP, is that you can animate almost anything with it. This is often because visual things are what spring to mind when thinking about animation — the actual physical movement of something. Our first thought isn’t about taking that process to a meta-level and animating from a step back.

But, think about animation work on a larger scale and then break it down into layers. For example, you play a cartoon. The cartoon is a collection of compositions. Each composition is a scene. And then you have the power to scrub through that collection of compositions with a remote, whether it’s on YouTube, using your TV remote, or whatever. There are almost three levels to what is happening.

And this is the trick we need for creating different types of infinite loops. This is the main concept right here. We animate the play head position of a timeline with a timeline. And then we can scrub that timeline with our scroll position.

Don‘t worry if that sounds confusing. We’re going to break it down.

Going “meta”

Let‘s start with an example. We’re going to create a tween that moves some boxes from left to right. Here it is.

Ten boxes that keep going left to right. That’s quite straightforward with Greensock. Here, we use fromTo and repeat to keep the animation going. But, we have a gap at the start of each iteration. We’re also using stagger to space out the movement and that’s something that will play an important role as we continue.

gsap.fromTo('.box', {   xPercent: 100 }, {   xPercent: -200,   stagger: 0.5,   duration: 1,   repeat: -1,   ease: 'none', })

Now comes the fun part. Let’s pause the tween and assign it to a variable. Then let’s create a tween that plays it. We can do this by tweening the totalTime of the tween, which allows us to get or set the tween’s playhead tween, while considering repeats and repeat delays.

const SHIFT = gsap.fromTo('.box', {   xPercent: 100 }, {   paused: true,   xPercent: -200,   stagger: 0.5,   duration: 1,   repeat: -1,   ease: 'none', })  const DURATION = SHIFT.duration(), {   totalTime: DURATION,   repeat: -1,   duration: DURATION,   ease: 'none', })

This is our first “meta” tween. It looks exactly the same but we’re adding another level of control. We can change things on this layer without affecting the original layer. For example, we could change the tween ease to This completely changes the animation but without affecting the underlying animation. We’re kinda safeguarding ourselves with a fallback.

Not only that, we might choose to repeat only a certain part of the timeline. We could do that with another fromTo, like this:

The code for that would be something like this.

gsap.fromTo(SHIFT, {   totalTime: 2, }, {   totalTime: DURATION - 1,   repeat: -1,   duration: DURATION,   ease: 'none' })

Do you see where this is going? Watch that tween. Although it keeps looping, the numbers flip on each repeat. But, the boxes are in the correct position.

Achieving the “perfect” loop

If we go back to our original example, there’s a noticeable gap between each repetition.

Here comes the trick. The part that unlocks everything. We need to build a perfect loop.

Let‘s start by repeating the shift three times. It’s equal to using repeat: 3. Notice how we’ve removed repeat: -1 from the tween.

const getShift = () => gsap.fromTo('.box', {   xPercent: 100 }, {   xPercent: -200,   stagger: 0.5,   duration: 1,   ease: 'none', })  const LOOP = gsap.timeline()   .add(getShift())   .add(getShift())   .add(getShift())

We’ve turned the initial tween into a function that returns the tween and we add it to a new timeline three times. And this gives us the following.

OK. But, there’s still a gap. Now we can bring in the position parameter for adding and positioning those tweens. We want it to be seamless. That means inserting each each set of tweens before the previous one ends. That’s a value based on the stagger and the amount of elements.

const stagger = 0.5 // Used in our shifting tween const BOXES = gsap.utils.toArray('.box') const LOOP = gsap.timeline({   repeat: -1 })   .add(getShift(), 0)   .add(getShift(), BOXES.length * stagger)   .add(getShift(), BOXES.length * stagger * 2)

If we update our timeline to repeat and watch it (while adjusting the stagger to see how it affects things)…

You‘ll notice that there‘s a window in the middle there that creates a “seamless” loop. Recall those skills from earlier where we manipulated time? That’s what we need to do here: loop the window of time where the loop is “seamless.”

We could try tweening the totalTime through that window of the loop.

const LOOP = gsap.timeline({   paused: true,   repeat: -1, }) .add(getShift(), 0) .add(getShift(), BOXES.length * stagger) .add(getShift(), BOXES.length * stagger * 2)  gsap.fromTo(LOOP, {   totalTime: 4.75, }, {   totalTime: '+=5',   duration: 10,   ease: 'none',   repeat: -1, }) 

Here, we’re saying tween the totalTime from 4.75 and add the length of a cycle to that. The length of a cycle is 5. And that’s the middle window of the timeline. We can use GSAP’s nifty += to do that, which gives us this:

Take a moment to digest what‘s happening there. This could be the trickiest part to wrap your head around. We’re calculating windows of time in our timeline. It’s kinda hard to visualize but I’ve had a go.

This is a demo of a watch that takes 12 seconds for the hands go round once. It‘s looped infinitely with repeat: -1 and then we‘re using fromTo to animate a specific time window with a given duration. If you, reduce the time window to say 2 and 6, then change the duration to 1, the hands will go from 2 o‘clock to 6 o’clock on repeat. But, we never changed the underlying animation.

Try configuring the values to see how it affects things.

At this point, it’s a good idea to put together a formula for our window position. We could also use a variable for the duration it takes for each box to transition.


Instead of using three stacked timelines, we could loop over our elements three times where we get the benefit of not needing to calculate the positions. Visualizing this as three stacked timelines is a neat way to grok the concept, though, and a nice way to help understand the main idea.

Let’s change our implementation to create one big timeline from the start.

const STAGGER = 0.5 const BOXES = gsap.utils.toArray('.box')  const LOOP = gsap.timeline({   paused: true,   repeat: -1, })  const SHIFTS = [...BOXES, ...BOXES, ...BOXES]  SHIFTS.forEach((BOX, index) => {   LOOP.fromTo(BOX, {     xPercent: 100   }, {     xPercent: -200,     duration: 1,     ease: 'none',   }, index * STAGGER) })

This is easier to put together and gives us the same window. But, we don’t need to think about math. Now we loop through three sets of the boxes and position each animation according to the stagger.

How might that look if we adjust the stagger? It will squish the boxes closer together.

But, it’s broken the window because now the totalTime is out. We need to recalculate the window. Now’s a good time to plug in the formula we calculated earlier.

const DURATION = 1 const CYCLE_DURATION = STAGGER * BOXES.length const START_TIME = CYCLE_DURATION + (DURATION * 0.5) const END_TIME = START_TIME + CYCLE_DURATION  gsap.fromTo(LOOP, {   totalTime: START_TIME, }, {   totalTime: END_TIME,   duration: 10,   ease: 'none',   repeat: -1, })


We could even introduce an “offset” if we wanted to change the starting position.


Now our window starts from a different position.

But still, this isn’t great as it gives us these awkward stacks at each end. To get rid of that effect, we need to think about a “physical” window for our boxes. Or think about how they enter and exit the scene.

We’re going to use document.body as the window for our example. Let’s update the box tweens to be individual timelines where the boxes scale up on enter and down on exit. We can use yoyo and repeat: 1 to achieve entering and exiting.

SHIFTS.forEach((BOX, index) => {   const BOX_TL = gsap     .timeline()     .fromTo(       BOX,       {         xPercent: 100,       },       {         xPercent: -200,         duration: 1,         ease: 'none',       }, 0     )     .fromTo(       BOX,       {         scale: 0,       },       {         scale: 1,         repeat: 1,         yoyo: true,         ease: 'none',         duration: 0.5,       },       0     )   LOOP.add(BOX_TL, index * STAGGER) })

Why are we using a timeline duration of 1? It makes things easier to follow. We know the time is 0.5 when the box is at the midpoint. It‘s worth noting that easing won’t have the effect we usually think of here. In fact, easing will actually play a part in how the boxes position themselves. For example, an ease-in would bunch the boxes up on the right before they move across.

The code above gives us this.

Almost. But, our boxes disappear for a time in the middle. To fix this, let’s introduce the immediateRender property. It acts like animation-fill-mode: none in CSS. We’re telling GSAP that we don’t want to retain or pre-record any styles that are being set on a box.

SHIFTS.forEach((BOX, index) => {   const BOX_TL = gsap     .timeline()     .fromTo(       BOX,       {         xPercent: 100,       },       {         xPercent: -200,         duration: 1,         ease: 'none',         immediateRender: false,       }, 0     )     .fromTo(       BOX,       {         scale: 0,       },       {         scale: 1,         repeat: 1,         zIndex: BOXES.length + 1,         yoyo: true,         ease: 'none',         duration: 0.5,         immediateRender: false,       },       0     )   LOOP.add(BOX_TL, index * STAGGER) })

That small change fixes things for us! Note how we’ve also included z-index: BOXES.length. That should safeguard us against any z-index issues.

There we have it! Our first infinite seamless loop. No duplicate elements and perfect continuation. We’re bending time! Pat yourself on the back if you’ve gotten this far! 🎉

If we want to see more boxes at once, we can tinker with the timing, stagger, and ease. Here, we have a STAGGER of 0.2 and we’ve also introduced opacity into the mix.

The key part here is that we can make use of repeatDelay so that the opacity transition is quicker than the scale. Fade in over 0.25 seconds. Wait 0.5 seconds. Fade back out over 0.25 seconds.

.fromTo(   BOX, {     opacity: 0,   }, {     opacity: 1,     duration: 0.25,     repeat: 1,     repeatDelay: 0.5,     immediateRender: false,     ease: 'none',     yoyo: true,   }, 0)

Cool! We could do whatever we want with those in and out transitions. The main thing here is that we have our window of time that gives us the infinite loop.

Hooking this up to scroll

Now that we have a seamless loop, let’s attach it to scroll. For this we can use GSAP’s ScrollTrigger. This requires an extra tween to scrub our looping window. Note how we’ve set the loop to be paused now, too.

const LOOP_HEAD = gsap.fromTo(LOOP, {   totalTime: START_TIME, }, {   totalTime: END_TIME,   duration: 10,   ease: 'none',   repeat: -1,   paused: true, })  const SCRUB =, {   totalTime: 0,   paused: true,   duration: 1,   ease: 'none', })

The trick here is to use ScrollTrigger to scrub the play head of the loop by updating the totalTime of SCRUB. There are various ways we could set up this scroll. We could have it horizontal or bound to a container. But, what we‘re going to do is wrap our boxes in a .boxes element and pin that to the viewport. (This fixes its position in the viewport.) We’ll also stick with vertical scrolling. Check the demo to see the styling for .boxes which sets things to the size of the viewport.

import ScrollTrigger from '' gsap.registerPlugin(ScrollTrigger)  ScrollTrigger.create({   start: 0,   end: '+=2000',   horizontal: false,   pin: '.boxes',   onUpdate: self => {     SCRUB.vars.totalTime = LOOP_HEAD.duration() * self.progress     SCRUB.invalidate().restart()   } })

The important part is inside onUpdate. That’s where we set the totalTime of the tween based on the scroll progress. The invalidate call flushes any internally recorded positions for the scrub. The restart then sets the position to the new totalTime we set.

Try it out! We can go back and forth in the timeline and update the position.

How cool is that? We can scroll to scrub a timeline that scrubs a timeline that is a window of a timeline. Digest that for a second because that‘s what’s happening here.

Time travel for infinite scrolling

Up to now, we‘ve been manipulating time. Now we’re going to time travel!

To do this, we‘re going to use some other GSAP utilities and we‘re no longer going to scrub the totalTime of LOOP_HEAD. Instead, we’re going to update it via proxy. This is another great example of going “meta” GSAP.

Let’s start with a proxy object that marks the playhead position.

const PLAYHEAD = { position: 0 }

Now we can update our SCRUB to update the position. At the same time, we can use GSAP’s wrap utility, which wraps the position value around the LOOP_HEAD duration. For example, if the duration is 10 and we provide the value 11, we will get back 1.

const POSITION_WRAP = gsap.utils.wrap(0, LOOP_HEAD.duration())  const SCRUB =, {   position: 0,   onUpdate: () => {     LOOP_HEAD.totalTime(POSITION_WRAP(PLAYHEAD.position))   },   paused: true,   duration: 1,   ease: 'none', })

Last, but not least, we need to revise ScrollTrigger so it updates the correct variable on the SCRUB. That’s position, instead of totalTime.

ScrollTrigger.create({   start: 0,   end: '+=2000',   horizontal: false,   pin: '.boxes',   onUpdate: self => {     SCRUB.vars.position = LOOP_HEAD.duration() * self.progress     SCRUB.invalidate().restart()   } })

At this point we’ve switched to a proxy and we won’t see any changes.

We want an infinite loop when we scroll. Our first thought might be to scroll to the start when we complete scroll progress. And it would do exactly that, scroll back. Although that‘s what we want to do, we don’t want the playhead to scrub backwards. This is where totalTime comes in. Remember? It gets or sets the position of the playhead according to the totalDuration which includes any repeats and repeat delays.

For example, say the duration of the loop head was 5 and we got there, we won‘t scrub back to 0. Instead, we will keep scrubbing the loop head to 10. If we keep going, it‘ll go to 15, and so on. Meanwhile, we‘ll keep track of an iteration variable because that tells us where we are in the scrub. We’ll also make sure that we only update iteration when we hit the progress thresholds.

Let’s start with an iteration variable:

let iteration = 0

Now let’s update our ScrollTrigger implementation:

const TRIGGER = ScrollTrigger.create({   start: 0,   end: '+=2000',   horizontal: false,   pin: '.boxes',   onUpdate: self => {     const SCROLL = self.scroll()     if (SCROLL > self.end - 1) {       // Go forwards in time       WRAP(1, 1)     } else if (SCROLL < 1 && self.direction <; 0) {       // Go backwards in time       WRAP(-1, self.end - 1)     } else {       SCRUB.vars.position = (iteration + self.progress) * LOOP_HEAD.duration()       SCRUB.invalidate().restart()      }   } }) 

Notice how we‘re now factoring iteration into the position calculation. Remember that this gets wrapped with the scrubber. We‘re also detecting when we hit the limits of our scroll, and that’s the point where we WRAP. This function sets the appropriate iteration value and sets the new scroll position.

const WRAP = (iterationDelta, scrollTo) => {   iteration += iterationDelta   TRIGGER.scroll(scrollTo)   TRIGGER.update() }

We have infinite scrolling! If you have one of those fancy mice with the scroll wheel that you can let loose on, give it a go! It’s fun!

Here’s a demo that displays the current iteration and progress:

Scroll snapping

We‘re there. But, there are always ”nice to haves” when working on a feature like this. Let’s start with scroll snapping. GSAP makes this easy, as we can use gsap.utils.snap without any other dependencies. That handles snapping to a time when we provide the points. We declare the step between 0 and 1 and we have 10 boxes in our example. That means a snap of 0.1 would work for us.

const SNAP = gsap.utils.snap(1 / BOXES.length)

And that returns a function we can use to snap our position value.

We only want to snap once the scroll has ended. For that, we can use an event listener on ScrollTrigger. When the scroll ends, we are going to scroll to a certain position.

ScrollTrigger.addEventListener('scrollEnd', () => {   scrollToPosition(SCRUB.vars.position) })

And here’s scrollToPosition:

const scrollToPosition = position => {   const SNAP_POS = SNAP(position)   const PROGRESS =     (SNAP_POS - LOOP_HEAD.duration() * iteration) / LOOP_HEAD.duration()   const SCROLL = progressToScroll(PROGRESS)   TRIGGER.scroll(SCROLL) }

What are we doing here?

  1. Calculating the point in time to snap to
  2. Calculating the current progress. Let’s say the LOOP_HEAD.duration() is 1 and we’ve snapped to 2.5. That gives us a progress of 0.5 resulting in an iteration of 2, where 2.5 - 1 * 2 / 1 === 0.5 . We calculate the progress so that it’s always between 1 and 0.
  3. Calculating the scroll destination. This is a fraction of the distance our ScrollTrigger can cover. In our example, we’ve set a distance of 2000 and we want a fraction of that. We create a new function progressToScroll to calculate it.
const progressToScroll = progress =>   gsap.utils.clamp(1, TRIGGER.end - 1, gsap.utils.wrap(0, 1, progress) * TRIGGER.end)

This function takes the progress value and maps it to the largest scroll distance. But we use a clamp to make sure the value can never be 0 or 2000. This is important. We are safeguarding against snapping to these values as it would put us in an infinite loop.

There is a bit to take in there. Check out this demo that shows the updated values on each snap.

Why are things a lot snappier? The scrubbing duration and ease have been altered. A smaller duration and punchier ease give us the snap.

const SCRUB =, {   position: 0,   onUpdate: () => {     LOOP_HEAD.totalTime(POSITION_WRAP(PLAYHEAD.position))   },   paused: true,   duration: 0.25,   ease: 'power3', })

But, if you played with that demo, you‘ll notice there‘s an issue. Sometimes when we wrap around inside the snap, the playhead jumps about. We need to account for that by making sure we wrap when we snap — but, only when it’s necessary.

const scrollToPosition = position => {   const SNAP_POS = SNAP(position)   const PROGRESS =     (SNAP_POS - LOOP_HEAD.duration() * iteration) / LOOP_HEAD.duration()   const SCROLL = progressToScroll(PROGRESS)   if (PROGRESS >= 1 || PROGRESS < 0) return WRAP(Math.floor(PROGRESS), SCROLL)   TRIGGER.scroll(SCROLL) }

And now we have infinite scrolling with snapping!

What next?

We’ve completed the groundwork for a solid infinite scroller. We can leverage that to add things, like controls or keyboard functionality. For example, this could be a way to hook up “Next” and “Previous” buttons and keyboard controls. All we have to do is manipulate time, right?

const NEXT = () => scrollToPosition(SCRUB.vars.position - (1 / BOXES.length)) const PREV = () => scrollToPosition(SCRUB.vars.position + (1 / BOXES.length))  // Left and Right arrow plus A and D document.addEventListener('keydown', event => {   if (event.keyCode === 37 || event.keyCode === 65) NEXT()   if (event.keyCode === 39 || event.keyCode === 68) PREV() })  document.querySelector('.next').addEventListener('click', NEXT) document.querySelector('.prev').addEventListener('click', PREV)

That could give us something like this.

We can leverage our scrollToPosition function and bump the value as we need.

That’s it!

See that? GSAP can animate more than elements! Here, we bent and manipulated time to create an almost perfect infinite slider. No duplicate elements, no mess, and good flexibility.

Let’s recap what we covered:

  • We can animate an animation. 🤯
  • We can think about timing as a positioning tools when we manipulate time.
  • How to use ScrollTrigger to scrub an animation via proxy.
  • How to use some of GSAP’s awesome utilities to handle logic for us.

You can now manipulate time! 😅

That concept of going “meta” GSAP opens up a variety of possibilities. What else could you animate? Audio? Video? As for the ”Cover Flow” demo, here’s where that went!

The post Going “Meta GSAP”: The Quest for “Perfect” Infinite Scrolling appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , , , , ,

Why Netlify?

I think it’s fair to think of Netlify as a CDN-backed static file host. But it would also be silly to think that’s all it is. That’s why I think it’s smart for them to have pages like this, comparing Netlify to GitHub Pages. GitHub Pages is a lot closer to only being a static file host. That’s still nice, but Netlify just brings so much more to the table.

Need to add a functional form to the site? Netlify does that.

Need to roll back to a previous version without any git-fu? Netlify does that.

Need to make sure you’re caching assets the best you can and breaking that cache for new versions? Netlify does that.

Need a preview of a pull request before you merge it? Netlify does that.

Need to set up redirects and rewrite rules so that your SPA behaves correctly? Netlify does that.

Need to run some server-side code? Netlify does that.

Need to do some A/B testing? Netlify does that.

That’s not all, just a random spattering of Netlify’s many features that take it to another level of hosting with a developer experience that’s beyond a static file host.

This same kind of thing came up on ShopTalk the other week. Why pick Netlify when you can toss files in a S3 bucket with Cloudfront in front of it? It’s a fair question, as maybe the outcome isn’t that different. But there are 100 other things to think about that, once you do, make Netlify seem like a no-brainer.

The post Why Netlify? appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.



Cloud study

This Robin Sloan fella is an interesting character. Not only have I read his one of his fiction novels, the tremendous Mr. Penumbra’s 24‑Hour Bookstore, but I also use the olive oil he makes with partner Kathryn Tomajan. But here, I’m linking to an article from his not-so-secret email club The Society of Double Daggers that’s about… cloud functions?

Back in January, when I was putting this website together, I’d already implemented the subscriber tools on AWS Lambda when Google announced that their Cloud Functions would support Ruby. I did a little investigation and was so impressed that I switched everything over; now, I want to share a couple of findings.

A glowing endorsement of the DX behind Google’s cloud functions over AWS Lambdas.

Direct Link to ArticlePermalink

The post Cloud study appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.



CSS is a Strongly Typed Language

One of the ways you can classify a programming language is by how strongly or weakly typed it is. Here, “typed” means if variables are known at compile time. An example of this would be a scenario where an integer (1) is added to a string containing an integer ("1"):

result = 1 + "1";

The string containing an integer could have been unintentionally generated from a complicated suite of logic with lots of moving parts. It could also have been intentionally generated from a single source of truth.

Despite the connotations that the terms “weak” and “strong” imply, a strongly-typed programming language isn’t necessarily better than a weakly-typed one. There may be scenarios where flexibility is needed more than rigidity, and vice-versa. As with many aspects of programming, the answer is dependent on multiple external contexts (i.e “it depends”).

The other interesting bit is there is no formal definition of what constitutes strong or weak typing. This means that perceptions of what is considered a strongly or weakly-typed language differ from person to person, and may change over time.


JavaScript is considered a weakly-typed language, and this flexibility contributed to its early adoption on the web. However, as the web has matured and industrialized, use cases for JavaScript have become more complicated.

Extensions like TypeScript were created to help with this. Think of it as a “plugin” for JavaScript, which grafts strong typing onto the language. This helps programmers navigate complicated setups. An example of this could be a data-intensive single page application used for e-commerce.

TypeScript is currently very popular in the web development industry, and many new projects default to using TypeScript when first setting things up.

Compile time

Compile time is the moment when a programming language is converted into machine code. It is a precursor to runtime, the moment when machine code is performed by the computer.

As with many things on the web, compile time is a bit tricky. A setup that utilizes TypeScript will stitch together component pieces of JavaScript code and compile them into a single JavaScript file for the browser to read and run.

The time when component pieces compile is when they are all combined. TypeScript serves as a kind of overseer, and will yell at you if you try to break the typed conventions you have set up for yourself before combination occurs.

A tooltip that reads, “var content: any. Property ‘content’ does not exist on type ‘PropsWithChildren<Props>’. ts(2339). View Problem (Option F8). No quick fixes available. Tooltip is pointing towards an argument called “content.”
A sample TypeScript error in VS Code.

The stitched-together JavaScript file is then ingested by the browser, which has its own compile time. Browser compile time is highly variable, depending on:

  • The device the browser is on,
  • What other work the browser is doing, and
  • What other work the device’s other programs are doing.

TypeScript isn’t directly used by the browser, but its presence is felt. JavaScript is fragile. TypeScript helps with this fragility by trying to prevent errors upstream in the code editor. This lessens the chance errors occur in the JavaScript read by the browser — errors that would cause JavaScript to stop functioning on the website or web app a person is using.


CSS is a declarative, domain-specific programming language. It is also strongly typed. For the most part, values in CSS stay declared as authored. If a value is invalid the browser throws the entire property away.

Types in CSS

The list of types in CSS is thorough. They are:

Textual types
  • Globally-scoped keywords:
    • initial
    • inherit
    • unset
    • revert
  • Custom identifies, which are specifically used for things, such as providing a grid-area name
  • Strings, such as, "hello"
  • URLs, such as
  • Dashed idents (--), which are used to create custom properties (more on this in a bit)
Numeric types
  • Integers, which are decimal numbers 0–9
  • Real numbers, such as 3.14
  • Percentages, such as 25%
  • Dimensions, a number with a unit appended to it such as (100px or 3s)
  • Ratios, such as 16/9
  • flex, a variable length for CSS grid calculation
Quantity types
  • Lengths:
  • Angles, such as 15deg
  • Time, such as 250ms
  • Frequencies, such 16Hz
  • Resolution, such as 96dpi

Dimensions and lengths might seem similar, but dimensions can contain percentages and lengths cannot.

Color types
  • Keywords:
    • Named colors, such as papayawhip
    • transparent
    • currentColor
  • RGB colors:
    • Hexidecimal notation, such as #FF8764
    • RGB/RGBa notation, such as rgba(105, 221, 174, 0.5)
  • HSL/HSLA colors, such as hsl(287, 76%, 50%)
  • System colors, such as ButtonText
Image types
  • Image, which is a URL reference to an image file or gradient
  • color-stop-list, a list of two or more color stops, used for gradient notion
  • linear-color-stop, a color and length expression used to indicate a gradient color stop
  • linear-color-hint, a length percentage used to interpolate color
  • ending-shape, which uses a keyword of either circle or ellipse for radial gradients
2D positioning types
  • Keywords:
    • top
    • right
    • bottom
    • left
    • center
  • A percentage length, such as 25%

Programming in CSS

The bulk of programming in CSS is authoring selectors, then specifying a suite of properties and their requisite values. Collections of selectors give content a visual form, much as how collections of JavaScript logic creates features.

CSS has functions. It can perform calculation, conditional logic, algorithmic expressions, state, and mode-based behavior. It also has custom properties, which are effectively CSS variables that allow values to be updated dynamically. Heck, you can even solve fizzbuzz with CSS.

Like other programming languages, there is also a “meta” layer, with different thoughts and techniques on how to organize, manage and maintain things.

Throwing errors

Unlike other programming languages where code largely exists under the hood, CSS is highly visual. You won’t see warnings or errors in the console if you use an invalid value for a property declaration, but you will get visuals that don’t update the way you anticipated.

The reason for this is that CSS is resilient. When visuals don’t update because of a misconstructed declaration, CSS is prioritizing, ensuring content can be shown at all costs and will render every other valid declaration it possibly can. This is in keeping with the design principles of the language, the principles of the platform, and the overarching goals of the web’s mission.


Let’s demonstrate how strong typing in CSS keeps the guardrails on in three examples: one with a straightforward property/value declaration, one with calculation, and one with redefining a custom property.

Example 1: Straightforward property/value declaration

See the Pen Basic example by Eric Bailey (@ericwbailey) on CodePen.

For this example, the browser does not understand the banner’s border-style “potato” declaration. Note that the other .banner class selector property/value declarations are honored by the browser and rendered, even though border-style has a type mismatch. This is an example of how resilient CSS is.

The border-style declaration is expecting one of the following textual style types:

  • Globally-scoped keywords, or a
  • Dashed indent for a custom property.

If we update border-style to use a valid, typed value of dotted, the browser will render the border!

Example 2: Calculation

The calc() function in CSS allows us to take two arguments and an operator to return a calculated result. If one of the arguments doesn’t use a valid type, the calculation won’t work.

In this Pen, the p selector’s font-size property is expecting a value with a numeric dimension type (e.g. 1.5rem). However, the calculation function produces an invalid type value for the font-size property. This is because the second argument in the calc() function is a string ("2rem"), and not a numeric dimension type.

Because of this, the paragraph’s font size falls back to the next most applicable parent node — the font-size of 1.5rem declared on the body element.

This is a bit in the weeds, but worth pointing out: Combining two custom properties in a calc() function can cause errors. While both custom properties may be valid on their own, calc() will not accept dashed indent textual types. Think of a scenario where we might try multiplying custom properties that contain mismatched units, e.g. --big: 500px and --small: 1em.

Example 3: Redefined custom property

Like JavaScript variables, custom property values can be redefined. This flexibility allows for things like easily creating dark mode color themes.

In the :root selector of this CodePen, I have set a custom property of --color-cyan, with a value of #953FE3. Then, in the .square class, I have updated the --color-cyan custom property’s value to be top. While top is a valid, typed value, it is not a type that background-color honors.

Notice that the updated custom property is scoped to .square, and does not affect other usages, such as the right-hand border on the phrase “Don’t play to type.” And if you remove the redefined custom property from .square, you’ll see the cyan background color snap back in.

While this is a bit contrived, it serves as an example of how redefining custom properties can get away from you if you’re not careful.

This phenomenon can be found in projects with poor communication, larger CSS codebases, and situations where CSS preprocessors are used to construct custom properties at scale.


With the gift of hindsight, I think a lack of console warnings for CSS is a flaw, and has contributed to a lot of the negative perceptions about the language.

Hoping a developer will notice a potentially tiny visual change is too big an ask, and does not meet them where they are for most of their other daily tools. There are a couple of initiatives I’m aware of that try to address this.

First is stylelint, a linter made specifically to deal with CSS and CSS-like preprocessing languages. stylelint can integrate with code editors, task runners, command line tools, and GitHub actions to help keep your CSS under control. This allows it to meet developers where they already are.

A tooltip that reads, “var content: any. Property ‘content’ does not exist on type ‘PropsWithChildren<Props>’. ts(2339). View Problem (Option F8). No quick fixes available. Tooltip is pointing towards an argument called “content.”
stylelint terminal output.

Second is Firefox’s excellent suite of CSS inspection options in its Developer Tools. In particular, I would like to call attention to its ability to identify unused CSS. This is extremely helpful for identifying selectors that may have run afoul of a type mismatch.

Tooltip attached to an unused selector in the Developer panel. The tooltip reads, “vertical-align has no effect on this element since it’s not an inline or table-cell element. Try adding display: inline or display: table-cell. Learn more. Screenshot.”
Firefox Developer edition

Wrapping up

CSS has been strongly typed for as long as it has been a programming language, and as a programming language it has been around for a long time. It’s also done a lot of growing up lately. If you haven’t checked in, there are some new, amazing features available.

As strongly-typed JavaScript becomes more popular, it is my hope that it helps developers become more comfortable with the firm, yet flexible approach of CSS.

Thank you to Miriam Suzanne for her feedback.

The post CSS is a Strongly Typed Language appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, ,

See You Around

Get it? Because this blog post is about Around, the wonderful new video call software. I’ve been using it for my video calls and I’d be happy to deliver you a TLDR right off the bat: It’s nice. It has all the important features of video call software you need while being very design-focused in a way that feels stand-out fresh. Thank god someone is getting this right.

Little floating circles

Make no mistake: chatting with people where you see their faces in a little floating circle is way nicer than a giant rectangle. It may not seem like a massive difference, but it really does feel different and better, particularly when you’re chatting with multiple people and/or people you chat with all the time.

You still get the face, that all-important human connector, but you aren’t seeing my disheveled bookshelf, my laundry hamper, or my dead plant in the corner. Even if you have a nice background or nobody cares about your unmade bed in the background (they probably don’t), there is a literal fatigue that sets in when you have a camera pointed at your whole area for any sustained period. It’s hard to describe, but it just feels like… a lot. That all changes for the better when all you are sharing is a cropped circle of your face. There is less pressure to be maintaining eye contact, for one thing. Here’s me and Geoff:

I have these little circles tucked into the upper right of my monitor.

There is real tech behind these circles. The virtual camera “zooms” to properly size your face in the circle so you’re always in focus. If you move off to any direction, it “pans” to keep you centered (as much as it can). Even the color filters it offers, while on the surface might just seem like a bit of fun, lower video fatigue. I gotta imagine the pressure to wear makeup is a bit lower when you’re green and pixelated:

The circles go everywhere in Around. They don’t have to be floating (although I like that mode the best). If you pop into Campfire mode, you’ll see everyone together in a more normal/dedicated window. If you pop into Notes or Image Sharing view, they come along in there too. Speaking of which…


There is a collaborative Notes view in every call. The editor is very polished, and I was impressed. This isn’t some half-baked slapped-on chatroom-esque thing; it’s for actual formatted note-taking. The real-time-ness of it is spot on.

But here’s the actual best feature… when the meeting ends, everyone in the meeting gets emailed a copy of the notes. I’m going to give that three clap-hands emojis: 👏👏👏. Here’s Dave and I having a little call about ShopTalk, writing some silly notes, next to the emailed notes that arrive immediately after the end of the call:

Just the notes. No crap.

Notes can have a chat-like feel to them, but there is literally a chat feature as well. Like everything in Around, it’s got interesting UX to it. When you type and send a chat message, it appears next to your floating head. Just one chat message. It stays there until you remove it. It just makes a ton of sense. You don’t really need a traditional chatroom, you’re already talking to each other! More likely you need to share a link or something, and this UI is perfect for that.


Aside from notes, there is Image Sharing, which is a very focused way to get everyone looking at one thing.

And then, of course, Screen Sharing. All the features you need for that are there. You can share your whole screen, or select just one window. Here’s me having a meeting with Dave where we’re looking at different polling options:

I’m sharing my browser window. I have the floaty heads I can move wherever.

Dave sees my screen within the Around interface with the floaty heads in there.

You can give control of your computer to the person you are sharing with as well, which is awfully handy for pair programming sessions, which I’m doing constantly.

Audio quality

The first thing that Geoff said to me when we popped on to our first Around call was “Wow, you sound good.” I don’t hear that often at my desk because I work in an office with glass walls and it’s a bit echo-y and I haven’t gotten around to sound-dampening stuff in here yet. There is more real tech at work here with Around’s build-in noise reduction.

Around takes this echo cancellation stuff even further with their EchoTerminator feature (video). Even if you’re in the same room as other people on the same Around call, you don’t have to do that little dance where everyone mutes except one and hope that works. With Around, you don’t have to think about it, it just works (without the echo and feedback).


I spent a good while today watching Dave basically re-create the Dramatic Chipmunk GIF with his face, playing with how Around detects and zooms on faces. So that was a good time. But many of the features of Around have fun built right in.

You can just straight up turn off your video if you want, of course, or, you can be a cute bear GIF. It just stays that way until you turn it off. So you can use it as a reaction, a replacement for yourself, or just for a quick bit of fun.

Emoji reactions pop up and cover your face for a second. Way more fun that it should be.


The most important integration for a video call app to me is Slack. That’s where my co-workers are, so kicking off a call happens many times a day right from there. As expected, you can do /around and off we go. Google Calendar is also a no-brainer.

On a serious note, it’s good to see Around on top of security. For example, if you share an image, it’s on their servers only until the call is over and then deleted. All text is encrypted. Meeting rooms aren’t going to get bombed as there are one-off ID’s and entrance controls.

Ready to give it a try?

Around is free for anyone to download, with pricing coming later this year.

The post See You Around appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.



How to Leverage the Fullscreen API… and Style It

Let’s look at the Fullscreen API in JavaScript. It allows you to do a pretty powerful thing: full screening exactly one particular element you want it to. Not only that, but CSS can help as well with a special selector.

Every major browser has built-in fullscreen functionality in the form of maximizing the browser window itself. You press F11 or ⌃⌘F (WinKey ⬆ on PC) and the browser will fill your monitor, going as far as removing UI components (like the window management buttons).

When you go into fullscreen mode, it maximizes the space for a web page to display, but this isn’t always exactly what you want. More often, you either want to fullscreen some particular element of the page, for example, a video or a game. This is where Fullscreen API comes in handy.

The Fullscreen API helps achieve something that the browser‘s fullscreen modes won’t be able to, like:

  • Fullscreen a specific element of the page and not the whole page.
  • Match elements in CSS with the :fullscreen pseudo-class, which checks if a particular element’s fullscreen flag is set.
  • Full control over when to enter fullscreen.

Let’s do it

First, decide what element needs to be shown in fullscreen mode. After that, the only thing is to check if that element has the requestFullscreen() method, and then call it on the element.

Let’s start with a simple <div> element to fullscreen:

<div id="fullscreen"></div>

First, we’ll select it and check if it has the method. If it does, then we call the requestFullscreen method on it. It’s that simple:

let fullscreen = document.querySelector("#fullscreen");  if (fullscreen.requestFullscreen) {   fullscreen.requestFullscreen(); }

But, we want to run this code conditionally and not as soon as somebody lands on the page. We’ll make a button that toggles fullscreen mode.

<div id="fullscreen">   <button id="button">Toggle Fullscreen</button> </div>
let fullscreen = document.querySelector("#fullscreen"); let button = document.querySelector("#button");  button.addEventListener("click", () => {   if (!document.fullscreenElement) {     fullscreen?.requestFullscreen();   } else {     document.exitFullscreen();   } });

Notice how we’re using document.fullscreenElement. If there is an element that has been fullscreen before, then it will return it; if not, it will return null.

It’s also a good idea to check if fullscreen mode was even enabled in the user’s browser at all. For that, we can use document.fullscreenEnabled. It returns a boolean saying whether or not we can use the Fullscreen API in this particular browser. Fullscreen could be disabled by the user, or the browser might not support it.

The last method that we will cover is document.exitFullscreen(). A user should always have the ability to exit fullscreen mode by pressing ESC on the keyboard. We could do some sort of custom way to exit fullscreen mode using exitFullscreen. It doesn’t matter what element was fullscreen; it will go back to window mode after calling this method.

As you can see in the code for our button element, we first check if fullscreen has already been activated. Then, based on that information, we either go to fullscreen mode or we go back to window mode.

Styling fullscreen

The cool thing about the Fullscreen API is that we can match a fullscreen element in CSS. That’s exactly what the :fullscreen pseudo-selector is designed to do!

#fullscreen:fullscreen {   background-color: yellow; }

As you can see, the #fullscreen div is the only element that gets a yellow background color when the element is in fullscreen mode, and only in fullscreen mode. The catch is that we can’t actually select any element like this; we can only select the root element that’s in fullscreen mode. None of the child elements will match.

In other words, something like #button:fullscreen won’t select a button that is contained within the fullscreen element — that is, unless we want the button to be fullscreen instead of the div.

Some browsers require a prefix for this to work. Firefox uses -moz-full-screen and WebKit-based browsers use -webkit-full-screen.

#fullscreen:-webkit-full-screen {   background-color: yellow; }  #fullscreen:-moz-full-screen {   background-color: yellow; }

Did you know that when we’re in fullscreen mode that there is a pseudo-element that is rendered right below your fullscreen element? By default that pseudo-element is black. If you want to change the styles of that pseudo-element you can do it like this:

#fullscreen::backdrop {   background-color: skyblue; }

Here’s the final example. Keep in mind that fullscreen mode might not work with embedded Pens, so you will need to view it outside this article.

The post How to Leverage the Fullscreen API… and Style It appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , ,

Headless Form Submission With the WordPress REST API

If you’re building a WordPress site, you need a good reason not to choose a WordPress form plugin. They are convenient and offer plenty of customizations that would take a ton of effort to build from scratch. They render the HTML, validate the data, store the submissions, and provide integration with third-party services.

But suppose we plan to use WordPress as a headless CMS. In this case, we will be mainly interacting with the REST API (or GraphQL). The front-end part becomes our responsibility entirely, and we can’t rely anymore on form plugins to do the heavy lifting in that area. Now we’re in the driver’s seat when it comes to the front end.

Forms were a solved problem, but now we have to decide what to do about them. We have a couple of options:

  • Do we use our own custom API if we have such a thing? If not, and we don’t want to create one, we can go with a service. There are many good static form providers, and new ones are popping up constantly.
  • Can we keep using the WordPress plugin we already use and leverage its validation, storage, and integration?

The most popular free form plugin, Contact Form 7, has a submission REST API endpoint, and so does the well-known paid plugin, Gravity Forms, among others.

From a technical standpoint, there’s no real difference between submitting the form‘s data to an endpoint provided by a service or a WordPress plugin. So, we have to decide based on different criteria. Price is an obvious one; after that is the availability of the WordPress installation and its REST API. Submitting to an endpoint presupposes that it is always available publicly. That’s already clear when it comes to services because we pay for them to be available. Some setups might limit WordPress access to only editing and build processes. Another thing to consider is where you want to store the data, particularly in a way that adheres to GPDR regulations.

When it comes to features beyond the submission, WordPress form plugins are hard to match. They have their ecosystem, add-ons capable of generating reports, PDFs, readily available integration with newsletters, and payment services. Few services offer this much in a single package.

Even if we use WordPress in the “traditional” way with the front end based on a WordPress theme, using a form plugin’s REST API might make sense in many cases. For example, if we are developing a theme using a utility-first CSS framework, styling the rendered form with fixed markup structured with a BEM-like class convention leaves a sour taste in any developer’s mouth.

The purpose of this article is to present the two WordPress form plugins submission endpoints and show a way to recreate the typical form-related behaviors we got used to getting out of the box. When submitting a form, in general, we have to deal with two main problems. One is the submission of the data itself, and the other is providing meaningful feedback to the user.

So, let’s start there.

The endpoints

Submitting data is the more straightforward part. Both endpoints expect a POST request, and the dynamic part of the URL is the form ID.

Contact Form 7 REST API is available immediately when the plugin is activated, and it looks like this:


If we’re working with Gravity Forms, the endpoint takes this shape:


The Gravity Forms REST API is disabled by default. To enable it, we have to go to the plugin’s settings, then to the REST API page, and check the “Enable access to the API” option. There is no need to create an API key, as the form submission endpoint does not require it.

The body of the request

Our example form has five fields with the following rules:

  • a required text field
  • a required email field
  • a required date field that accepts dates before October 4, 1957
  • an optional textarea
  • a required checkbox

For Contact Form 7’s request’s body keys, we have to define them with the form-tags syntax:

{   "somebodys-name": "Marian Kenney",   "any-email": "",   "before-space-age": "1922-03-11",   "optional-message": "",   "fake-terms": "1" }

Gravity Forms expects the keys in a different format. We have to use an auto-generated, incremental field ID with the input_ prefix. The ID is visible when you are editing the field.

{   "input_1": "Marian Kenney",   "input_2": "",   "input_3": "1922-03-11",   "input_4": "",   "input_5_1": "1" }

Submitting the data

We can save ourselves a lot of work if we use the expected keys for the inputs’ name attributes. Otherwise, we have to map the input names to the keys.

Putting everything together, we get an HTML structure like this for Contact Form 7:

<form action="https://your-site.tld/wp-json/contact-form-7/v1/contact-forms/<FORM_ID>/feedback" method="post">   <label for="somebodys-name">Somebody's name</label>   <input id="somebodys-name" type="text" name="somebodys-name">   <!-- Other input elements -->   <button type="submit">Submit</button> </form>

In the case of Gravity Forms, we only need to switch the action and the name attributes:

<form action="https://your-site.tld/wp-json/gf/v2/forms/<FORM_ID>/submissions" method="post">   <label for="input_1">Somebody's name</label>   <input id="input_1" type="text" name="input_1">   <!-- Other input elements -->   <button type="submit">Submit</button> </form>

Since all the required information is available in the HTML, we are ready to send the request. One way to do this is to use the FormData in combination with the fetch:

const formSubmissionHandler = (event) => {   event.preventDefault();    const formElement =,     { action, method } = formElement,     body = new FormData(formElement);    fetch(action, {     method,     body   })     .then((response) => response.json())     .then((response) => {       // Determine if the submission is not valid       if (isFormSubmissionError(response)) {         // Handle the case when there are validation errors       }       // Handle the happy path     })     .catch((error) => {       // Handle the case when there's a problem with the request     }); };  const formElement = document.querySelector("form");  formElement.addEventListener("submit", formSubmissionHandler);

We can send the submission with little effort, but the user experience is subpar, to say the least. We owe to users as much guidance as possible to submit the form successfully. At the very least, that means we need to:

  • show a global error or success message,
  • add inline field validation error messages and possible directions, and
  • draw attention to parts that require attention with special classes.

Field validation

On top of using built-in HTML form validation, we can use JavaScript for additional client-side validation and/or take advantage of server-side validation.

When it comes to server-side validation, both Contact Form 7 and Gravity Forms offer that out of the box and return the validation error messages as part of the response. This is convenient as we can control the validation rules from the WordPress admin.

For more complex validation rules, like conditional field validation, it might make sense to rely only on the server-side because keeping the front-end JavaScript validation in sync with the plugins setting can become a maintenance issue.

If we solely go with the server-side validation, the task becomes about parsing the response, extracting the relevant data, and DOM manipulation like inserting elements and toggle class-names.

Response messages

The response when there is a validation error for Contact Form 7 look like this:

{   "into": "#",   "status": "validation_failed",   "message": "One or more fields have an error. Please check and try again.",   "posted_data_hash": "",   "invalid_fields": [     {       "into": "span.wpcf7-form-control-wrap.somebodys-name",       "message": "The field is required.",       "idref": null,       "error_id": "-ve-somebodys-name"     },     {       "into": "span.wpcf7-form-control-wrap.any-email",       "message": "The field is required.",       "idref": null,       "error_id": "-ve-any-email"     },     {       "into": "span.wpcf7-form-control-wrap.before-space-age",       "message": "The field is required.",       "idref": null,       "error_id": "-ve-before-space-age"     },     {       "into": "span.wpcf7-form-control-wrap.fake-terms",       "message": "You must accept the terms and conditions before sending your message.",       "idref": null,       "error_id": "-ve-fake-terms"     }   ] }

On successful submission, the response looks like this:

{   "into": "#",   "status": "mail_sent",   "message": "Thank you for your message. It has been sent.",   "posted_data_hash": "d52f9f9de995287195409fe6dcde0c50" }

Compared to this, Gravity Forms’ validation error response is more compact:

{   "is_valid": false,   "validation_messages": {     "1": "This field is required.",     "2": "This field is required.",     "3": "This field is required.",     "5": "This field is required."   },   "page_number": 1,   "source_page_number": 1 }

But the response on a successful submission is bigger:

{   "is_valid": true,   "page_number": 0,   "source_page_number": 1,   "confirmation_message": "<div id='gform_confirmation_wrapper_1' class='gform_confirmation_wrapper '><div id='gform_confirmation_message_1' class='gform_confirmation_message_1 gform_confirmation_message'>Thanks for contacting us! We will get in touch with you shortly.</div></div>",   "confirmation_type": "message" }

While both contain the information we need, they don‘t follow a common convention, and both have their quirks. For example, the confirmation message in Gravity Forms contains HTML, and the validation message keys don’t have the input_ prefix — the prefix that’s required when we send the request. On the other side, validation errors in Contact Form 7 contain information that is relevant only to their front-end implementation. The field keys are not immediately usable; they have to be extracted.

In a situation like this, instead of working with the response we get, it’s better to come up with a desired, ideal format. Once we have that, we can find ways to transform the original response to what we see fit. If we combine the best of the two scenarios and remove the irrelevant parts for our use case, then we end up with something like this:

{   "isSuccess": false,   "message": "One or more fields have an error. Please check and try again.",   "validationError": {     "somebodys-name": "This field is required.",     "any-email": "This field is required.",     "input_3": "This field is required.",     "input_5": "This field is required."   } }

And on successful submission, we would set isSuccess to true and return an empty validation error object:

{   "isSuccess": true,   "message": "Thanks for contacting us! We will get in touch with you shortly.",   "validationError": {} }

Now it’s a matter of transforming what we got to what we need. The code to normalize the Contact Forms 7 response is this:

const normalizeContactForm7Response = (response) => {   // The other possible statuses are different kind of errors   const isSuccess = response.status === 'mail_sent';   // A message is provided for all statuses   const message = response.message;   const validationError = isSuccess     ? {}     : // We transform an array of objects into an object     Object.fromEntries( => {         // Extracts the part after "cf7-form-control-wrap"         const key = /cf7[-a-z]*.(.*)/.exec(error.into)[1];          return [key, error.message];       })     );    return {     isSuccess,     message,     validationError,   }; };

The code to normalize the Gravity Forms response winds up being this:

const normalizeGravityFormsResponse = (response) => {   // Provided already as a boolean in the response   const isSuccess = response.is_valid;   const message = isSuccess     ? // Comes wrapped in a HTML and we likely don't need that       stripHtml(response.confirmation_message)     : // No general error message, so we set a fallback       'There was a problem with your submission.';   const validationError = isSuccess     ? {}     : // We replace the keys with the prefixed version;       // this way the request and response matches       Object.fromEntries(         Object.entries(             response.validation_messages         ).map(([key, value]) => [`input_$ {key}`, value])       );    return {     isSuccess,     message,     validationError,   }; };

We are still missing a way to display the validation errors, success messages, and toggling classes. However, we have a neat way of accessing the data we need, and we removed all of the inconsistencies in the responses with a light abstraction. When put together, it’s ready to be dropped into an existing codebase, or we can continue building on top of it.

There are many ways to tackle the remaining part. What makes sense will depend on the project. For situations where we mainly have to react to state changes, a declarative and reactive library can help a lot. Alpine.js was covered here on CSS-Tricks, and it’s a perfect fit for both demonstrations and using it in production sites. Almost without any modification, we can reuse the code from the previous example. We only need to add the proper directives and in the right places.

Wrapping up

Matching the front-end experience that WordPress form plugins provide can be done with relative ease for straightforward, no-fuss forms — and in a way that is reusable from project to project. We can even accomplish it in a way that allows us to switch the plugin without affecting the front end.

Sure, it takes time and effort to make a multi-page form, previews of the uploaded images, or other advanced features that we’d normally get baked right into a plugin, but the more unique the requirements we have to meet, the more it makes sense to use the submission endpoint as we don’t have to work against the given front-end implementation that tries to solve many problems, but never the particular one we want.

Using WordPress as a headless CMS to access the REST API of a form plugin to hit the submissions endpoints will surely become a more widely used practice. It’s something worth exploring and to keep in mind. In the future, I would not be surprised to see WordPress form plugins designed primarily to work in a headless context like this. I can imagine a plugin where front-end rendering is an add-on feature that’s not an integral part of its core. What consequences that would have, and if it could have commercial success, remains to be explored but is a fascinating space to watch evolve.

The post Headless Form Submission With the WordPress REST API appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , , ,

Typography tips and best practices for websites