Sitemap

Animate with length keyword: min-content、max-content and fit-content

20 min readAug 18, 2025
Press enter or click to view image in full size
Animate with lenght keyword: min-content、max-content and fit-content

Introduction

相信大家對於 CSS min-content、max-content 以及 fit-content 都不陌生,透過這些新穎的設定,developers 可以讓容器的寬高設定隨著內容物佔用面積能自適應變化。透過這些 length keyword 的加持,便可以有效省卻切版以及 dynamic layout 所需要帶來的額外計算。更棒的是瀏覽器會選染的洽到十分,不會太過亦不會不及。非常適合用在內容物沒有明確寬高的情境上~

Issue

雖然說這樣的設置對切版來說便利非常,然而亦會衍生出其他問題,那便是 CSS animation 的設置就會無效化。由於 CSS animation 的設置需要有起訖明確的長度設置,瀏覽器一但無法明確得知其明確長度,便會無法起到任何作用以至於 animation 無法順利產生。

<style>
.container {
inline-size: fit-content;

transition: inline-size .2s ease;
will-change: inline-size;

@media (hover: hover) {
inline-size: 100px;
}
}
</style>

<div class="container">
...
</div>

如上例所示,筆者針對 div.container 的 inline-size 屬性設置 transition,期待 hover 行為產生時它的長度變化可以由 fit-content 轉換到 100px。不過,由於瀏覽器無法解析 fit-content 代表何樣的長度,所以導致 transition 直接被 drop,無法順利產生變化。

Solution

通常要解決的問題不外乎就是改變 div.container 的 inline-size 初始值,看看是要直接寫死通用長度或是透過 JavaScript 進行即時長度輔助運算,不管是哪一種,都需要增加 developers 額外的 effort。

有鑑於此,所以促生的 interpolate-size 這個新穎屬性,是的!只要在用上 length keyword 的地方額外設置了這個屬性,那麼瀏覽器再進行 transition 的時候便可以有效解析該元件確切的長度了~

<style>
.container {
interpolate-size: allow-keywords;

inline-size: fit-content;

transition: inline-size .2s ease;
will-change: inline-size;

@media (hover: hover) {
inline-size: 100px;
}
}
</style>

<div class="container">
...
</div>

簡單改寫先前的 code,只需額外設置 interpolate-size: allow-keywords,如此一來便能讓使用 length keyword 的容器也可以順利產生 animation 變化了。

另外,developers 也可以透過 CSS 或是 JavaScript 來偵測當前環境是否支援這個新穎的屬性。

CSS:

<style>
@supports (interpolate-size: allow-keywords) {
/* interpolate-size ready */
...
}
</style>

JavaScript:

<script type="module">
const supported = CSS.supports('interpolate-size', 'allow-keywords');

if (supported) {
/* interpolate-size ready */
...
}
</script>

Example

了解了基本用法之後,自然便可以將之訴諸於當前模組的開發上,筆者便仿照 Facebook Messenger 輸入訊息模組來進行演示。

Press enter or click to view image in full size
Facebook Messenger > input module

如上所示,這個輸入模組在 user 輸入訊息後,便會將 input field 寬度放滿,讓 user 可以更清楚的看到自己輸入的內容。這主要是透過按鈕區塊寬度的變化來製作出這樣的互動效果。

透過以下 video 可以清楚看到實際變化:

核心思想
筆者將按鈕區塊的 inline-size 設置為 fit-content,如此一來方便日後的開發與維護,完全不需要考慮按鈕個數的增減。由於產生變化的 trigger 為 input field 是否有填入內容,所以非常適合搭配 pseudo class > :placeholder-shown,透過偵測 placeholder 是否顯示來進行動態調整。

HTML & CSS:

<style>
.container {
--inline-size-normal: fit-content;
--inline-size-active: 40px;
--inline-size: var(--inline-size-normal);

inline-size: 400px;

&:not(:has(:placeholder-shown)) {
--inline-size: var(--inline-size-active);
}

.container__buttons {
interpolate-size: allow-keywords;

inline-size: var(--inline-size);
transition: inline-size .2s ease;
will-change: inline-size;
}

.container__input-field {
...
}
}
</style>

<div class="container">
<div class="container__buttons">
<button type="button">button 1</button>
<button type="button">button 2</button>
...
</div>

<div class="container__input-field">
<input type="text" value="" placeholder="Aa" />
</div>
</div>

簡單導讀以上的 code,筆者先將 inline-size 的前後變化抽出成 custom properties,並透過 &:not(:has(:placeholder-shown)) 陳述讓 placeholder 不再顯示的時候 (表示 input field 已填入文字)便改變 — inline-size 的 value,便能立馬完成 inline-size 的變化進而驅動 transition。

以上便是簡單的範例,相信可以讓大家對於使用情境有一定程度上的理解。接下來再附上完整的 HTML code 與範例,方便大家實作與理解。

<style>
.container {
--count: 5;
--gap: .5em;

/* self */
--inline-size: 400px;
--background-color: rgba(255 255 255);
--box-shadow: 0 0 1px rgba(0 0 0/.1), 0 2px 4px rgba(0 0 0/.08);

/* button */
--button-size: 40;
--button-size-with-unit: calc(var(--button-size) * 1px);
--button-icon-scale-rate: .57;
--button-icon-scale-basis: calc((var(--button-size) * var(--button-icon-scale-rate)) / 24);

--button-background-color-normal: transparent;
--button-background-color-active: rgba(242 242 242);
--button-background-color: var(--button-background-color-normal);

--icon-color: rgba(64 104 240);
--icon-collections: path('M20 4v12H8V4h12m0-2H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-8.5 9.67l1.69 2.26 2.48-3.1L19 15H9zM2 6v14c0 1.1.9 2 2 2h14v-2H4V6H2z');
--icon-sticky-note: path('M19,5v9l-5,0l0,5H5V5H19 M19,3H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h10l6-6V5C21,3.9,20.1,3,19,3z M12,14H7v-2h5V14z M17,10H7V8h10V10z');
--icon-mic: path('M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z');
--icon-add-circle: path('M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z');
--icon-video-call: path('M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4zM15 16H5V8h10v8zm-6-1h2v-2h2v-2h-2V9H9v2H7v2h2z');
--icon-games: path('M13 4v2.67l-1 1-1-1V4h2m7 7v2h-2.67l-1-1 1-1H20M6.67 11l1 1-1 1H4v-2h2.67M12 16.33l1 1V20h-2v-2.67l1-1M15 2H9v5.5l3 3 3-3V2zm7 7h-5.5l-3 3 3 3H22V9zM7.5 9H2v6h5.5l3-3-3-3zm4.5 4.5l-3 3V22h6v-5.5l-3-3z');

--button-add-display-normal: none;
--button-add-display-active: grid;
--button-add-display: var(--button-add-display-normal);

/* input */
--input-background-color: rgba(240 242 245);
--input-placeholder-color: rgba(108 110 114);
--input-color: rgba(8 8 9);

/* container__actions */
--actions-inline-size-normal: fit-content;
--actions-inline-size-active: var(--button-size-with-unit);
--actions-inline-size: var(--actions-inline-size-normal);

inline-size: var(--inline-size);
background-color: var(--background-color);
border-radius: 4em;
padding: .75em;
box-sizing: border-box;
box-shadow: var(--box-shadow);

display: flex;
gap: var(--gap);

@media (prefers-color-scheme: dark) {
--background-color: rgba(36 37 38);
--box-shadow: none;

--button-background-color-active: rgba(58 59 60);
--icon-color: rgba(47 110 237);

--input-background-color: rgba(51 51 52);
--input-placeholder-color: rgba(177 179 184);
--input-color: rgba(220 222 225);
}

@supports not (interpolate-size: allow-keywords) {
--actions-inline-size-normal: calc(
(var(--count) * var(--button-size-with-unit)) +
(var(--gap) * (var(--count) - 1))
);

&:has(:nth-child(2 of .container__actions__button:not(.container__actions__button--add-circle))) {
--count: 2;
}

&:has(:nth-child(3 of .container__actions__button:not(.container__actions__button--add-circle))) {
--count: 3;
}

&:has(:nth-child(4 of .container__actions__button:not(.container__actions__button--add-circle))) {
--count: 4;
}

&:has(:nth-child(5 of .container__actions__button:not(.container__actions__button--add-circle))) {
--count: 5;
}
}

button {
flex-shrink: 0;
font-size: 0;
appearance: none;
box-shadow: unset;
border: unset;
background: transparent;
-webkit-user-select: none;
user-select: none;
pointer-events: auto;
margin: 0;
padding: 0;
outline: 0 none;
}

&:not(:has(:placeholder-shown)) {
--actions-inline-size: var(--actions-inline-size-active);
--button-add-display: var(--button-add-display-active);

:nth-child(1 of .container__actions__button:not(.container__actions__button--add-circle)) {
visibility: hidden;
}
}

.container__actions {
interpolate-size: allow-keywords;

flex-shrink: 0;
position: relative;
inline-size: var(--actions-inline-size);
display: flex;
gap: var(--gap);
overflow: clip;

transition: inline-size .25s ease;
will-change: inline-size;

.container__actions__button {
inline-size: var(--button-size-with-unit);
aspect-ratio: 1/1;
background-color: var(--button-background-color);
border-radius: var(--button-size-with-unit);
display: grid;
place-content: center;
transition: background-color .2s ease;
will-change: background-color;

@media (hover: hover) {
&:hover {
--button-background-color: var(--button-background-color-active);
}
}

&:active {
scale: .85;
}

&::before {
content: '';
inline-size: 24px;
aspect-ratio: 1/1;
background-color: var(--icon-color);
display: block;
clip-path: var(--icon);
scale: var(--button-icon-scale-basis);
}

&.container__actions__button--add-circle {
--icon: var(--icon-add-circle);

position: absolute;
inset-inline-start: 0;
inset-block-start: 0;
display: var(--button-add-display);
}

&.container__actions__button--collections {
--icon: var(--icon-collections);
}

&.container__actions__button--sticky-note {
--icon: var(--icon-sticky-note);
}

&.container__actions__button--mic {
--icon: var(--icon-mic);
}

&.container__actions__button--video-call {
--icon: var(--icon-video-call);
}

&.container__actions__button--games {
--icon: var(--icon-games);
}
}
}

.container__form {
flex-grow: 1;
min-inline-size: 0;

.container__form__input {
inline-size: 100%;
block-size: var(--button-size-with-unit);

font-size: 16px;
color: var(--input-color);
line-height: var(--button-size-with-unit);
background-color: var(--input-background-color);

box-sizing: border-box;
display: block;
padding-inline: 1em;
border-radius: var(--button-size-with-unit);

border: 0 none;
outline: 0 none;

caret-color: var(--icon-color);

&::-webkit-input-placeholder {
color: var(--input-placeholder-color);
}

&::-moz-placeholder {
color: var(--input-placeholder-color);
}
}
}
}
</style>

<div class="container">
<div class="container__actions">
<button type="button" class="container__actions__button container__actions__button--add-circle">Add Circle</button>

<button type="button" class="container__actions__button container__actions__button--collections">Collections</button>
<button type="button" class="container__actions__button container__actions__button--sticky-note">Sticky Note</button>
<button type="button" class="container__actions__button container__actions__button--mic">Mic</button>
<button type="button" class="container__actions__button container__actions__button--video-call">Video Call</button>
<button type="button" class="container__actions__button container__actions__button--games">Games</button>
</div>

<form class="container__form">
<input type="text" class="container__form__input" placeholder="Aa" />
</form>
</div>

Conclusion

透過 CSS interpolate-size 的加持便能有效地讓瀏覽器可以針對 length keyword 進行 animation 變化,對 developers 來說著實便利不少,然而目前支援度尚未普及,所以還是得要搭配 CSS 或是 JavaScript 進行 feature detect 比較保險。(上段的完整範例便是有額外處理這些例外狀態)

以上便是筆者透過實作所做的一些分享,感謝您的閱讀亦希望以上內容對於你以及未來得我均能有所助益。 #CSS

Reference

--

--

Paul Li
Paul Li

Written by Paul Li

Paul is the lead programmer of the AMP project at Yahoo Taiwan and is always eager for modern web technologies. He is also focusing on UX for vivid user flow.

No responses yet