he Shrinkwrap Problem: Possible Future Solutions

There is one old, yet unsolved, CSS problem: shrinking containers to fit the content when it automatically wraps. While not intentional, anchor positioning allows us to come closer to solving it, at least for a few cases. In this article, I’ll demonstrate how we can use anchor positioning to neatly decorate wrapping text or elements in flex or grid contexts.

pdate from 2024-04-20

There were many changes since I did write this article. I did update the examples' code for them to work, but did not yet update the code snippets in the article, as I’m waiting for the implementation to be more stable, in particular, the examples in this article require either a inside keyword inside an anchor () tag to be implemented, or the all keyword for the inset-area property. After the implementation will become more stable, I will update the examples in this article to use it.

he Problem

When different content wraps — be it text, floats, inline-blocks, flex, or grid, — if that wrapping is automatic (without hard breaks), the way CSS calculates the final width is limited. The element with wrapped items gets expanded to fill all the available space.

In CSS2 specs, this behavior is called shrink-to-fit:

shrink-to-fit width is: min(max(preferred minimum width, available width), preferred width)

Let me demonstrate the problem and the fixGo to a sidenote in the same example:

This does not wrap.

Now, this would wrap as it has an overwhelmingly-long word.

This does not wrap.

This would also wrap as it has an overwhelmingly-long word.

Align
Text-wrap

We can see how the first pink element in the example above gets the “preferred width”, and the second pink element gets the “available width”. Green elements look like they get the “preferred width” in both cases.

An issue about a similar case was opened in CSSWG GitHub all the way back in 2016. There are numerous places on the web where people askGo to a sidenote if there is any solution to this.

So far, there have been none (besides using JavaScript or fixed breakpoints in media queries).

he Inline Solution

One of the specs I can’t wait to become finalizedGo to a sidenote is Anchor Positioning. I did write a lengthy article with some of my initial experiments using it: Future CSS: Anchor Positioning.

After publishing this article, I did many other experiments and wanted to write another big article similar to the first one. However, what I present today deserves a separate article.

I’m talking about the fix that I used in the example above, which relies on how anchor positioning is applied to inline elements. Let me quote the current specs:

If the target anchor element is fragmented, the axis-aligned bounding rectangle of the fragments’ border boxes is used instead.

How does this help? First, we need to wrap our element’s content into an additional span. While the pink elements are regular <p>, the HTML for our fixed elements is this:

<p class="fixed">
  <span>
    This would also wrap as it contains
    an overwhelmingly long word.
  </span>
</p>

And the CSS that is responsible for our visuals is this:

@supports (anchor-name: --foo) { /* 1 */
  .example1 p.fixed {
    position: relative; /* 2 */
    isolation: isolate; /* 3 */
    background: none;   /* 4 */

    & > span {
      anchor-name: --span; /* 5 */
    }

    &::after {
      content: "";
      position: absolute;
      z-index: -1; /* 3 */

      position-anchor: --span; /* 5 */
      inset:
        0 /* 6 */
        calc(anchor(auto-same) - 1em); /* 7 */

      background: var(--GREEN); /* 4 */
      border-radius: inherit;
    }
  }
}

There are a few things to explain:

  1. Because we would have to override the element’s background, we need to use progressive enhancement and wrap our styles in a @supports, allowing anything that won’t support anchor positioning to get the original styles.
  2. By using the position: relative we have established a containing block, scoping the anchor-name inside.
  3. Because we would be using our pseudo-element as the background, we need to establish the stacking context. isolation: isolate in this case is just a fancy z-index: 0, allowing us to use our pseudo-element in the background via z-index: -1.
  4. We remove the background from our element and then apply it to our pseudo-element.
  5. We add an anchor-name to the span, making it our anchor, and then use this name for the anchored pseudo-element’s position.
  6. We set the block part of the inset property to use the regular positioning: after all, the only axis where we have a problem is the inline one. In the block direction, the wrapper will properly fit around its content.
  7. We set the inline inset properties using the anchor() and a calculation.

he Limitations

If you’re attentive, you could’ve noticed one issue. We “fake” the shrinking: the actual layout of the element does not change, as we are applying a purely visual effect by using the metrics of our line fragments, not changing the dimensions of any elements.

When we were aligning things to the right in the first example, we had to align the text to the right at the same time. But what if we’d want to have non-matching alignments, like with an element being on the right, but with the text inside being left-aligned?

The below example demonstrates the issue:

This is ok.

Now, this would wrap as it has an overwhelmingly-long word.

This would also wrap as it has an overwhelmingly-long word.

Text-align
Text-wrap

We can see how, because of our parent element taking all the available space, we can work strictly in its limits, with things looking good only when we have both alignment and text-align with the same values.

Regardless, I can find plenty of cases where this technique would be tremendously useful: decorations around headers (especially with text-wrap: balanceGo to a sidenote), tooltips, and any other elements where it is ok for them to not interact with the surrounding elements at an inline axis.

Worth noting that I’m currently working on another article, which could be applied to solve some limitations. Stay tuned!

ixing Flex-Like Layouts

As I mentioned in the beginning, the “shrinkwrap” problem exists not only for the normal flow, with the inline elements. It can be prominent in grids and flex layouts: whenever we have wrapping items or wrapping content over a main axis, be it because of flex-wrap: wrap or via auto-fit & auto-fill for repeating grids when these items would wrap, their container would get the maximum available width. We could not solve this in a way similar to the inline elements — unlike them, for grids and flex, we won’t have the automatically created fragments that we could use to target the items inside as a group.

However, in a few cases, we would have other methods in our toolbox to solve this.

rapping Flex Items

In the below example, we have a flex context with elements of different widths, resulting in the container expanding to fill the available space. We can, in a limited fashion, decorate it to look like having a fitted container around the items:

  • An item
  • Second item
  • Third item
  • Fourth item
  • Fifth item
Justify-content

The technique I’m using here is far from being optimalGo to a sidenote or easy to use. Why? We have to assign a unique anchor name for every item, and then calculate the position, checking the values of every element:

  inset-inline:
    min(
      anchor(--a auto-same),
      anchor(--b auto-same),
      anchor(--c auto-same),
      anchor(--d auto-same),
      anchor(--e auto-same)
    );

For the above example, we have to repeat it five times, which can make maintaining this cumbersome. But it works! What we do here is test every item and get the one that is the closest to the edge. At least, we can rely on the anchor positioning’s auto-same (to be replaced with the same in the future), defining the value once for both sides. We could even define it as the inset shorthand, but I prefer to use an inset-block: 0 here, and not do the useless comparisons, as the size in the block dimension is something we can calculate.

eturn of the Inline-Blocks

Having to define a unique ident for every list item can be limiting. For the flex context, the only wrapper available for us is the flex container, which would expand to fill the available space.

But what if we’d go back, to the times when we used to make flex-like layouts using floats and inline-blocks? Given how our core technique involves inline elements, I thought: “Let’s convert our wrapping flex items to our good old inline-blocks, and then wrap all of them with an inline span, which could be that group we could use to decorate everything!”.

It works!

  • An item
  • Second item
  • Third item
  • Fourth item
  • Fifth item
  • Sixth item
  • Seventh item
  • Eighth item
text-align

Now, we don’t have to think about the uniqueness of our elements, though by losing the flex layout we lose a lot of the convenience that came with the gap property. Oh, the mess we had to work with to make it look good when handling floats and inline-blocks!

In our case, the HTML is, uh, unconventional:

<div class="wrapper">
  <ul role="list"
    ><li role="listitem">An item</li
    ><li role="listitem">Second item</li
    ><li role="listitem">Third item</li
    ><li role="listitem">Fourth item</li
    ><li role="listitem">Fifth item</li
    ><li role="listitem">Sixth item</li
    ><li role="listitem">Seventh item</li
    ><li role="listitem">Eighth item</li
  ></ul>
</div>

The main thing to note here is that, yes, I’m wrapping the elements weirdly. I know this might be controversial, but I always considered this way of removing the HTML whitespace to be the proper way of handling inline-block layouts, compared to hacking around it with the font-size: 0.

Then, I had to use an additional wrapper around our list, as the ul itself got display: inline:

.example2-ib ul {
  display: inline;
  anchor-name: --wrapper;
}

I won’t show the code that makes the “gaps” work — as I did mention, it’s not as neat as the native gap.

I like that we can use the older techniques in combination with the new toys to achieve something unusual.

aking a More Complex Layout

Occasionally, we could want to place something on one side of these wrapping elements. For example, if we have a navigation in our header with this effect, we could want to have other non-wrapping elements alongside it.

While we cannot rely on the proper layout for this — our element would take more space in it than it would look like, — we could “fake” our layout. Again, with anchor positioning.

  • An item
  • Second item
  • Third item
  • Fourth item
  • Fifth item
  • Sixth item
  • Seventh item
  • Eighth item
This element takes the rest of the space.
text-align

We can see how we’re limited again by the elements’ alignment. All because we rely on the position of elements in their container, which gets the whole available width.

The CSS I added to this example is as follows:

.fake-layout ul::after {
  anchor-name: --list;
}

.fake-layout .fill-available {
  position: absolute;
  inset: 0;
  inset-inline-start: anchor(--list end);
  margin-left: 1rem;
}

We added an anchor name to the pseudo-element that works as our “background”, and then positioned our additional content on the left edge, starting from where this pseudo-element ends.

Note that our secondary column could not get higher than the content in the left one, as our element does not participate in the layout — it is positioned absolutely, and the only thing it knows is our decorative list wrapper.

rids with Fixed Column Width

When I mentioned grids, the only case that we could solve is the one when we have all columns with an equal width. And — we have to implement this grid via display: flex.

The reason: we cannot make a grid work with max-width: max-content for the single line case, due to the way repeat() with auto-fill or auto-fit contributes to the container’s intrinsic size. On the other hand, we could use this method sooner than the one using anchor positioning!

  • An item
  • Second item
  • Third item
  • Fourth item
  • Fifth item
  • Sixth item
  • Seventh item
  • Eighth item

The solution here is the round() function!

  box-sizing: content-box; /* 1 */
  max-width: max-content;  /* 2 */
  width: calc(
    round(down, /* 3 */
      100cqi /* 4 */
      +
      var(--gap) /* 5 */
      -
      2 * var(--padding) /* 6 */
      ,
      var(--column-width)
      +
      var(--gap) /* 5 */
    )
    -
    var(--gap) /* 5 */
  );

For the final calculation, we need a bunch of moving parts:

  1. First thing: to simplify things, we have to use box-sizing: content-box. Because we explicitly set the width, we need to know how we size the box, so we either would add the paddings inside or not.
  2. To handle the single-line case, we use max-width: max-content to limit how wide our container could get.
  3. The important part: using round() with the down keyword to, well, round things down.
  4. Notable thing: we’re using container query length units here, and have to set up a container around our wrapper, as otherwise 100% won’t work in Safari, at least for now.
  5. The basic calculation is: we want to calculate how many times our elements would fit into our box, and include the gap in the calculation. Because the number of gaps is one fewer than the number of items, we would like to round things by the sum of the column width and gap, and then subtract one gap at the end.
  6. Curiously, even if we would use 100%, in the context of round() it will use the border-box of our element for 100% regardless of border-box, making us manually subtract the paddings inside the round() (and we’d need to subtract the borders if we’d have them).

ore Complex Layout with Round()

Because in the above example, we’re not using anchor positioning and are calculating the width via round(), we can use a similar calculation to have a proper layout:

  • An item
  • Second item
  • Third item
  • Fourth item
  • Fifth item
  • Sixth item
  • Seventh item
  • Eighth item
This element takes the rest of the space.

Because the round() produces the proper length, we can modify the grid to be auto 1fr — making the rounded left part get its width calculated, and then the area that should fill the available space to rely only on 1fr to do its job.

One modification to the definition of our rounded width: we had to define the “proportions” by modifying the “rounded” width to have the initial value of 2/3 of the container:

.complex-layout ul {
  width: calc(
    round(down,
      2 * 100cqi / 3
      +
      var(--gap)
      -
      2 * var(--padding)
      ,
      var(--column-width)
      +
      var(--gap)
    )
    -
    var(--gap)
  );
}

Fun fact: this section was updated on December 8th — the original version of this example was overcomplicated, and not working correctly.

he Basic Setup with the Fallback

One issue with anchor positioning for inline elements is that it might be tricky to get the graceful degradation right. Because we’re faking the way the background works by separating it into separate elements, things might get weirdGo to a sidenote.

The best approach I found for handling this is to define our styles as progressive enhancement. How would you want the element to look when anchor positioning is not applied? Style the element this way, then add the @supports and modify things to use anchor-positioning later.

One convenient thing to do is to make our anchor target the positioning context, allowing us to continue using the extra element as the background. Then, instead of using additional anchors, it would rely on the target itself for regular positioning. The final “basic” styles I would be using for the later examples are similar to this:

.shrinkwrap {
  position: relative; /* 1 */
  isolation: isolate; /* 1 */
}

.shrinkwrap-target {
  position: relative;    /* 2 */
  display: inline-block; /* 2 */
  anchor-name: --target; /* 3 */
}

.shrinkwrap-target::before { /* 2 */
  content: "";
  position: absolute;
  z-index: -1;              /* 1 */
  position-anchor: --target; /* 3 */
  inset: 0; /* 4 */
  inset-inline: anchor(auto-same); /* 5 */
}

@supports (anchor-name: --foo) {
  .shrinkwrap-target {
    position: static; /* 2 */
    display: inline;  /* 2 */
  }
}

A few notes:

  1. We’re using the parent node as our root positioning context, scoping our nested anchor-names, and providing an isolation for any z-index we’re using (like for putting our element into the “background” via z-index: -1).

    The root positioning context can also be useful for using the non-anchored coordinates when using anchor positioning, as we cannot apply position: relative to our target without losing an ability to anchor things to it — a quick that, I hope, will get changed (I’ll put a link to an issue about this here later).

  2. Without anchor positioning, the context for our background element would be the target, so we’d make it an inline-block, and add position: relative. However, we’d want to reset both of these to static and inline when we use anchor positioning.

  3. It is not necessary to put properties that are used for anchor positioning like anchor-name and position-anchor inside the @supports, as they would be ignored when not supported.

  4. We’d want to have a fallback for our inset property when the anchor() is not supported.

    **Important note: ** If there will be a var() inside the inset for anchor positioning, we would have to move the whole declaration inside a @supports, as using the CSS variables would make browsers think that the declaration is “supported” and the fallback mechanism won’t work.

  5. Currently, the keyword inside is auto-same; however, it is likely it will be renamed to same in the future.

se Cases

I can think of many use cases for all the above, I will provide a few that I did remember from the top of my head, and I will leave the rest for you to experiment on.

hat Bubbles

The initial demos in this article did already look like them, but to re-iterate in a more obvious way: bubbles in various message apps can sometimes have this style. I remember doing a custom CSS theme for Adium, and stumbling upon the shrinkwrap issue, where I wanted the messages to be short and wrap the content. Now I know how to do it! Or — will know what to do in the future when anchor positioning will be available everywhere. But not in the past.

Hello, there!

Oh hey, this is a bubble with a lot of text, so it would wrap, hopefully!

This is cool! I like how the bubbles go neatly around the wrapped text.

We can even continue using anchor positioning to attach things to this “fake” background, like the emoji on the previous message!

Debug

egends and Headings

Oh hey, this is a callback to my old post here with the same name!

egends

The idea of that older experiment was to have a <legend> inside a <fieldset> or a heading that would have lines around it. In the case of a legend, we could “emulate” the position of it in the center of its fieldset, which is not possible with regular means.

Some text inside a centered legend that should span over two lines

Just some content inside a fieldset

Not enough content to wrap

The legend won't wrap (at least at the default desktop browser width, and a non-resized example).

Debug

eadings

Both fieldsets and headings did use inline-blocks to emulate the borders that go from the edges of text, and for wrapping text I had to add a <br /> to avoid the shrinkwrap problem. This time, it is absolute positioning, though for some time I thought that if I had to implement this method once more, I’d do it via flex or grid — but with them, we couldn’t solve the “shrinkwrap”.

Some text inside a centered h5 that should span over two lines

Just some paragraph

Not enough content to wrap

The header won't wrap (at least at the default desktop browser width, and a non-resized example).

Debug

ven More Heading Decorations

These are simple-looking — I’m not a designer — but I hope they will demonstrate the ideas that we can implement with this technique.

Some text inside a centered h5 that should span over two lines

Not enough content to wrap

Debug

Some text inside a centered h5 that should span over two lines

Not enough content to wrap

Debug

There might be many other ways we could style the headers, I feel like I only scratched the surface with these simple examples. If you’re a designer and have an idea for some header style that you might think could be achieved with this technique — throw it at me!

ther Use Cases (Without Examples)

I could continue creating demos, but at this point, the article is long enough! Instead, if you’re curious, I recommend trying this method yourself and implementing these as an experiment (not in production, of course).

he Future

I’m not sure if we will ever get a proper way to handle this. Certain cases could be simplified, and potentially several of them could be solved in a manner similar to the text-wrap: balance, where we could limit the number of lines covered by the algorithm, simplifying things without a big impact on performance.

For now, a place to monitor would be the CSSWG How to shrink to fit the width? issue. If you have any other use cases that involve the “shrinkwrap” — provide them in this issue!


Let me know what you think about this article on Mastodon!