Css Tricks

Syndicate content CSS-Tricks
Tips, Tricks, and Techniques on using Cascading Style Sheets.
Updated: 4 hours 47 min ago

Explaining the Accessible Benefits of Using Semantic HTML Elements

Thu, 11/06/2025 - 5:57am

Here’s something you’ll spot in the wild:

<div class="btn" role="button">Custom Button</div>

This is one of those code smells that makes me stop in my tracks because we know there’s a semantic <button> element that we can use instead. There’s a whole other thing about conflating anchors (e.g., <a class="btn">) and buttons, but that’s not exactly what we’re talking about here, and we have a great guide on it.

A semantic <button> element makes a lot more sense than reaching for a <div> because, well, semantics. At least that’s what the code smell triggers for me. I can generically name some of the semantical benefits we get from a <button> off the top of my head:

  • Interactive states
  • Focus indicators
  • Keyboard support

But I find myself unable to explicitly define those benefits. They’re more like talking points I’ve retained than clear arguments for using <button> over <div>. But as I’ve made my way through Sara Soueidan’s Practical Accessibility course, I’m getting a much clearer picture of why <button> is a best practice.

Let’s compare the two approaches:

CodePen Embed Fallback

Did you know that you can inspect the semantics of these directly in DevTools? I’m ashamed to admit that I didn’t before watching Sara’s course.

There’s clearly a difference between the two “buttons” and it’s more than visual. Notice a few things:

  • The <button> gets exposed as a button role while the <div> is a generic role. We already knew that.
  • The <button> gets an accessible label that’s equal to its content.
  • The <button> is focusable and gets a click listener right out of the box.

I’m not sure exactly why someone would reach for a <div> over a <button>. But if I had to wager a guess, it’s probably because styling <button> is tougher that styling a <div>. You’ve got to reset all those user agent styles which feels like an extra step in the process when a <div> comes with no styling opinions whatsoever, save for it being a block-level element as far as document flow goes.

I don’t get that reasoning when all it take to reset a button’s styles is a CSS one-liner:

CodePen Embed Fallback

From here, we can use the exact same class to get the exact same appearance:

CodePen Embed Fallback

What seems like more work is the effort it takes to re-create the same built-in benefits we get from a semantic <button> specifically for a <div>. Sara’s course has given me the exact language to put words to the code smells:

  • The div does not have Tab focus by default. It is not recognized by the browser as an interactive element, even after giving it a button role. The role does not add behavior, only how it is presented to screen readers. We need to give it a tabindex.
  • But even then, we can’t operate the button on Space or Return. We need to add that interactive behavior as well, likely using a JavaScript listener for a button press to fire a function.
  • Did you know that the Space and Return keys do different things? Adrian Roselli explains it nicely, and it was a big TIL moment for me. Probably need different listeners to account for both interactions.
  • And, of course, we need to account for a disabled state. All it takes is a single HTML attribute on a <button>, but a <div> probably needs yet another function that looks for some sort of data-attribute and then sets disabled on it.

Oh, but hey, we can slap <div role=button> on there, right? It’s super tempting to go there, but all that does is expose the <div> as a button to assistive technology. It’s announced as a button, but does nothing to recreate the interactions needed for the complete user experience a <button> does. And no amount of styling will fix those semantics, either. We can make a <div> look like a button, but it’s not one despite its appearances.

Anyway, that’s all I wanted to share. Using semantic elements where possible is one of those “best practice” statements we pick up along the way. I teach it to my students, but am guilty of relying on the high-level “it helps accessibility” reasoning that is just as generic as a <div>. Now I have specific talking points for explaining why that’s the case, as well as a “new-to-me” weapon in my DevTools arsenal to inspect and confirm those points.

Thanks, Sara! This is merely the tip of the iceberg as far as what I’m learning (and will continue to learn) from the course.

Explaining the Accessible Benefits of Using Semantic HTML Elements originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

The “Most Hated” CSS Feature: tan()

Mon, 11/03/2025 - 6:03am

Last time, we discussed that, sadly, according to the State of CSS 2025 survey, trigonometric functions are deemed the “Most Hated” CSS feature.

That shocked me. I may have even been a little offended, being a math nerd and all. So, I wrote an article that tried to showcase several uses specifically for the cos() and sin() functions. Today, I want poke at another one: the tangent function, tan().

CSS Trigonometric Functions: The “Most Hated” CSS Feature
  1. sin() and cos()
  2. tan() (You are here!)
  3. asin(), acos(), atan() and atan2() (Coming soon)

Before getting to examples, we have to ask, what is tan() in the first place?

The mathematical definition

The simplest way to define the tangent of an angle is to say that it is equal to the sine divided by its cosine.

Again, that’s a fairly simple definition, one that doesn’t give us much insight into what a tangent is or how we can use it in our CSS work. For now, remember that tan() comes from dividing the angles of functions we looked at in the first article.

Unlike cos() and sin() which were paired with lots of circles, tan() is most useful when working with triangular shapes, specifically a right-angled triangle, meaning it has one 90° angle:

If we pick one of the angles (in this case, the bottom-right one), we have a total of three sides:

  • The adjacent side (the one touching the angle)
  • The opposite side (the one away from the angle)
  • The hypotenuse (the longest side)

Speaking in those terms, the tan() of an angle is the quotient — the divided result — of the triangle’s opposite and adjacent sides:

If the opposite side grows, the value of tan() increases. If the adjacent side grows, then the value of tan() decreases. Drag the corners of the triangle in the following demo to stretch the shape vertically or horizontally and observe how the value of tan() changes accordingly.

CodePen Embed Fallback

Now we can start actually poking at how we can use the tan() function in CSS. I think a good way to start is to look at an example that arranges a series of triangles into another shape.

Sectioned lists

Imagine we have an unordered list of elements we want to arrange in a polygon of some sort, where each element is a triangular slice of the polygonal pie.

So, where does tan() come into play? Let’s start with our setup. Like last time, we have an everyday unordered list of indexed list items in HTML:

<ul style="--total: 8"> <li style="--i: 1">1</li> <li style="--i: 2">2</li> <li style="--i: 3">3</li> <li style="--i: 4">4</li> <li style="--i: 5">5</li> <li style="--i: 6">6</li> <li style="--i: 7">7</li> <li style="--i: 8">8</li> </ul>

Note: This step will become much easier and concise when the sibling-index() and sibling-count() functions gain support (and they’re really neat). I’m hardcoding the indexes with inline CSS variables in the meantime.

So, we have the --total number of items (8) and an index value (--i) for each item. We’ll define a radius for the polygon, which you can think of as the height of each triangle:

:root { --radius: 35vmin; }

Just a smidge of light styling on the unordered list so that it is a grid container that places all of the items in the exact center of it:

ul { display: grid; place-items: center; } li { position: absolute; }

Now we can size the items. Specifically, we’ll set the container’s width to two times the --radius variable, while each element will be one --radius wide.

ul { /* same as before */ display: grid; place-items: center; /* width equal to two times the --radius */ width: calc(var(--radius) * 2); /* maintain a 1:1 aspect ratio to form a perfect square */ aspect-ratio: 1; } li { /* same as before */ position: absolute; /* each triangle is sized by the --radius variable */ width: var(--radius); }

Nothing much so far. We have a square container with eight rectangular items in it that stack on top of one another. That means all we see is the last item in the series since the rest are hidden underneath it.

CodePen Embed Fallback

We want to place the elements around the container’s center point. We have to rotate each item evenly by a certain angle, which we’ll get by dividing a full circle, 360deg, by the total number of elements, --total: 8, then multiply that value by each item’s inlined index value, --i, in the HTML.

li { /* rotation equal to a full circle divided total items times item index */ --rotation: calc(360deg / var(--total) * var(--i)); /* rotate each item by that amount */ transform: rotate(var(--rotation)); }

Notice, however, that the elements still cover each other. To fix this, we move their transform-origin to left center. This moves all the elements a little to the left when rotating, so we’ll have to translate them back to the center by half the --radius before making the rotation.

li { transform: translateX(calc(var(--radius) / 2)) rotate(var(--rotation)); transform-origin: left center; /* Not this: */ /* transform: rotate(var(--rotation)) translateX(calc(var(--radius) / 2)); */ }

This gives us a sort of sunburst shape, but it is still far from being an actual polygon. The first thing we can do is clip each element into a triangle using the clip-path property:

li { /* ... */ clip-path: polygon(100% 0, 0 50%, 100% 100%); }

It sort of looks like Wheel of Fortune but with gaps between each panel:

CodePen Embed Fallback

We want to close those gaps. The next thing we’ll do is increase the height of each item so that their sides touch, making a perfect polygon. But by how much? If we were fiddling with hard numbers, we could say that for an octagon where each element is 200px wide, the perfect item height would be 166px tall:

li { width: 200px; height: 166px; }

But what if our values change? We’d have to manually calculate the new height, and that’s no good for maintainability. Instead, we’ll calculate the perfect height for each item with what I hope will be your new favorite CSS function, tan().

I think it’s easier to see what that looks like if we dial things back a bit and create a simple square with four items instead of eight.

Notice that you can think of each triangle as a pair of two right triangles pressed right up against each other. That’s important because we know that tan() is really, really good for working with right angles.

Hmm, if only we knew what that angle near the center is equal to, then we could find the length of the triangle’s opposite side (the height) using the length of the adjacent side (the width).

We do know the angle! If each of the four triangles in the container can be divided into two right triangles, then we know that the eight total angles should equal a full circle, or 360°. Divide the full circle by the number of right angles, and we get 45° for each angle.

Back to our general polygons, we would translate that to CSS like this:

li { /* get the angle of each bisected triangle */ --theta: calc(360deg / 2 / var(--total)); /* use the tan() of that value to calculate perfect triangle height */ height: calc(2 * var(--radius) * tan(var(--theta))); }

Now we always have the perfect height value for the triangles, no matter what the container’s radius is or how many items are in it!

CodePen Embed Fallback

And check this out. We can play with the transform-origin property values to get different kinds of shapes!

CodePen Embed Fallback

This looks cool and all, but we can use it in a practical way. Let’s turn this into a circular menu where each item is an option you can select. The first idea that comes to mind for me is some sort of character picker, kinda like the character wheel in Grand Theft Auto V:

Image credit: Op Attack

…but let’s use more, say, huggable characters:

CodePen Embed Fallback

You may have noticed that I went a little fancy there and cut the full container into a circular shape using clip-path: circle(50% at 50% 50%). Each item is still a triangle with hard edges, but we’ve clipped the container that holds all of them to give things a rounded shape.

We can use the exact same idea to make a polygon-shaped image gallery:

CodePen Embed Fallback

This concept will work maybe 99% of the time. That’s because the math is always the same. We have a right triangle where we know (1) the angle and (2) the length of one of the sides.

tan() in the wild

I’ve seen the tan() function used in lots of other great demos. And guess what? They all rely on the exact same idea we looked at here. Go check them out because they’re pretty awesome:

Bonus: Tangent in a unit circle

In the first article, I talked a lot about the unit circle: a circle with a radius of one unit:

We were able to move the radius line in a counter-clockwise direction around the circle by a certain angle which was demonstrated in this interactive example:

CodePen Embed Fallback

We also showed how, given the angle, the cos() and sin() functions return the X and Y coordinates of the line’s endpoint on the circle, respectively:

CodePen Embed Fallback

We know now that tangent is related to sine and cosine, thanks to the equation we used to calculate it in the examples we looked at together. So, let’s add another line to our demo that represents the tan() value.

If we have an angle, then we can cast a line (let’s call it L) from the center, and its point will land somewhere on the unit circle. From there, we can draw another line perpendicular to L that goes from that point, outward, along X-axis.

CodePen Embed Fallback

After playing around with the angle, you may notice two things:

  1. The tan()value is only positive in the top-right and bottom-left quadrants. You can see why if you look at the values of cos() and sin() there, since they divide with one another.
  2. The tan() value is undefined at 90° and 270°. What do we mean by undefined? It means the angle creates a parallel line along the X-axis that is infinitely long. We say it’s undefined since it could be infinitely large to the right (positive) or left (negative). It can be both, so we say it isn’t defined. Since we don’t have “undefined” in CSS in a mathematical sense, it should return an unreasonably large number, depending on the case.
More trigonometry to come!

So far, we have covered the sin() cos() and tan() functions in CSS, and (hopefully) we successfully showed how useful they can be in CSS. Still, we are still missing the bizarro world of trigonometric functions: asin(), acos(), atan() atan2().

That’s what we’ll look at in the third and final part of this series on the “Most Hated” CSS feature of them all.

CSS Trigonometric Functions: The “Most Hated” CSS Feature
  1. sin() and cos()
  2. tan() (You are here!)
  3. asin(), acos(), atan() and atan2() (Coming soon)

The “Most Hated” CSS Feature: tan() originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Getting Creative With Small Screens

Wed, 10/29/2025 - 6:22am

Over the past few months, I’ve explored how we can get creative using well-supported CSS properties. Each article is intended to nudge web design away from uniformity, toward designs that are more distinctive and memorable. One bit of feedback from Phillip Bagleg deserves a follow up:

Andy’s guides are all very interesting, but mostly impractical in real life. Very little guidance on how magazine style design, works when thrown into a responsive environment.

Fair point well made, Phillip. So, let’s bust the myth that editorial-style web design is impractical on small screens.

My brief: Patty Meltt is an up-and-coming country music sensation, and she needed a website to launch her new album and tour. She wanted it to be distinctive-looking and memorable, so she called Stuff & Nonsense. Patty’s not real, but the challenges of designing and developing sites like hers are.

The problem with endless columns

On mobile, people can lose their sense of context and can’t easily tell where a section begins or ends. Good small-screen design can help orient them using a variety of techniques.

When screen space is tight, most designers collapse their layouts into a single long column. That’s fine for readability, but it can negatively impact the user experience when hierarchy disappears; rhythm becomes monotonous, and content scrolls endlessly until it blurs. Then, nothing stands out, and pages turn from being designed experiences into content feeds.

Like a magazine, layout delivers visual cues in a desktop environment, letting people know where they are and suggesting where to go next. This rhythm and structure can be as much a part of visual storytelling as colour and typography.

But those cues frequently disappear on small screens. Since we can’t rely on complex columns, how can we design visual cues that help readers feel oriented within the content flow and stay engaged? One answer is to stop thinking in terms of one long column of content altogether. Instead, treat each section as a distinct composition, a designed moment that guides readers through the story.

Designing moments instead of columns

Even within a narrow column, you can add variety and reduce monotony by thinking of content as a series of meaningfully designed moments, each with distinctive behaviours and styles. We might use alternative compositions and sizes, arrange elements using different patterns, or use horizontal and vertical scrolling to create experiences and tell stories, even when space is limited. And fortunately, we have the tools we need to do that at our disposal:

These moments might move horizontally, breaking the monotony of vertical scrolling, giving a section its own rhythm, and keeping related content together.

Make use of horizontal scrolling

My desktop design for Patty’s discography includes her album covers arranged in a modular grid. Layouts like these are easy to achieve using my modular grid generator.

But that arrangement isn’t necessarily going to work for small screens, where a practical solution is to transform the modular grid into a horizontal scrolling element. Scrolling horizontally is a familiar behaviour and a way to give grouped content its own stage, the way a magazine spread might.

I started by defining the modular grid’s parent — in this case, the imaginatively named modular-wrap — as a container:

.modular-wrap { container-type: inline-size; width: 100%; }

Then, I added grid styles to create the modular layout:

.modular { display: grid; gap: 1.5rem; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(2, 1fr); overflow-x: visible; width: 100%; }

It would be tempting to collapse those grid modules on small screens into a single column, but that would simply stack one album on top of another.

Collapsing grid modules on small screens into a single column

So instead, I used a container query to arrange the album covers horizontally and enable someone to scroll across them:

@container (max-width: 30rem) { #example-1 .modular { display: grid; gap: 1.5rem; grid-auto-columns: minmax(70%, 1fr); grid-auto-flow: column; grid-template-columns: none; grid-template-rows: 1fr; overflow-x: auto; -webkit-overflow-scrolling: touch; } } Album covers are arranged horizontally rather than vertically. See this example in my lab.

Now, Patty’s album covers are arranged horizontally rather than vertically, which forms a cohesive component while preventing people from losing their place within the overall flow of content.

Push elements off-canvas

Last time, I explained how to use shape-outside and create the illusion of text flowing around both sides of an image. You’ll often see this effect in magazines, but hardly ever online.

The illusion of text flowing around both sides of an image

Desktop displays have plenty of space available, but what about smaller ones? Well, I could remove shape-outside altogether, but if I did, I’d also lose much of this design’s personality and its effect on visual storytelling. Instead, I can retain shape-outside and place it inside a horizontally scrolling component where some of its content is off-canvas and outside the viewport.

My content is split between two divisions: the first with half the image floating right, and the second with the other half floating left. The two images join to create the illusion of a single image at the centre of the design:

<div class="content"> <div> <img src="img-left.webp" alt=""> <p><!-- ... --></p> </div> <div> <img src="img-right.webp" alt=""> <p><!-- ... --></p> </div> </div>

I knew this implementation would require a container query because I needed a parent element whose width determines when the layout should switch from static to scrolling. So, I added a section outside that content so that I could reference its width for determining when its contents should change:

<section> <div class="content"> <!-- ... --> </div> </section> section { container-type: inline-size; overflow-x: auto; position: relative; width: 100%; }

My technique involves spreading content across two equal-width divisions, and these grid column properties will apply to every screen size:

.content { display: grid; gap: 0; grid-template-columns: 1fr 1fr; width: 100%; }

Then, when the section’s width is below 48rem, I altered the width of my two columns:

@container (max-width: 48rem) { .content { grid-template-columns: 85vw 85vw; } }

Setting the width of each column to 85% — a little under viewport width — makes some of the right-hand column’s content visible, which hints that there’s more to see and encourages someone to scroll across to look at it.

Some of the right-hand column’s content is visible. See this example in my lab.

The same principle works at a larger scale, too. Instead of making small adjustments, we can turn an entire section into a miniature magazine spread that scrolls like a story in print.

Build scrollable mini-spreads

When designing for a responsive environment, there’s no reason to lose the expressiveness of a magazine-inspired layout. Instead of flattening everything into one long column, sections can behave like self-contained mini magazine spreads.

Sections can behave like self-contained mini magazine spreads.

My final shape-outside example flowed text between two photomontages. Parts of those images escaped their containers, creating depth and a layout with a distinctly editorial feel. My content contained the two images and several paragraphs:

<div class="content"> <img src="left.webp" alt=""> <img src="right.webp" alt=""> <p><!-- ... --></p> <p><!-- ... --></p> <p><!-- ... --></p> </div>

Two images float either left or right, each with shape-outside applied so text flows between them:

.content img:nth-of-type(1) { float: left; width: 45%; shape-outside: url("left.webp"); } .spread-wrap .content img:nth-of-type(2) { float: right; width: 35%; shape-outside: url("right.webp"); }

That behaves beautifully at large screen sizes, but on smaller ones it feels cramped. To preserve the design’s essence, I used a container query to transform its layout into something different altogether.

First, I needed another parent element whose width would determine when the layout should change. So, I added a section outside so that I could reference its width and gave it a little padding and a border to help differentiate it from nearby content:

<section> <div class="content"> <!-- ... --> </div> </section> section { border: 1px solid var(--border-stroke-color); box-sizing: border-box; container-type: inline-size; overflow-x: auto; padding: 1.5rem; width: 100%; }

When the section’s width is below 48rem, I introduced a horizontal Flexbox layout:

@container (max-width: 48rem) { .content { align-items: center; display: flex; flex-wrap: nowrap; gap: 1.5rem; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; } }

And because this layout depends on a container query, I used container query units (cqi) for the width of my flexible columns:

.content > * { flex: 0 0 85cqi; min-width: 85cqi; scroll-snap-align: start; } On small screens, the layout flows from image to paragraphs to image. See this example in my lab.

Now, on small screens, the layout flows from image to paragraphs to image, with each element snapping into place as someone swipes sideways. This approach rearranges elements and, in doing so, slows someone’s reading speed by making each swipe an intentional action.

To prevent my images from distorting when flexed, I applied auto-height combined with object-fit:

.content img { display: block; flex-shrink: 0; float: none; height: auto; max-width: 100%; object-fit: contain; }

Before calling on the Flexbox order property to place the second image at the end of my small screen sequence:

.content img:nth-of-type(2) { order: 100; }

Mini-spreads like this add movement and rhythm, but orientation offers another way to shift perspective without scrolling. A simple rotation can become a cue for an entirely new composition.

Make orientation-responsive layouts

When someone rotates their phone, that shift in orientation can become a cue for a new layout. Instead of stretching a single-column design wider, we can recompose it entirely, making a landscape orientation feel like a fresh new spread.

Turning a phone sideways is an opportunity to recompose a layout.

Turning a phone sideways is an opportunity to recompose a layout, not just reflow it. When Patty’s fans rotate their phones to landscape, I don’t want the same stacked layout to simply stretch wider. Instead, I want to use that additional width to provide a different experience. This could be as easy as adding extra columns to a composition in a media query that’s applied when the device’s orientation is detected in landscape:

@media (orientation: landscape) { .content { display: grid; grid-template-columns: 1fr 1fr; } }

For the long-form content on Patty Meltt’s biography page, text flows around a polygon clip-path placed over a large faux background image. This image is inline, floated, and has its width set to 100%:

<div class="content"> <img src="patty.webp" alt=""> <!-- ... --> </div> .content > img { float: left; width: 100%; max-width: 100%; }

Then, I added shape-outside using the polygon coordinates and added a shape-margin:

.content > img { shape-outside: polygon(...); shape-margin: 1.5rem; }

I only want the text to flow around the polygon and for the image to appear in the background when a device is held in landscape, so I wrapped that rule in a query which detects the screen orientation:

@media (orientation: landscape) { .content > img { float: left; width: 100%; max-width: 100%; shape-outside: polygon(...); shape-margin: 1.5rem; } } See this example in my lab.

Those properties won’t apply when the viewport is in portrait mode.

Design stories that adapt, not layouts that collapse

Small screens don’t make design more difficult; they make it more deliberate, requiring designers to consider how to preserve a design’s personality when space is limited.

Phillip was right to ask how editorial-style design can work in a responsive environment. It does, but not by shrinking a print layout. It works when we think differently about how content flexes, shifts, and scrolls, and when a design responds not just to a device, but to how someone holds it.

The goal isn’t to mimic miniature magazines on mobile, but to capture their energy, rhythm, and sense of discovery that print does so well. Design is storytelling, and just because there’s less space to tell one, it shouldn’t mean it should make any less impact.

Getting Creative With Small Screens originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

CSS Animations That Leverage the Parent-Child Relationship

Fri, 10/24/2025 - 4:18am

Modern CSS has great ways to position and move a group of elements relative to each other, such as anchor positioning. That said, there are instances where it may be better to take up the old ways for a little animation, saving time and effort.

We’ve always been able to affect an element’s structure, like resizing and rotating it. And when we change an element’s intrinsic sizing, its children are affected, too. This is something we can use to our advantage.

Let’s say a few circles need to move towards and across one another. Something like this:

Our markup might be as simple as a <main> element that contains four child .circle elements:

<main> <div class="circle"></div> <div class="circle"></div> <div class="circle"></div> <div class="circle"></div> </main>

As far as rotating things, there are two options. We can (1) animate the <main> parent container, or (2) animate each .circle individually.

Tackling that first option is probably best because animating each .circle requires defining and setting several animations rather than a single animation. Before we do that, we ought to make sure that each .circle is contained in the <main> element and then absolutely position each one inside of it:

main { contain: layout; } .circle { position: absolute; &:nth-of-type(1){ background-color: rgb(0, 76, 255); } &:nth-of-type(2){ background-color: rgb(255, 60, 0); right: 0; } &:nth-of-type(3){ background-color: rgb(0, 128, 111); bottom: 0; } &:nth-of-type(4){ background-color: rgb(255, 238, 0); right: 0; bottom: 0; } }

If we rotate the <main> element that contains the circles, then we might create a specific .animate class just for the rotation:

/* Applied on <main> (the parent element) */ .animate { width: 0; transform: rotate(90deg); transition: width 1s, transform 1.3s; }

…and then set it on the <main> element with JavaScript when the button is clicked:

const MAIN = document.querySelector("main"); function play() { MAIN.className = ""; MAIN.offsetWidth; MAIN.className = "animate"; }

It looks like we’re animating four circles, but what we’re really doing is rotating the parent container and changing its width, which rotates and squishes all the circles in it as well:

CodePen Embed Fallback

Each .circle is fixed to a respective corner of the <main> parent with absolute positioning. When the animation is triggered in the parent element — i.e. <main> gets the .animate class when the button is clicked — the <main> width shrinks and it rotates 90deg. That shrinking pulls each .circle closer to the <main> element’s center, and the rotation causes the circles to switch places while passing through one another.

This approach makes for an easier animation to craft and manage for simple effects. You can even layer on the animations for each individual element for more variations, such as two squares that cross each other during the animation.

/* Applied on <main> (the parent element) */ .animate { transform: skewY(30deg) rotateY(180deg); transition: 1s transform .2s; .square { transform: skewY(30deg); transition: inherit; } } CodePen Embed Fallback

See that? The parent <main> element makes a 30deg skew and flip along the Y-axis, while the two child .square elements counter that distortion with the same skew. The result is that you see the child squares flip positions while moving away from each other.

If we want the squares to form a separation without the flip, here’s a way to do that:

/* Applied on <main> (the parent element) */ .animate { transform: skewY(30deg); transition: 1s transform .2s; .square { transform: skewY(-30deg); transition: inherit; } } CodePen Embed Fallback

This time, the <main> element is skewed 30deg, while the .square children cancel that with a -30deg skew.

Setting skew() on a parent element helps rearrange the children beyond what typical rectangular geometry allows. Any change in the parent can be complemented, countered, or cancelled by the children depending on what effect you’re looking for.

Here’s an example where scaling is involved. Notice how the <main> element’s skewY() is negated by its children and scale()s at a different value to offset it a bit.

/* Applied on <main> (the parent element) */ .animate { transform: rotate(-180deg) scale(.5) skewY(45deg) ; transition: .6s .2s; transition-property: transform, border-radius; .squares { transform: skewY(-45deg) scaleX(1.5); border-radius: 10px; transition: inherit; } } CodePen Embed Fallback

The parent element (<main>) rotates counter-clockwise (rotate(-180deg)), scales down (scale(.5)), and skews vertically (skewY(45deg)). The two children (.square) cancel the parent’s distortion by using the negative value of the parent’s skew angle (skewY(-45deg)), and scale up horizontally (scaleX(1.5)) to change from a square to a horizontal bar shape.

There are a lot of these combinations you can come up with. I’ve made a few more below where, instead of triggering the animation with a JavaScript interaction, I’ve used a <details> element that triggers the animation when it is in an [open] state once the <summary> element is clicked. And each <summary> contains an .icon child demonstrating a different animation when the <details> toggles between open and closed.

Click on a <details> to toggle it open and closed to see the animations in action.

CodePen Embed Fallback

That’s all I wanted to share — it’s easy to forget that we get some affordances for writing efficient animations if we consider how transforming a parent element intrinsically affects the size, position, and orientation. That way, for example, there’s no need to write complex animations for each individual child element, but rather leverage what the parent can do, then adjust the behavior at the child level, as needed.

CSS Animations That Leverage the Parent-Child Relationship originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

An Introduction to JavaScript Expressions

Wed, 10/22/2025 - 9:08am

Editor’s note: Mat Marquis and Andy Bell have released JavaScript for Everyone, an online course offered exclusively at Piccalilli. This post is an excerpt from the course taken specifically from a chapter all about JavaScript expressions. We’re publishing it here because we believe in this material and want to encourage folks like yourself to sign up for the course. So, please enjoy this break from our regular broadcasting to get a small taste of what you can expect from enrolling in the full JavaScript for Everyone course.

Hey, I’m Mat, but “Wilto” works too — I’m here to teach you JavaScript.

Well, not here-here; technically, I’m over at JavaScript for Everyone to teach you JavaScript. What we have here is a lesson from the JavaScript for Everyone module on lexical grammar and analysis — the process of parsing the characters that make up a script file and converting it into a sequence of discrete “input elements” (lexical tokens, line ending characters, comments, and whitespace), and how the JavaScript engine interprets those input elements.

An expression is code that, when evaluated, resolves to a value. 2 + 2 is a timeless example.

2 + 2 // result: 4

As mental models go, you could do worse than “anywhere in a script that a value is expected you can use an expression, no matter how simple or complex that expression may be:”

function numberChecker( checkedNumber ) { if( typeof checkedNumber === "number" ) { console.log( "Yep, that's a number." ); } } numberChecker( 3 ); // result: Yep, that's a number. numberChecker( 10 + 20 ); // result: Yep, that's a number. numberChecker( Math.floor( Math.random() * 20 ) / Math.floor( Math.random() * 10 ) ); // result: Yep, that's a number.

Granted, JavaScript doesn’t tend to leave much room for absolute statements. The exceptions are rare, but it isn’t the case absolutely, positively, one hundred percent of the time:

console.log( -2**1 ); // result: Uncaught SyntaxError: Unary operator used immediately before exponentiation expression. Parenthesis must be used to disambiguate operator precedence

Still, I’m willing to throw myself upon the sword of “um, actually” on this one. That way of looking at the relationship between expressions and their resulting values is heart-and-soul of the language stuff, and it’ll get you far.

Primary Expressions

There’s sort of a plot twist, here: while the above example reads to our human eyes as an example of a number, then an expression, then a complex expression, it turns out to be expressions all the way down. 3 is itself an expression — a primary expression. In the same way the first rule of Tautology Club is Tautology Club’s first rule, the number literal 3 is itself an expression that resolves in a very predictable value (psst, it’s three).

console.log( 3 ); // result: 3

Alright, so maybe that one didn’t necessarily need the illustrative snippet of code, but the point is: the additive expression 2 + 2 is, in fact, the primary expression 2 plus the primary expression 2.

Granted, the “it is what it is” nature of a primary expression is such that you won’t have much (any?) occasion to point at your display and declare “that is a primary expression,” but it does afford a little insight into how JavaScript “thinks” about values: a variable is also a primary expression, and you can mentally substitute an expression for the value it results in — in this case, the value that variable references. That’s not the only purpose of an expression (which we’ll get into in a bit) but it’s a useful shorthand for understanding expressions at their most basic level.

There’s a specific kind of primary expression that you’ll end up using a lot: the grouping operator. You may remember it from the math classes I just barely passed in high school:

console.log( 2 + 2 * 3 ); // result: 8 console.log( ( 2 + 2 ) * 3 ); // result: 12

The grouping operator (singular, I know, it kills me too) is a matched pair of parentheses used to evaluate a portion of an expression as a single unit. You can use it to override the mathematical order of operations, as seen above, but that’s not likely to be your most common use case—more often than not you’ll use grouping operators to more finely control conditional logic and improve readability:

const minValue = 0; const maxValue = 100; const theValue = 50; if( ( theValue > minValue ) && ( theValue < maxValue ) ) { // If ( the value of `theValue` is greater than that of `minValue` ) AND less than `maxValue`): console.log( "Within range." ); } // result: Within range.

Personally, I make a point of almost never excusing my dear Aunt Sally. Even when I’m working with math specifically, I frequently use parentheses just for the sake of being able to scan things quickly:

console.log( 2 + ( 2 * 3 ) ); // result: 8

This use is relatively rare, but the grouping operator can also be used to remove ambiguity in situations where you might need to specify that a given syntax is intended to be interpreted as an expression. One of them is, well, right there in your developer console.

The syntax used to initialize an object — a matched pair of curly braces — is the same as the syntax used to group statements into a block statement. Within the global scope, a pair of curly braces will be interpreted as a block statement containing a syntax that makes no sense given that context, not an object literal. That’s why punching an object literal into your developer console will result in an error:

{ "theValue" : true } // result: `Uncaught SyntaxError: unexpected token: ':'

It’s very unlikely you’ll ever run into this specific issue in your day-to-day JavaScript work, seeing as there’s usually a clear division between contexts where an expression or a statement are expected:

{ const theObject = { "theValue" : true }; }

You won’t often be creating an object literal without intending to do something with it, which means it will always be in the context where an expression is expected. It is the reason you’ll see standalone object literals wrapped in a a grouping operator throughout this course — a syntax that explicitly says “expect an expression here”:

({ "value" : true });

However, that’s not to say you’ll never need a grouping operator for disambiguation purposes. Again, not to get ahead of ourselves, but an Independently-Invoked Function Expression (IIFE), an anonymous function expression used to manage scope, relies on a grouping operator to ensure the function keyword is treated as a function expression rather than a declaration:

(function(){ // ... })(); Expressions With Side Effects

Expressions always give us back a value, in no uncertain terms. There are also expressions with side effects — expressions that result in a value and do something. For example, assigning a value to an identifier is an assignment expression. If you paste this snippet into your developer console, you’ll notice it prints 3:

theIdentifier = 3; // result: 3

The resulting value of the expression theIdentifier = 3 is the primary expression 3; classic expression stuff. That’s not what’s useful about this expression, though — the useful part is that this expression makes JavaScript aware of theIdentifier and its value (in a way we probably shouldn’t, but that’s a topic for another lesson). That variable binding is an expression and it results in a value, but that’s not really why we’re using it.

Likewise, a function call is an expression; it gets evaluated and results in a value:

function theFunction() { return 3; }; console.log( theFunction() + theFunction() ); // result: 6

We’ll get into it more once we’re in the weeds on functions themselves, but the result of calling a function that returns an expression is — you guessed it — functionally identical to working with the value that results from that expression. So far as JavaScript is concerned, a call to theFunction effectively is the simple expression 3, with the side effect of executing any code contained within the function body:

function theFunction() { console.log( "Called." ); return 3; }; console.log( theFunction() + theFunction() ); /* Result: Called. Called. 6 */

Here theFunction is evaluated twice, each time calling console.log then resulting in the simple expression 3 . Those resulting values are added together, and the result of that arithmetic expression is logged as 6.

Granted, a function call may not always result in an explicit value. I haven’t been including them in our interactive snippets here, but that’s the reason you’ll see two things in the output when you call console.log in your developer console: the logged string and undefined.

JavaScript’s built-in console.log method doesn’t return a value. When the function is called it performs its work — the logging itself. Then, because it doesn’t have a meaningful value to return, it results in undefined. There’s nothing to do with that value, but your developer console informs you of the result of that evaluation before discarding it.

Comma Operator

Speaking of throwing results away, this brings us to a uniquely weird syntax: the comma operator. A comma operator evaluates its left operand, discards the resulting value, then evaluates and results in the value of the right operand.

Based only on what you’ve learned so far in this lesson, if your first reaction is “I don’t know why I’d want an expression to do that,” odds are you’re reading it right. Let’s look at it in the context of an arithmetic expression:

console.log( ( 1, 5 + 20 ) ); // result: 25

The primary expression 1 is evaluated and the resulting value is discarded, then the additive expression 5 + 20 is evaluated, and that’s resulting value. Five plus twenty, with a few extra characters thrown in for style points and a 1 cast into the void, perhaps intended to serve as a threat to the other numbers.

And hey, notice the extra pair of parentheses there? Another example of a grouping operator used for disambiguation purposes. Without it, that comma would be interpreted as separating arguments to the console.log method — 1 and 5 + 20 — both of which would be logged to the console:

console.log( 1, 5 + 20 ); // result: 1 25

Now, including a value in an expression in a way where it could never be used for anything would be a pretty wild choice, granted. That’s why I bring up the comma operator in the context of expressions with side effects: both sides of the , operator are evaluated, even if the immediately resulting value is discarded.

Take a look at this validateResult function, which does something fairly common, mechanically speaking; depending on the value passed to it as an argument, it executes one of two functions, and ultimately returns one of two values.

For the sake of simplicity, we’re just checking to see if the value being evaluated is strictly true — if so, call the whenValid function and return the string value "Nice!". If not, call the whenInvalid function and return the string "Sorry, no good.":

function validateResult( theValue ) { function whenValid() { console.log( "Valid result." ); }; function whenInvalid() { console.warn( "Invalid result." ); }; if( theValue === true ) { whenValid(); return "Nice!"; } else { whenInvalid(); return "Sorry, no good."; } }; const resultMessage = validateResult( true ); // result: Valid result. console.log( resultMessage ); // result: "Nice!"

Nothing wrong with this. The whenValid / whenInvalid functions are called when the validateResult function is called, and the resultMessage constant is initialized with the returned string value. We’re touching on a lot of future lessons here already, so don’t sweat the details too much.

Some room for optimizations, of course — there almost always is. I’m not a fan of having multiple instances of return, which in a sufficiently large and potentially-tangled codebase can lead to increased “wait, where is that coming from” frustrations. Let’s sort that out first:

function validateResult( theValue ) { function whenValid() { console.log( "Valid result." ); }; function whenInvalid() { console.warn( "Invalid result." ); }; if( theValue === true ) { whenValid(); } else { whenInvalid(); } return theValue === true ? "Nice!" : "Sorry, no good."; }; const resultMessage = validateResult( true ); // result: Valid result. resultMessage; // result: "Nice!"

That’s a little better, but we’re still repeating ourselves with two separate checks for theValue. If our conditional logic were to be changed someday, it wouldn’t be ideal that we have to do it in two places.

The first — the if/else — exists only to call one function or the other. We now know function calls to be expressions, and what we want from those expressions are their side effects, not their resulting values (which, absent a explicit return value, would just be undefined anyway).

Because we need them evaluated and don’t care if their resulting values are discarded, we can use comma operators (and grouping operators) to sit them alongside the two simple expressions — the strings that make up the result messaging — that we do want values from:

function validateResult( theValue ) { function whenValid() { console.log( "Valid result." ); }; function whenInvalid() { console.warn( "Invalid result." ); }; return theValue === true ? ( whenValid(), "Nice!" ) : ( whenInvalid(), "Sorry, no good." ); }; const resultMessage = validateResult( true ); // result: Valid result. resultMessage; // result: "Nice!"

Lean and mean thanks to clever use of comma operators. Granted, there’s a case to be made that this is a little too clever, in that it could make this code a little more difficult to understand at a glance for anyone that might have to maintain this code after you (or, if you have a memory like mine, for your near-future self). The siren song of “I could do it with less characters” has driven more than one JavaScript developer toward the rocks of, uh, slightly more difficult maintainability. I’m in no position to talk, though. I chewed through my ropes years ago.

Between this lesson on expressions and the lesson on statements that follows it, well, that would be the whole ballgame — the entirety of JavaScript summed up, in a manner of speaking — were it not for a not-so-secret third thing. Did you know that most declarations are neither statement nor expression, despite seeming very much like statements?

Variable declarations performed with let or const, function declarations, class declarations — none of these are statements:

if( true ) let theVariable; // Result: Uncaught SyntaxError: lexical declarations can't appear in single-statement context

if is a statement that expects a statement, but what it encounters here is one of the non-statement declarations, resulting in a syntax error. Granted, you might never run into this specific example at all if you — like me — are the sort to always follow an if with a block statement, even if you’re only expecting a single statement.

I did say “one of the non-statement declarations,” though. There is, in fact, a single exception to this rule — a variable declaration using var is a statement:

if( true ) var theVariable;

That’s just a hint at the kind of weirdness you’ll find buried deep in the JavaScript machinery. 5 is an expression, sure. 0.1 * 0.1 is 0.010000000000000002, yes, absolutely. Numeric values used to access elements in an array are implicitly coerced to strings? Well, sure — they’re objects, and their indexes are their keys, and keys are strings (or Symbols). What happens if you use call() to give this a string literal value? There’s only one way to find out — two ways to find out, if you factor in strict mode.

That’s where JavaScript for Everyone is designed take you: inside JavaScript’s head. My goal is to teach you the deep magic — the how and the why of JavaScript. If you’re new to the language, you’ll walk away from this course with a foundational understanding of the language worth hundreds of hours of trial-and-error. If you’re a junior JavaScript developer, you’ll finish this course with a depth of knowledge to rival any senior.

I hope to see you there.

JavaScript for Everyone is now available and the launch price runs until midnight, October 28. Save £60 off the full price of £249 (~$289) and get it for £189 (~$220)!

Get the Course

An Introduction to JavaScript Expressions originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Building a Honeypot Field That Works

Mon, 10/20/2025 - 6:11am

Honeypots are fields that developers use to prevent spam submissions.

They still work in 2025.

So you don’t need reCAPTCHA or other annoying mechanisms.

But you got to set a couple of tricks in place so spambots can’t detect your honeypot field.

Use This

I’ve created a Honeypot component that does everything I mention below. So you can simply import and use them like this:

<script> import { Honeypot } from '@splendidlabz/svelte' </script> <Honeypot name="honeypot-name" />

Or, if you use Astro, you can do this:

--- import { Honeypot } from '@splendidlabz/svelte' --- <Honeypot name="honeypot-name" />

But since you’re reading this, I’m sure you kinda want to know what’s the necessary steps.

Preventing Bots From Detecting Honeypots

Here are two things that you must not do:

  1. Do not use <input type=hidden>.
  2. Do not hide the honeypot with inline CSS.

Bots today are already smart enough to know that these are traps — and they will skip them.

Here’s what you need to do instead:

  1. Use a text field.
  2. Hide the field with CSS that is not inline.

A simple example that would work is this:

<input class="honeypot" type="text" name="honeypot" /> <style> .honeypot { display: none; } </style>

For now, placing the <style> tag near the honeypot seems to work. But you might not want to do that in the future (more below).

Unnecessary Enhancements

You may have seen these other enhancements being used in various honeypot articles out there:

  • aria-hidden to prevent screen readers from using the field
  • autocomplete=off and tabindex="-1" to prevent the field from being selected
<input ... aria-hidden autocomplete="off" tabindex="-1" />

These aren’t necessary because display: none itself already does the things these properties are supposed to do.

Future-Proof Enhancements

Bots get smarter everyday, so I won’t discount the possibility that they can catch what we’ve created above. So, here are a few things we can do today to future-proof a honeypot:

  1. Use a legit-sounding name attribute values like website or mobile instead of obvious honeypot names like spam or honeypot.
  2. Use legit-sounding CSS class names like .form-helper instead of obvious ones like .honeypot.
  3. Put the CSS in another file so they’re further away and harder to link between the CSS and honeypot field.

The basic idea is to trick spam bot to enter into this “legit” field.

<input class="form-helper" ... name="occupation" /> <!-- Put this into your CSS file, not directly in the HTML --> <style> .form-helper { display: none; } </style>

There’s a very high chance that bots won’t be able to differentiate the honeypot field from other legit fields.

Even More Enhancements

The following enhancements need to happen on the <form> instead of a honeypot field.

The basic idea is to detect if the entry is potentially made by a human. There are many ways of doing that — and all of them require JavaScript:

  1. Detect a mousemove event somewhere.
  2. Detect a keyboard event somewhere.
  3. Ensure the the form doesn’t get filled up super duper quickly (‘cuz people don’t work that fast).

Now, the simplest way of using these (I always advocate for the simplest way I know), is to use the Form component I’ve created in Splendid Labz:

<script> import { Form, Honeypot } from '@splendidlabz/svelte' </script> <Form> <Honeypot name="honeypot" /> </Form>

If you use Astro, you need to enable JavaScript with a client directive:

--- import { Form, Honeypot } from '@splendidlabz/svelte' --- <Form client:idle> <Honeypot name="honeypot" /> </Form>

If you use vanilla JavaScript or other frameworks, you can use the preventSpam utility that does the triple checks for you:

import { preventSpam } from '@splendidlabz/utils/dom' let form = document.querySelector('form') form = preventSpam(form, { honeypotField: 'honeypot' }) form.addEventListener('submit', event => { event.preventDefault() if (form.containsSpam()) return else form.submit() })

And, if you don’t wanna use any of the above, the idea is to use JavaScript to detect if the user performed any sort of interaction on the page:

export function preventSpam( form, { honeypotField = 'honeypot', honeypotDuration = 2000 } = {} ) { const startTime = Date.now() let hasInteraction = false // Check for user interaction function checkForInteraction() { hasInteraction = true } // Listen for a couple of events to check interaction const events = ['keydown', 'mousemove', 'touchstart', 'click'] events.forEach(event => { form.addEventListener(event, checkForInteraction, { once: true }) }) // Check for spam via all the available methods form.containsSpam = function () { const fillTime = Date.now() - startTime const isTooFast = fillTime < honeypotDuration const honeypotInput = form.querySelector(`[name="${honeypotField}"]`) const hasHoneypotValue = honeypotInput?.value?.trim() const noInteraction = !hasInteraction // Clean up event listeners after use events.forEach(event => form.removeEventListener(event, checkForInteraction) ) return isTooFast || !!hasHoneypotValue || noInteraction } } Better Forms

I’m putting together a solution that will make HTML form elements much easier to use. It includes the standard elements you know, but with easy-to-use syntax and are highly accessible.

Stuff like:

  • Form
  • Honeypot
  • Text input
  • Textarea
  • Radios
  • Checkboxes
  • Switches
  • Button groups
  • etc.

Here’s a landing page if you’re interested in this. I’d be happy to share something with you as soon as I can.

Wrapping Up

There are a couple of tricks that make honeypots work today. The best way, likely, is to trick spam bots into thinking your honeypot is a real field. If you don’t want to trick bots, you can use other bot-detection mechanisms that we’ve defined above.

Hope you have learned a lot and manage to get something useful from this!

Building a Honeypot Field That Works originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

©2003 - Present Akamai Design & Development.