A gentle introduction to anchor positioning


Anchor positioning allows you to place an element on the page based on where another element is. It makes it easier to create responsive menus and tooltips with less code using only CSS. Here’s how it works.

Let’s say you have an avatar in your nav, like this:

Nav bar with two text options and one avatar

When you click the avatar, you want a menu to appear right below it. The clicking interaction can be handled with just CSS using the Popover API. But once you click, where does your menu show up?

Figuring this out typically requires some JavaScript. But now, with anchor positioning, you can accomplish this with just a few lines of CSS. Anchor positioning will use where the avatar is to determine where the menu will go.

For example, you might want to place it just below the avatar, nice and left-aligned, like this:

Nav bar with avatar's menu expanded and left aligned.

Or you can have it hang out on the side of the avatar, having a party off to the right, like this:

Nav bar with avatar's menu expanded to the right of the photo.

You can position it in a number of places, but I think that first example looks good. It’s something you’d frequently see on the web on sites and web apps. Let’s walk through the code of how to make it happen.

The first step in placing your menu is letting it know about your avatar.

A great way to think about the relationship between your avatar and your menu is to think of your menu as if it’s anchored to your avatar. With that in mind, we’ll refer to your avatar as your anchor and your menu as your target.

You’ll name your anchor by declaring an anchor-name on the avatar element. Since your avatar represents your profile and is behaving like button, let’s give it a class of profile-button.

Here’s what it looks like:

.profile-button {
  anchor-name: --profile-button;
}

Then you’ll go to your menu (your target) and declare a position-anchor where the value is the anchor-name of the anchor that you previously declared. This is what establishes the connection and tells the target about the anchor.

Let’s go to your target and declare it there:

.profile-menu {
  position-anchor: --profile-button;
}

And the final step is giving your menu an absolute or fixed position, like this:

.profile-menu {
  position-anchor: --profile-button;
  position: absolute;
}

Great, you’ve established your connection! Now you need to decide where to put your menu.

Position-area

One way to do this is to use the position-area property.

The position-area allows you to place your element on a nine-square grid where the anchor takes the center spot. The space that contains this grid is the containing block of the anchor.

Black grid with nine squares and avatar in the center labeled left, center, right, top, bottom.

You can use this grid to determine where you want to position your menu. Do you want the menu to show up on the top right? Great! You can write top right.

.profile-menu {
  position-anchor: --profile-button;
  position: absolute;
  position-area: top right;
}
Black grid with blue box on the top right and avatar in the center.

But here’s the thing about top right — it might feel like an intuitive way to describe where you want your target to be placed, but it’s actually not the best way to go about it.

Whenever possible, you want to use logical properties, not physical ones, in your CSS. That’s a more inclusive way to describe what you’re trying to do that doesn’t make assumptions about writing mode or language.

Here’s the same nine-square grid using logical properties:

Black grid with avatar in the center with labels start/block-start, center, end/block-end, end/inline-end, start/inline-start

If you rewrite your above physical property as a logical one, you’d go from top right to block-start inline-end, like this:

.profile-menu {
  position-anchor: --profile-button;
  position: absolute;
  position-area: block-start inline-end;
}
Black grid with avatar in the center and the top right block in blue with block-start inline-end in it

Do you want it to be on the bottom center of your grid? No problem, we’d write that logically as block-end center.

.profile-menu {
  position-anchor: --profile-button;
  position: absolute;
  position-area: block-end center;
}
Black grid with avatar in the center and the bottom center block being blue with block-end center written on it

In your example, you want to put the menu under the avatar, left-aligned, to create that layout you’re going for. But, in this case, there’s a problem: the menu is wider than the avatar. So when you use block-end center as your position-area value, it crosses the grid lines and pokes out like this:

Table showing values, flexbox and grid columns for Item Flow properties.

Let’s take the grid lines away and see what that would look like.

Nav bar with two text items and one avatar with expanded menu left aligned under avatar.

That’s not the look you’re going for. How do you get that clean, left-alignment instead?

You can set position-area to block-end span-inline-end. Instead of centering the element, that’ll start the position directly below the avatar and let it spill over to end of the inline direction, like this:

Table showing values, flexbox and grid columns for Item Flow properties.

And here’s the code:

.profile-menu {
  position-anchor: --profile-button;
  position: absolute;
  position-area: block-end span-inline-end;
}

Great, exactly what you want!

The main logical values you can use are start/block-start, end/block-end, start/inline-start and end/inline-end. And, if you must, you can use the physical values: left, center, right, top, bottom.

For a full list of values you can use, check out MDN’s resource here. Let’s get back to the positioning.

The positioning you set for your menu works on desktop, where you’re got plenty of space on the right for the element to spill over, but what about on mobile where your viewport is more narrow?

In mobile view, it’s probably better to do a right-align and allow the menu to spill over to the left instead. To do that, what we want is a way to tell the menu to switch to a different position when it’s run out of room.

And anchor positioning can do that! The benefit of anchor positioning is you get this responsive flexibility. It knows when there’s no room for your defined position and it’ll happily try something else. So let’s give it a different position to try when it’s out of space. To do that, you’ll use position-try.

Here’s what that looks like:

.profile-menu {
  position-anchor: --profile-button;
  position: absolute;
  position-area: block-end span-inline-end;
  position-try: block-end span-inline-start;
}

position-try allows you to give your element a new position to try, hence the name. Here’s what the results look like in action.

Anchor()

The anchor() function uses a different framework than position-area for placing your target. Whereas position-area uses the concept of a grid to place the target, anchor() is all about placing your target based on the edges of your anchor.

It can only be used within the inset properties. Inset properties allow you to control where the element is located by declaring the offsets from its default positions. They can be the physical insets, which are top , right, bottom, and left, the logical inset properties, which are inset-block-start , inset-block-end, inset-inline-start and inset-inline-end, and the short-hand inset properties, inset-block and inset-inline .

So to recreate the above effect where the left side of the avatar is aligned to the left side of the menu, you would use the left inset property and assign it to anchor(left). Now the two sides are lined up to each other. And then to line up the top of the menu with the bottom of the avatar, you would set the top inset property of the menu to anchor(bottom).

Here’s the code put together:

.profile-menu {
  position-anchor: --profile-button;
  position: absolute;
  left: anchor(left);
  top: anchor(bottom);
}

This uses physical properties, but once again, we really should be using logical properties, so let’s rewrite this.

.profile-menu {
  position-anchor: --profile-button;
  position: absolute;
  inset-inline-start: anchor(start);
  inset-block-start: anchor(end);
}

Here, left and right values are replaced with inset-inline-start and inset-block-start. For the anchor() function, there are a few logical values to pick from. start and end allow you to set the start and end of the anchor’s containing block based on whatever axis the inset property you’re using is on. You also have self-start and self-end which are based on the anchor element’s content instead of its containing block.

anchor() also allows you to be specific about what anchor you’re working with. You can pass in an optional anchor-name and be explicit about your anchor. If you did that here, here’s what it would look like:

.profile-menu {
  position-anchor: --profile-button;
  position: absolute;
  left: anchor(--profile-button left);
  top: anchor(--profile-button bottom);
}

But in this situation, you only have the one anchor, so if you don’t explicitly state it, it’ll use whatever you set as your position-anchor.

The anchor() function can also be used in the calc() function. So far, you’ve been aligning the menu with the avatar’s container which includes some padding. Let’s say you wanted to align it with just the photo minus the padding, what would you do?

One way is to use the calc() function and add the padding (which is 1.25em) to the anchor(start) value, like this:

.profile-menu {
  position-anchor: --profile-button;
  position: absolute;
  inset-inline-start: calc(anchor(start) + 1.25em);
  inset-block-start: anchor(bottom);
}

That’ll get you this result (the pink line is to illustrate alignment):

Table showing values, flexbox and grid columns for Item Flow properties.

position-area and anchor() both help you position your targets based on your anchor. Which one you use depends on your preferred mental model. Thinking of anchor positioning as a grid that you place things on can be a helpful and intuitive way of writing your code. But if you prefer to think of positioning as assigning values to the edges of your anchor, that’s fine too. They can both help you accomplish your goals.

To play around with the different ways to position your target, check out this CodePen I set up for you to experiment. Replace the position-area with your own values and see how the target element moves around. Try to put it in different places and give anchor() a spin too.

There are more things you can do with anchor positioning but the information in this blog post should get you pretty far. For more details on additional properties, checkout MDN’s resource here. You can also check out this fun game, Anchoreum, to teach you anchor positioning.

And let me know what you think of this post. Send me, Saron Yitbarek, a message on BlueSky, or reach out to our other evangelists — Jen Simmons, on Bluesky / Mastodon, and Jon Davis, on Bluesky / Mastodon. You can also follow WebKit on LinkedIn. If you find a bug or problem, please file a WebKit bug report.



Source link