Animate with length 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 輸入訊息模組來進行演示。
如上所示,這個輸入模組在 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