Simple CSS Pattern to Dynamically Abbreviate Text
Ideally, an HTML element should be able to decide for itself the best way that it should be displayed based on the current amount of space that is available. This is the idea behind intrinsic design.
Instead of trying to design a static layout for one viewport and then force my content into that layout, I start by writing semantic HTML and then add a layer of CSS as a suggestion for my content so that the browser can decide what the best way to render the page.
Consider the sidebar layout which splits up into a sidebar and main content area based on the amount of space that is available.
The content which is placed in the main content area will actually have less space in the two column layout on certain viewports. When I place content (like a table) into this area, I want the table to have the ideal layout regardless of how wide my device or browser window currently is.
Consider the following table:
Short Header | SuperLongTableHeaderName |
---|---|
Long content which should actually take up more place | 1 |
The width of a column is determined based on the child element with the largest content. However, when the content of a column is more important than the header and the header is the cell with the largest width, the column will take up more space than necessary, potentially squeezing the content of other columns and taking up valuable real estate.
Short Header | SuperLongTableHeaderName |
---|---|
Long content which should actually take up more place | 1 |
What I want is to make the content in my table headers adjust to the amount of space available, and if there is not enough space, to show an abbreviation of the heading instead of the complete text. But if there is enough space, I want to be able to show the full column name.
Ideally, we would have content breakpoints in CSS and could do this.
Unfortunately, we do not have content breakpoints.
Because of this, I have looked into how we could do this using CSS and was inspired by the holy albatross pattern by Heydon Pickering as to how to switch content based on the width of the parent container.
My markup for this is as follows:
<span class="squishable"
aria-label="Position"
data-short="Pos" />
By using the aria-label
attribute, the full unabbreviated text
will be available to a screen reader.
I then set the content of the ::before
and ::after
pseudo-elements in my HTML
.squishable::before {
content: attr(data-short);
}
.squishable::after {
content: attr(aria-label);
}
By default, both of the texts are now visible, but what I want is to blend in one or the other based on the width that is available in the column.
To do this, I define a content breakpoint for my span as a css property
.squishable {
--squish-at: 1.5rem;
}
When the parent container is smaller than my --squish-at
value,
I want to show the .squishable::before
element. Otherwise, I
want to show the .squishable::after
element.
The trick here is to use the calc
function to derive the width
of the container (as I said before, this is inspired by the
holy albatross pattern) and to set the max-width
property.
To set the max-width
property, we need our pseudo-elements to
have block spacing and we also want to hide any overflow (e.g
when max-width
is 0
we do not want to see anything).
.squishable {
white-space: nowrap;
}
.squishable::before,
.squishable::after {
display: inline-block;
overflow: hidden;
}
Now comes the magic!
When do we want to show our abbreviation? Here we can set the
max-width
of the ::before
pseudo-element.
.squishable::before {
max-width: calc((var(--squish-at) - 100%) * 999);
}
This works because the calculation var(--squish-at) - 100%
is
only positive when the parent container is smaller than var(--squish-at)
. We then multiply it by the 999
magic number so
that it is a really big positive number and since the width
of our abbreviation is smaller than this large max-width
that
we have set, the element will definitely be visible.
The other side of the equation is that when the parent container
is larger than our var(--squish-at)
, the calculation
var(--squish-at) - 100%
will be a negative value. Because the
browser converts this negative max-width
to 0
, this means
that if the parent container is larger than var(--squish-at)
,
the abbreviation will not be visible.
We now do the same for the ::after
pseudo-element, except that
now we use 100% - var(--squish-at)
because this is the value
that is positive when the parent element is wider than our
content breakpoint.
.squishable::after {
max-width: calc((100% - var(--squish-at)) * 999);
}
And we add just a few styles for the table to make it more responsive and make it possible for the columns headers to shrink.
.squishable-table {
width: 100%;
}
.squishable-table th {
max-width: 1.5rem;
}
Because I am using CSS properties, I can set the content
breakpoint for my column based on the content that is in it.
One easy way to estimate a breakpoint is by using the ch
unit
and setting it to the number of characters in the aria-label
attribute. This isn’t always a good breakpoint, because the ch
unit is based on the width of the 0
character, but it’s a good
starting off point.
<span class="squishable"
aria-label="THN"
data-short="No." /
style="--squish-at: 25ch;">
The main point is that I can set it independently and based on the content that I am putting inside of the element.
Now the width of the table we had before can be determined based on the content of the table cells and not on the width of the table header.
Long content which should actually take up more place | 1 |
Here’s another table that you can play around with if you are in a browser and can change the width of the viewport. And here is the Codepen of the implementation.
Name | |||||
---|---|---|---|---|---|
… | … | … | … | … | … |
… | … | … | … | … | … |
… | … | … | … | … | … |
… | … | … | … | … | … |
… | … | … | … | … | … |
… | … | … | … | … | … |
… | … | … | … | … | … |