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:
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:
- 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. - By using the
position: relative
we have established a containing block, scoping theanchor-name
inside. - 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 fancyz-index: 0
, allowing us to use our pseudo-element in the background viaz-index: -1
. - We remove the background from our element and then apply it to our pseudo-element.
- We add an
anchor-name
to the span, making it our anchor, and then use this name for the anchored pseudo-element’s position. - 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. - We set the inline
inset
properties using theanchor()
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:
The example shows the issue with the text-alignment and a non-matching block alignment.
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.
text-wrap-style
, but for now only text-wrap
was implemented in browsers. Jump to this sidenote’s context.
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:
There are five items with different widths that go over several lines, resizing the example shows how they have a background neatly wrapping around them.
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!
There are eight items with different widths that go over several lines, resizing the example shows how they have a background neatly wrapping around them.
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.
The left side of this example is the same as in the previous example, the right side is an element that takes the rest of the space.
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!
There are eight items that have the same width going over several lines, resizing the example shows how they have a background neatly wrapping around them.
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:
- First thing: to simplify things, we have to use
box-sizing: content-box
. Because we explicitly set thewidth
, we need to know how we size the box, so we either would add the paddings inside or not. - To handle the single-line case, we use
max-width: max-content
to limit how wide our container could get. - The important part: using
round()
with thedown
keyword to, well, round things down. - 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. - 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.
- Curiously, even if we would use
100%
, in the context ofround()
it will use the border-box of our element for100%
regardless ofborder-box
, making us manually subtract the paddings inside theround()
(and we’d need to subtract the borders if we’d have them).
Round()
ore Complex Layout with 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:
The left side of this example is the same as in the previous example, the right side is an element that 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:
-
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” viaz-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). -
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 tostatic
andinline
when we use anchor positioning. -
It is not necessary to put properties that are used for anchor positioning like
anchor-name
andposition-anchor
inside the@supports
, as they would be ignored when not supported. -
We’d want to have a fallback for our
inset
property when theanchor()
is not supported.**Important note: ** If there will be a
var()
inside theinset
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. -
Currently, the keyword inside is
auto-same
; however, it is likely it will be renamed tosame
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.
There are several text bubbles in this example, some are aligned to the left, and some — to the right. Text inside of them can wrap, but the bubbles won’t wrap around them neatly without the shrinkwrap fix.
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.
There are two fieldset elements with centered legends. The border goes to the sides of the text inside the legends, even though one of them wraps. Disabling the shrinkwrap fix shows how the borders would not go close to the text when it wraps.
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”.
There are two headers with the borders going from their sides to the edges of the sections they’re in. One of the headers wraps, and if we disable the shrinkwrap fix, the borders for this wrapping header disappear.
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.
Headers that have fleurons on the sides. When the header wraps, the fleurons are centered vertically and are close to the text.
Headers that have a single underline beneath. Without the fix, the underline would go from the edge to the edge of the container, while with the fix, it goes only to the width of the widest line.
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).
- Notification boxes — sometimes we’d want to show a notification box (also known as “toast”), which would have a border up to its content. With multiline content, things might not look good!
- Image captions — elements like copyright information, captions, and similar — can be sometimes seen in a corner of an image with a semi-opaque background. Again, right now, these have to contain hard breaks, but with this method, we could allow doing line breaks automatically.
- Blockquotes — when these have “big” quotation marks around them. A similar case to the headers with fleurons.
- Tooltips — these could contain multiple lines of text, and we’d want to limit their width, but then neatly wrap around the content, and without any dead space of the underlying background.
- Wrapping menu alongside a search — an example close to the “faking a more complex layout” case, where the menu could wrap, but the search field alongside it would like to take the rest of the space.
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!
Published on with tags: #Anchor Positioning #Future CSS #CSS Round #Experiment #CSS