回顾 2024 Google I/O 大会:CSS 和 Web UI 最新动态

2024 年 Google I/O 大会已经结束很久了,一直拖到今天才着手写这篇文章。似乎现在有了一个不成文的惯例,每年 Google I/O 大会结束,都会有一篇关于"CSS 和 Web UI 最新动态" 的文章发布(2022年2023年2024年)。我个人对该话题也是非常感兴趣的。今天我们就来聊聊 CSS 和 Web UI 在 2024 年 Google I/O 大会上与 2023 年有哪些差异。

先上一张图:

上图中标红的是在 2023 年 Google I/O 大会上未提及的话题。

我的小册《 现代 Web 布局 》、《 现代 CSS 》和《 Web 动画之旅 》覆盖了上图中提到 99% 的知识点,并以图文并茂以及案例的形式向读者阐述了这些话题。如果你对这方面知识感兴趣,或者想深入了解这些知识,那么这两本小册将对你有所帮助!

广告打完之后,我们就开始进入今天的话题吧!

滚动驱动动画

滚动驱动动效是 Web 上常见的一种用户交互模式。在过去,Web 开发者要么使用 JavaScript 的滚动事件(scroll),要么使用像 Intersection Observer API 这样的东西,要么使用像 GreenSock 动画库来实现它。如今,W3C 的 CSS 工作小组为滚动驱动动效提供相应的规范(Scroll-driven Animations),可以将滚动作为 CSS 动画的时间轴。这意味着,当你向上或向下滚动时,CSS 动画会向前或向后播放。

CSS 滚动驱动动效是一种强大的工具,使 Web 设计师和开发人员能够在用户滚动页面时呈现出动态而引人注目的动画效果。通过巧妙地运用 CSS 的动画、过渡视图过渡路径动画等特性,可以实现各种独特的滚动驱动动效,例如视差滚动、滚动进度指示器,以及在进入视图时显示的图片可以具有淡入淡出的效果。为用户提供更加生动和丰富的浏览体验。

它与基于时间的动画在客户端上的运作方式类似,你现在可以使用滚动条的滚动进度来启动、暂停和反向播放动画。因此,当你向前滚动时,你会看到动画进度;如果向后滚动,则会看到动画播放进度相反的情况。这样,你就可以使用在视口内或视口内呈现动画效果的元素制作部分或整页的视觉效果,也称为"滚动式展示",以获得动态的视觉冲击。滚动条驱动的动画可以突出显示重要内容、引导用户浏览故事,或者只是为 Web 页面添加动画元素。

Demo 地址:codepen.io/airen/full/...

你会发现,中间列和其两侧列滚动方向刚好相反。使用滚动进度时间轴,可以很容易就实现上图的效果:

HTML 复制代码
<div class="columns">
    <div class="column column-reverse">
        <figure>
            <img src="thumbnail.jpg" alt="" />
            <figcaption>Gnostic Will 2012</figcaption>
        </figure>
    </div>
    <div class="column">
        <figure>
            <img src="thumbnail.jpg" alt="" />
            <figcaption>Gnostic Will 2012</figcaption>
        </figure>
    </div>
    <div class="column column-reverse">
        <figure>
            <img src="thumbnail.jpg" alt="" />
            <figcaption>Gnostic Will 2012</figcaption>
        </figure>
    </div>
</div>

核心 CSS 代码:

CSS 复制代码
@layer animation {
    .columns {
        overflow-y: hidden;
        scroll-snap-type: y mandatory;
    
        & figure {
            scroll-snap-align: start;
            scroll-snap-stop: always;
        }
    }

    .column-reverse {
        flex-direction: column-reverse;
    }

    @keyframes adjust-position {
        from {
            transform: translateY(calc(-100% + 100vh));
        }
    
        to {
            transform: translateY(calc(100% - 100vh));
        }
    }

    .column-reverse {
        animation: adjust-position linear forwards;
        animation-timeline: scroll(root block);
    }
}

动画由滚动位置驱动。如需创建此效果,请先设置 CSS 动画,然后再设置 animation-timeline 。在这个示例中,我们使用的是 scroll() 函数,该函数创建了一个匿名滚动时间轴,其参数 (root block) 会将文档视口作为滚动容器。

滚动驱动的动画可能意味着全屏滚动讲述体验,但也可能意味着更精细的动画,例如在你滚动 Web 应用时,可以根据滚动方向和滚动的速度来调整元素样式。这使得 Web 开发在创造动画的时候更为灵活和便利,甚至以前我们无法使用纯 CSS 实现的动画效果,现在可以轻易就实现,而且效果甚至比依赖于 JavaScript 脚本实现的效果还好。例如下面这个进度条指示器效果,用户向下滚动页面时,小鸡向前(向右)跑;用户向上滚动页面时,小鸡掉转方向向后(向左)跑。

Demo 地址:codepen.io/bramus/full...

CSS 复制代码
@property --scroll-position {
    syntax: "<number>";
    inherits: true;
    initial-value: 0;
}
@property --scroll-position-delayed {
    syntax: "<number>";
    inherits: true;
    initial-value: 0;
}

@keyframes adjust-pos {
    to {
        --scroll-position: 1;
        --scroll-position-delayed: 1;
    }
}

:root {
    animation: adjust-pos linear both;
    animation-timeline: scroll(root);
}

body {
    transition: --scroll-position-delayed 0.15s linear;
    --scroll-velocity: calc(var(--scroll-position) - var(--scroll-position-delayed));
    --scroll-speed: abs(var(--scroll-velocity));
    --scroll-direction: sign(var(--scroll-velocity));

    --when-scrolling: abs(var(--scroll-direction));
    --when-not-scrolling: abs(var(--when-scrolling) - 1);

    --when-scrolling-up: min(abs(var(--scroll-direction) - abs(var(--scroll-direction))),        1);
    --when-scrolling-down: min(var(--scroll-direction) + abs(var(--scroll-direction)), 1);

    --when-scrolling-down-or-when-not-scrolling: clamp(0, var(--scroll-direction) + 1, 1);
    --when-scrolling-up-or-when-not-scrolling: clamp(0, abs(var(--scroll-direction) - 1), 1);
}

@supports not (transform: scaleX(sign(-1))) {
    body {
        --scroll-speed: max(var(--scroll-velocity), -1 * var(--scroll-velocity));
        --scroll-direction: calc(var(--scroll-velocity) / var(--scroll-speed));
    }
}

.chicky-walk {
    /* css */
    @supports (animation-timeline: view()) {
        animation-direction: alternate;
        animation-timing-function: linear;
        animation-name: moveChicky;
        animation-timeline: scroll(root);
    }
}
.leg-left {
    transform-origin: top center;
    animation-duration: 1s;
    animation-direction: normal;
    animation-timing-function: linear;
    animation-name: moveLegLeft;
    animation-timeline: scroll(root);
}

.leg-right {
    transform-origin: top center;
    animation-duration: 1s;
    animation-direction: normal;
    animation-timing-function: linear;
    animation-name: moveLegRight;
    animation-timeline: scroll(root);
}

.eggshell {
    display: none;
    @supports (scroll-timeline: works) {
        display: block;
        animation-duration: 1s;
        animation-direction: normal;
        animation-name: eggShell;
        animation-timeline: scroll(root);
        z-index: 10;
    }
}
.chick-in-shell {
    animation-duration: 1s;
    animation-direction: normal;
    animation-name: shellChicky;
    animation-timeline: scroll(root);
}
.victory-chicky {
    animation-duration: 1s;
    animation-direction: normal;
    animation-name: victoryChicky;
    animation-timeline: scroll(root);
}

.chicky-eye {
    transform-origin: center;
    animation: blink 8s infinite;
    @media (prefers-reduced-motion) {
        animation: none;
    }
}

@keyframes moveChicky {
    0%,2% {
        opacity: 0;
        transform: translateX(80px) scaleX(calc(var(--scroll-direction) + var(--when-not-scrolling)));
    }
    3% {
        opacity: 1;
        transform: translateX(80px) scaleX(calc(var(--scroll-direction) + var(--when-not-scrolling)));
    }
    92% {
        opacity: 1;
        transform: translateX(100vw) scaleX(calc(var(--scroll-direction) + var(--when-not-scrolling)));
    }
    93%, 100% {
        opacity: 0;
        transform: translateX(100vw) scaleX(calc(var(--scroll-direction) + var(--when-not-scrolling)));
    }
}

@keyframes shellChicky {
    0%,2% {
        opacity: 1;
    }
    3%, 100% {
        opacity: 0;
    }
}

@keyframes eggShell {
    0%, 2% {
        opacity: 0;
    }
    3%, 100% {
        opacity: 1;
    }
    8%, 100% {
        transform: translateY(100%);
        opacity: 1;
    }
}

@keyframes victoryChicky {
    0%, 93% {
        opacity: 0;
    }
    94%, 100% {
        opacity: 1;
    }
}

@keyframes blink {
    0%,7.5%,12.5%,100% {
        opacity: 1;
    }
    5%,10% {
        opacity: 0.2;
    }
}

@keyframes moveLegRight {
    0%,10%, 20%,30%,40%,50%,60%,70%,80%,90%,100% {
        transform: translate(0) rotate(0);
    }
    2.5%,12.5%,22.5%,32.5%,42.5%,52.5%,62.5%,72.5%, 82.5%,92.5% {
        transform: translate(-4px, 2px) rotate(0);
    }
    5%, 15%,25%,35%,45%,55%,65%,75%,85%,95% {
        transform: translate(-15px, 5px) rotate(4deg);
    }
    7.5%,17.5%,27.5%,37.5%,47.5%, 57.5%, 67.5%,77.5%,87.5%,97.5% {
        transform: translate(-4px, 2px) rotate(0);
    }
}

@keyframes moveLegLeft {
    0%,10%,20%,30%,40%,50%,60%,70%,80%,90%,100% {
        transform: rotate(0);
    }
    2.5%,12.5%,22.5%,32.5%,42.5%,52.5%,62.5%,72.5%,82.5%,92.5% {
        transform: rotate(5deg);
    }
    5%,15%,25%,35%,45%,55%,65%,75%,85%,95% {
        transform: rotate(0);
    }
    7.5%,17.5%,27.5%,37.5%,47.5%,57.5%,67.5%,77.5%,87.5%,97.5% {
        transform: rotate(-5deg);
    }
}

注意,在这里还结合了其他的一些 CSS 特性:

  • CSS 自定义属性 @property :注册两个类型为 <number> 的自定义属性,使它们能够在动画中使用

  • 滚动驱动动效:在滚动时对这些自定义属性进行动画处理。

  • 过渡延迟时间 transition-delay :延迟子元素上的第二个自定义属性的计算。

  • calc() :计算两个数值之间的差异,得到滚动速度(滚动速度 = 速度 + 方向)。

  • sign() :从速度中提取滚动方向,得到值为 10-1

  • abs() :从速度中提取滚动速度。

正如你所看到的。使用 CSS 滚动驱动动画,你无需使用任何 JavaScript 脚本,这意味着它具备着某些显著的性能优势。因为,无论是直接在 CSS 中使用新 API 还是使用 JavaScript 钩子,滚动驱动的动画在为可在合成器上添加动画的属性(如变形和不透明度)添加动画效果时,会在主线程以外工作。

我们知道这些影响将继续使 Web 更具吸引力,在未来还会有更多滚动驱动动画的功能出现。比如,可以使用滚动点来触发动画的开始播放(称为滚动触发的动画)。下面这个示例,它使用 CSS scroll-start-target 在选择器中设置初始日期和时间,并使用 JavaScript scrollsnapchange 事件更新标头日期,从而轻松地将数据与贴靠事件同步。

Demo 地址:codepen.io/argyleink/f... (请使用 Chrome Canary 浏览器查看)

如需详细了解如何开始使用滚动条驱动的动画请阅读《CSS 滚动驱动动效的艺术》一文!在这里,你将获得滚动驱动动画基础知识,包括该功能的运作方式和各种效果创建方式,以及如何组合效果以打造丰富的体验。

视图过渡

我们刚刚介绍了在 Web 内添加滚动驱动动画的强大新功能,除此之外,还有一项名为"视图转换"的强大新功能,用于在 Web 浏览之间添加动画效果,从而打造无缝的用户体验。视图过渡为 Web 带来了全新的流畅性,让你可以在单个页面甚至不同页面的不同视图之间创建无缝过渡。

Demo 地址:astro-movies.pages.dev/

这些全屏显示的效果既美观又顺畅,但你也可以创建微互动,比如这个示例中点击卡片上的删除按钮,将会从卡片列表中删除卡片,卡片在删除的过程中会有一个过渡的动画效果。通过视图过渡可以轻松实现这种效果。

Demo 地址:codepen.io/airen/full/...

在单页应用中快速启用视图过渡的方法非常简单,只需使用 document.startViewTransition 封装交互,并确保每个过渡元素都有 view-transition-name、内嵌或使用 JavaScript 动态地创建 DOM 节点即可。

Demo 地址:codepen.io/airen/full/...

这是 Web 上一个经典的组件,以往要实现这样的效果是需要依赖复杂的 JavaScript 脚本的。现在使用 CSS View Transitions API 构建的话,只需要几行 CSS 代码和一点 JavaScript 代码即可。

实现上面这个示例,你需要类似下面这样的 HTML 结构:

HTML 复制代码
<figure>
    <img src="https://picsum.photos/1920/768?random=1" alt="" class="b">
    <figcaption>Decorate tree</figcaption>
</figure>
<ul class="thumbnails">
    <li>
        <img style="--index: 0;" class="thumbnail" src="https://picsum.photos/1920/768?random=1" alt="Decorate tree" />
    </li>
    <!-- 省略其他列表项 -->
</ul>

和上面那个案例相似,我们在 document.startViewTransition() 方法中使 figure 重新显示图片 displayNewImage()

JavaScript 复制代码
const thumbnails = document.querySelector(".thumbnails");
const mainImage = document.querySelector("figure img");
const imageHeading = document.querySelector("figcaption");

const thumbnailHandler = (event) => {
    const clickTarget = event.target;
  
    const displayNewImage = () => {
        mainImage.src = clickTarget.src;
        imageHeading.textContent = clickTarget.alt;
        document.documentElement.style.setProperty(
            "--originPosUnit",
            `${getComputedStyle(clickTarget).getPropertyValue("--index") * 25 + 12.5}%`
        );
    };

    if (clickTarget.classList.contains("thumbnail")) {
        if (!document.startViewTransition) {
            displayNewImage();
            return;
        }
     
        const transition = document.startViewTransition(() => displayNewImage());
    }
};

thumbnails.addEventListener("click", thumbnailHandler, false);

可以使用 view-transition-name 属性给视图过渡提供一个独特的标识名称,并且将其当作 ::view-transition-old()::view-transition-new() 伪元素来匹配 view-transition-name 指定的视图转换组。

CSS 复制代码
@layer transitions {
    :root {
        --originPosUnit: 25%;
    }

    @keyframes grow {
        from {
            scale: 0;
        }
        to {
            scale: 1;
        }
    }

    figure {
        view-transition-name: figure;
    }
  
    figcaption {
        view-transition-name: figureCaption;
    }

    ::view-transition-old(figure),
    ::view-transition-new(figure) {
        transform-origin: 100% var(--originPosUnit);
    }

    ::view-transition-new(figure) {
        animation: 400ms ease-out both grow;
    }
}

视图过渡名称可用于将自定义动画应用于视图过渡,但这对于许多元素过渡而言可能会很麻烦。今年,视图过渡的第一次更新简化了此问题,并引入了创建可应用于自定义动画的视图过渡类的功能。假设你的视图过渡包含许多卡片,但页面上还设置了标题。若要为除标题以外的所有卡片添加动画效果,你必须编写一个选择器,用于定位每张卡片。

CSS 复制代码
h1 {
    view-transition-name: title;
}
::view-transition-group(title) {
    animation-timing-function: ease-in-out;
}

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
...
#card20 { view-transition-name: card20; }

::view-transition-group(card1),
::view-transition-group(card2),
::view-transition-group(card3),
::view-transition-group(card4),
...
::view-transition-group(card20) {
    animation-timing-function: var(--bounce);
}

有 20 个元素?你需要编写 20 个选择器。添加新元素?然后,你还需要扩大应用动画样式的选择器。可扩展性不强。view-transition-class 可在视图转换伪元素中使用,以应用相同的样式规则。

CSS 复制代码
#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
#card5 { view-transition-name: card5; }
...
#card20 { view-transition-name: card20; }

#cards-wrapper > div {
    view-transition-class: card;
}
html::view-transition-group(.card) {
    animation-timing-function: var(--bounce);
}

以下卡片示例使用了之前的 CSS 代码段。所有卡片(包括新添加的卡片)都可通过以下 1 个选择器应用相同的时间设置:html::view-transition-group(.card)

HTML 复制代码
<button class="add-btn">
    <span class="sr-only">Add</span>
</button>

<template id="card">
    <li class="card">
        <button class="delete-btn">
            <span class="sr-only">Delete</span>
        </button>
    </li>
</template>

<ul class="cards">
    <li class="card" style="view-transition-name: card-1; background-color: tan;">
        <button class="delete-btn">
            <span class="sr-only">Delete</span>
        </button>
    </li>
    <li class="card" style="view-transition-name: card-2; background-color: khaki;">
        <button class="delete-btn">
            <span class="sr-only">Delete</span>
        </button>
    </li>
    <!-- 省略其他的 li -->
</ul>

请注意,上面代码中 listyle 中的 view-transition-name ,每张卡片都通过 view-transition-name 设置了一个名称,例如 card-1card-2 ,依此类推。

CSS 复制代码
@layer view-transitions {
    @layer no-root {
        :root {
            view-transition-name: none;
        }
        ::view-transition {
            pointer-events: none;
        }
    }

    @layer reorder-cards {
        @supports (view-transition-class: card) {
            .card {
                view-transition-class: card;
            }
    
            ::view-transition-group(*.card) {
                animation-timing-function: var(--bounce-easing);
                animation-duration: 0.5s;
            }
        }
    }

    @layer add-card {
        @keyframes animate-in {
            0% {
                opacity: 0;
                translate: 0 -200px;
            }
            100% {
                opacity: 1;
                translate: 0 0;
            }
        }
    
        ::view-transition-new(targeted-card):only-child {
            animation: animate-in ease-in 0.25s;
        }
    }

    @layer remove-card {
        @keyframes animate-out {
            0% {
                opacity: 1;
                translate: 0 0;
            }
            100% {
                opacity: 0;
                translate: 0 -200px;
            }
        }
    
        ::view-transition-old(targeted-card):only-child {
            animation: animate-out ease-out 0.5s;
        }
    }
}
JavaScript 复制代码
document.querySelector(".cards").addEventListener("click", (e) => {
    if (e.target.classList.contains("delete-btn")) {
        if (!document.startViewTransition) {
            e.target.parentElement.remove();
            return;
        }
    
        e.target.parentElement.style.viewTransitionName = "targeted-card";
        document.startViewTransition(() => {
            e.target.parentElement.remove();
        });
    }
});

document.querySelector(".add-btn").addEventListener("click", async (e) => {
    const template = document.getElementById("card");

    const $newCard = template.content.cloneNode(true);

    if (!document.startViewTransition) {
        document.querySelector(".cards").appendChild($newCard);
        return;
    }

    $newCard.firstElementChild.style.viewTransitionName = "targeted-card";
    $newCard.firstElementChild.style.backgroundColor = `#${Math.floor(Math.random() * 16777215).toString(16)}`;
  
    const transition = document.startViewTransition(() => {
        document.querySelector(".cards").appendChild($newCard);
    });

    await transition.finished;

    const rand = window.performance.now().toString().replace(".", "_") + Math.floor(Math.random() * 1000);
    document.querySelector(".cards .card:last-child").style.viewTransitionName = `card-${rand}`;
});

Demo 地址:codepen.io/airen/full/...

视图过渡的另一大改进是支持视图过渡类型。当你在页面视图之间以动画形式呈现动画效果时,如果想要另一种视觉视图过渡效果,视图过渡类型非常有用。

例如,你可能希望首页以不同于博客页面动画返回到首页的方式以动画形式呈现博客页面。或者,你可能希望页面以不同方式来回切换,就像本例中所示,即从左向右或从左到右切换。之前这样做会很麻烦。你可以向 DOM 添加类以应用样式,然后必须移除这些类。view-transition-types 使浏览器可以清理旧转换,而无需在启动新转换之前手动清理,从而为你代劳。

Demo 地址:codepen.io/airen/full/...

你可以在 document.startViewTransition 函数中设置类型,该函数现在接受一个对象。update 是更新 DOM 的回调函数,types 是包含这些类型的数组。

JavaScript 复制代码
document.startViewTransition({
    update: myUpdate,
    types: ['slide', 'forwards']
})

你还可以将视图过渡用于多页应用。与同文档视图过渡的主要区别之一是,你不需要使用 document.startViewTransition() 封装过渡。而是使用 CSS @view-transition 来选择启用视图过渡所涉及的两个页面。

CSS 复制代码
@view-transition {
    navigation: auto;
}

如需自定义程度更高的效果,你可以使用新的 pageswappagereveal 事件监听器,它们允许你访问视图转换对象。借助 pageswap,你可以在获取旧快照之前,在传出网页上进行一些最后一刻的更改;使用 pagereveal,你可以在新页面初始化后开始渲染之前对其进行自定义。

JavaScript 复制代码
window.addEventListener('pageswap', async (e) => {
    if (e.viewTransition) {
        const currentUrl = e.activation.from?.url ? new URL(e.activation.from.url) : null;
        const targetUrl = new URL(e.activation.entry.url);

        if (!targetUrl.pathname.startsWith(basePath)) {
            e.viewTransition.skipTransition();
        }

        if (isProfilePage(currentUrl) && isHomePage(targetUrl)) {
            setTemporaryViewTransitionNames([
                [document.querySelector(`#detail main h1`), 'name'],
                [document.querySelector(`#detail main img`), 'avatar'],
            ], e.viewTransition.finished);
        }

        if (isProfilePage(targetUrl)) {
            const profile = extractProfileNameFromUrl(targetUrl);
            
            setTemporaryViewTransitionNames([
                [document.querySelector(`#${profile} span`), 'name'],
                [document.querySelector(`#${profile} img`), 'avatar'],
            ], e.viewTransition.finished);
        }
    }
});

window.addEventListener('pagereveal', async (e) => {
    if (!navigation.activation.from) return;

    if (e.viewTransition) {
        const fromUrl = new URL(navigation.activation.from.url);
        const currentUrl = new URL(navigation.activation.entry.url);

        if (!fromUrl.pathname.startsWith(basePath)) {
            e.viewTransition.skipTransition();
        }

        if (isProfilePage(fromUrl) && isHomePage(currentUrl)) {
            const profile = extractProfileNameFromUrl(fromUrl);

            setTemporaryViewTransitionNames([
                [document.querySelector(`#${profile} span`), 'name'],
                [document.querySelector(`#${profile} img`), 'avatar'],
            ], e.viewTransition.ready);
       }

       if (isProfilePage(currentUrl)) {
           setTemporaryViewTransitionNames([
               [document.querySelector(`#detail main h1`), 'name'],
               [document.querySelector(`#detail main img`), 'avatar'],
           ], e.viewTransition.ready);
       }
   }
});

Demo 地址:view-transitions.netlify.app/profiles/mp...

如果你对 CSS 视图过渡感兴趣的话,请移步阅读《使用 CSS 视图过渡创造流畅的界面动效》一文!

Popover API

HTML5 为元素新增了一个 popover 全局属性。该属性可以直接使用调用器(例如按钮)或 JavaScript 将一个 display: none 隐藏元素打开。如果需要创建基础的弹出式窗口,只需在元素上设置弹出式窗口属性 popover ,然后使用 popovertarget 将其 ID 关联到按钮。

HTML 复制代码
<button popovertarget="my-popover">Open Popover</button>

<div id="my-popover" popover>
    <p>I am a popover with more information.</p>
</div>

现在,按钮是调用方。点击按钮"Open Popover",弹窗式窗口就会显示出来;再点击窗口之外的任何地方,它又会重新被隐藏:

Demo 地址:codepen.io/una/full/qB...

启用弹出属性后,浏览器无需任何额外的脚本即可处理许多关键行为,包括:

  • 提升到顶层 :在网页其余部分之上单独显示的一层,因此你不必操心 z-index

  • 关闭功能:点击弹出式窗口区域以外的位置,即可关闭弹出式窗口并返回焦点

  • 默认标签页焦点管理:打开弹出式窗口后,下一个标签页就会停止在弹出式窗口内

  • 内置键盘绑定功能 :按 esc 键或切换两次将关闭弹出式窗口并返回焦点

  • 默认组件绑定 :浏览器从语义上将弹出窗口与其触发器相关联

你还可以使用下面这几个功能为弹出式窗口添加动画效果:

  • 能够在关键帧时间轴上为 displaycontent-visibility 添加动画效果

  • transition-behavior 属性与 allow-discrete 关键字搭配使用,可实现离散属性过渡动效,例如 display

  • @starting-style 规则,用于为从 display: none 到顶层的进入效果添加动画效果

  • overlay 属性,用于控制动画期间的顶层行为

这些属性适用于你要向顶层添加动画效果的任何元素,无论是弹出式窗口还是对话框(模态框)。总之,带有背景的对话框应如下所示:

Demo 地址:codepen.io/una/full/Ba...

CSS 复制代码
dialog, ::backdrop{
    opacity: 0;
    transition: opacity 1s, display 1s allow-discrete, overlay 1s allow-discrete;
}

[open], [open]::backdrop {
    opacity: 1;
}

@starting-style {
    [open], [open]::backdrop {
        opacity: 0;
    }
}

首先,设置 @starting-style,以便浏览器知道要通过哪种样式将此元素添加到 DOM 中。系统会同时对对话框和背景执行此操作。然后,设置对话框和背景的打开状态的样式。对于对话框,这使用 open 属性;对于弹出窗口,这则使用 ::popover-open 伪元素。最后,使用 allow-discrete 关键字为 opacitydisplayoverlay 添加动画效果,以启用离散属性可以转换的动画模式。

有关于这方面更详细的介绍还可以阅读:

锚点定位

传统的 CSS 定位机制position)为我们提供了绝对定位(absolute)的能力,使我们可以将元素精确放置在页面上的任何位置。这种灵活性为 Web 设计师和开发人员带来了巨大的便利,但与此同时,它也带来了挑战。特别是在创建响应式 Web 布局或需要相对于其他元素进行定位的情况下,传统的 CSS 定位可能显得复杂和限制多多。

CSS 锚点定位(CSS Anchor Positioning)是一项创新性的功能,它为我们带来了元素定位的全新范式。通过 CSS 锚点定位,我们能够将元素与其他元素关联起来,实现相对于其他元素的精确定位,而无需繁琐的 JavaScript 计算或额外的 HTML 标记。这一功能的出现,为我们提供了更多灵活性、更简单的实现方式,以及更好的性能。

例如下面这个示例,通过使用锚点定位,浏览器只需几行代码即可处理逻辑,将提示信息锚定到按钮底部的中心位置:

Demo 地址:codepen.io/una/full/wv...

HTML 复制代码
<div class="button-group">
    <button style="anchor-name: --anchor-btn-1" popovertarget="my-tooltip-1">
        <p aria-hidden="true">?</p>
        <p class="sr-only">Open Tooltip</p>
    </button>
    <div id="my-tooltip-1" class="tooltip" style="position-anchor: --anchor-btn-1" popover>
        <p>The sun dipped, fiery orange melting into buttery yellow. Maya mirrored the hues on canvas, each stroke bittersweet -- fleeting beauty, a day gone. Yet, she painted on, for in those streaks lay the promise of a new dawn.</p>
    </div>

    <button style="anchor-name: --anchor-btn-2" popovertarget="my-tooltip-2">
        <p aria-hidden="true">?</p>
        <p class="sr-only">Open Tooltip</p>
    </button>
    <div id="my-tooltip-2" class="tooltip" style="position-anchor: --anchor-btn-2" popover>
        <p>Shades of green swirled through the ancient forest. A hiker breathed in the scent of earth, worries dissolving in the symphony of green. Moss clung to weathered trunks, ferns unfurled. Here was a sense of belonging, far from the city's strife.</p>
    </div>
</div>

在这里,使用 popoverpopovertarget 来控制提示框(.tooltip)内容的隐藏与显示。另外,在 button 元素上使用 anchor-name 显示声明了一个锚点名称 --anchor-btn- ,并在 .tooltip 上使用 position-anchor 来引用相匹配的锚点名称。它们定义了锚点位置关系。

也就是说,在 CSS 中设置锚眯位置关系,方法是对锚点元素使用 anchor-name 属性,并对定位的元素使用 position-anchor 属性。然后,使用 anchor() 函数应用相对于锚点的绝对定位或固定定位。以下代码可将提示框(.tooltip)的顶部定位至按钮底部:

CSS 复制代码
.anchor {
    anchor-name: --my-anchor;
}

.positioned {
    position: absolute;
    position-anchor: --my-anchor;
}

或者,直接在锚点函数中使用锚点名称,并跳过 position-anchor 属性。这在锚定到多个元素时很有用。

CSS 复制代码
.anchor {
    anchor-name: --my-anchor;
}

.positioned {
    position: absolute;
    top: anchor(--my-anchor bottom);
}

最后,为 justifyalign 属性使用新的 anchor-center 关键字,使已定位的元素在其锚点的中心位置。

CSS 复制代码
.anchor {
    anchor-name: --my-anchor;
}

.positioned {
    position: absolute;
    top: anchor(--my-anchor bottom);
    justify-self: anchor-center;
}

虽然使用带有弹出窗口的锚点定位非常方便,但使用弹出窗口绝对不是使用锚点定位的必要条件。锚点定位可与任意两个(或更多)元素结合使用,以建立视觉关系。例如,以往不得不依赖 JavaScript 才能实现的熔岩灯导航菜单效果,现在仅使用 CSS 锚点定位就可实现:

Demo 地址:codepen.io/airen/full/...

HTML 部分很简单,就是一个包含链接元素 <a> 的无序列表 <ul>

HTML 复制代码
<ul class="nav">
    <li><a href="">Home</a></li>
    <!-- 省略其他的导航菜单项 -->
    <li><a href="">Contact us</a></li>
</ul>

鼠标悬停在菜单项时有一个粉红色的圆环(类似焦点环效果)出现,我们把这个称为"熔岩灯",它是列表(ul)的 ::before 伪元素构建的(通常很多 Web 开发者喜欢添加一个名为 .lava 的元素)。我们可以利用 CSS 锚点定位的特性,将 ul::before 锚定到不同的菜单项( a 元素)上。因此,我们需要先给每个菜单项(锚点元素)指定 anchor-name

CSS 复制代码
.nav {

    & li {
        &:nth-child(1) {
            --is: --item-1;
        }
        
        &:nth-child(2) {
            --is: --item-2;
        }
        
        &:nth-child(3) {
            --is: --item-3;
        }
        
        &:nth-child(4) {
            --is: --item-4;
        }
        
        &:nth-child(5) {
            --is: --item-5;
        }
    }
    
    & a {
        anchor-name: var(--is);
    }
}

你也可以考虑直接在 HTML 的 <a> 元素以内联的方式定义 --is 的值:

HTML 复制代码
<ul class="nav">
    <li><a href="" style="--is: item-1">Home</a></li>
    <!-- 省略其他的导航菜单项 -->
    <li><a href="" style="--is: item-5">Contact us</a></li>
</ul>

这两种方式都可以,看你自己喜好!

我们使用一个名为 --target 的 CSS 变量来控制伪元素 ::before 的锚点。我们的锚定元素(ul::before)使用了 anchor() 函数定位:

CSS 复制代码
.nav {
    anchor-name: --nav-menu;
    --target: --nav-menu;

    &::before {
        position: absolute;
        top: anchor(var(--target) top);
        left: anchor(var(--target) left);
        right: anchor(var(--target) right);
        bottom: anchor(var(--target) bottom);
    }
}

默认情况下,将列表 ul.nav 作为目标,使初始过渡来自项目的"周围"。然后,对定位(实际上是对 toprightbottomleft 属性的动画过渡)和其他可视效果使用了过渡效果。

接下来,鼠标悬浮在每个列表项(li)时,锚定元素 ul::before 需要滑到相应的锚点位置(鼠标悬浮对应的列表项)。我们可以通过 CSS 的 :has() 选择器来检测列表项中是否有被悬停或聚焦的链接,如果有,将此项目分配为 --target

CSS 复制代码
.nav {
    &:has(:nth-child(1) > a:is(:hover, :focus-visible)) {
        --target: --item-1;
    }
    
    &:has(:nth-child(2) > a:is(:hover, :focus-visible)) {
        --target: --item-2;
    }
    
    &:has(:nth-child(3) > a:is(:hover, :focus-visible)) {
        --target: --item-3;
    }
    
    &:has(:nth-child(4) > a:is(:hover, :focus-visible)) {
        --target: --item-4;
    }
    
    &:has(:nth-child(5) > a:is(:hover, :focus-visible)) {
        --target: --item-5;
    }
}

同样的,你可以使用 :not() 选择器来设置检测列表项中没有被悬停或聚焦的链接时,将锚定元素 ul::before 从视觉上隐藏起来:

CSS 复制代码
.nav {
    &:not(:has(a:is(:hover, :focus-visible)))::before {
        visibility: hidden;
        opacity: 0;
        filter: blur(2em);
    }
}

将所有代码结合起来,就能看到一个熔岩灯菜单效果,而且其过渡效果是那么的优雅(几乎能和 JavaScript 版本媲美):

CSS 复制代码
@layer anchor {
    .nav {
        anchor-name: --nav-menu;
        --target: --nav-menu;
    
        &::before {
            position: absolute;
            top: anchor(var(--target) top);
            left: anchor(var(--target) left);
            right: anchor(var(--target) right);
            bottom: anchor(var(--target) bottom);

            transition: all 0.3s;
        }
    
        & li {
            &:nth-child(1) {
                --is: --item-1;
            }
            
            &:nth-child(2) {
                --is: --item-2;
            }
            
            &:nth-child(3) {
                --is: --item-3;
            }
            
            &:nth-child(4) {
                --is: --item-4;
            }
            
            &:nth-child(5) {
                --is: --item-5;
            }
        }
    
        & a {
            anchor-name: var(--is);
        }
    
        &:not(:has(a:is(:hover, :focus-visible)))::before {
            visibility: hidden;
            opacity: 0;
            filter: blur(2em);
        }
    
        &:has(:nth-child(1) > a:is(:hover, :focus-visible)) {
            --target: --item-1;
        }
        
        &:has(:nth-child(2) > a:is(:hover, :focus-visible)) {
            --target: --item-2;
        }
        
        &:has(:nth-child(3) > a:is(:hover, :focus-visible)) {
            --target: --item-3;
        }
        
        &:has(:nth-child(4) > a:is(:hover, :focus-visible)) {
            --target: --item-4;
        }
        
        &:has(:nth-child(5) > a:is(:hover, :focus-visible)) {
            --target: --item-5;
        }
    }
}

使用锚点定位时,除了你之前可能使用过的默认方向绝对定位之外,还包含一个新布局机制,已作为锚点定位 API 的一部分(称为边衬区)引入。inset-area 属性(插入区域)可让你轻松放置已定位的元素相对于其各自的锚点,它适用于包含 9 个单元格的网格,且锚定元素位于中心位置。例如,inset-area: top 会将已定位的元素置于顶部,inset-area: bottom 则会将已定位的元素置于底部。

CSS 复制代码
.anchor {
    anchor-name: --my-anchor;
}

.positioned {
    position: absolute;
    position-anchor: --my-anchor;
    inset-area: bottom;
}

另外,将弹出窗口和锚点定位结合使用,可以更轻松地创建菜单和子菜单导航。此外,当你使用锚定元素到达视口边缘时,你也可以让浏览器为你处理位置变化。 你可以通过以下几种方式完成此操作。第一种是创建你自己的定位规则。在这种情况下,子菜单最初位于"店面"按钮的右侧。不过,当菜单右侧没有足够的空间时,你可以创建一个 @position-try 代码块,为其提供自定义标识符 --bottom。然后,使用 position-try-options 将此 @position-try 代码块连接到锚点。

现在,浏览器将在这些锚定状态之间切换,先尝试切换到正确的位置,然后再切换到底部。这可以通过很好的转场来实现:

Demo 地址:codepen.io/una/full/KK...

CSS 复制代码
#submenu {
    position-anchor: --submenu;
    top: anchor(top);
    left: anchor(right);
    margin-left: var(--padding);

    position-try-options: --bottom;

    transition: top 0.25s, left 0.25s;
    width: max-content;
}

@position-try --bottom {
    top: anchor(left);
    left: anchor(bottom);
    margin-left: var(--padding);
}

除了明确的定位逻辑之外,浏览器还提供了一些关键字,供你进行一些基本的互动(例如在块中翻转锚点或内嵌方向)。

CSS 复制代码
position-try-options: flip-block, flip-inline;

为了获得简单的翻转体验,请充分利用这些 flip 关键字值,完全跳过编写 position-try 定义。因此,现在你只需几行 CSS 即可得到一个功能齐全的位置自适应锚点定位元素。

Demo 地址:codepen.io/una/full/jO...

CSS 复制代码
.tooltip {
    inset-area: top;
    position-try-options: flip-block;
}

注意,position-try-optionsposition-try@position-try 替代了之前的 position-fallback 属性 @position-fallback 规则。

如果你想更深入的了解 CSS 锚点定位相关的知识,请移步阅读《CSS 锚点定位:探索下一代 Web 布局》!

自定义 select 控件样式

通常情况之下,Web 开发者都会通过 CSS 和 JavaScript 一起为 select 控件自定义样式。现在,你可以通过结合使用 popoveranchorselect 自定义样式。目前,正在为此转换的方法是使用 CSS 的 apperance 属性:

CSS 复制代码
select {
    appearance: base-select;
}

注意,appearance: base-select; 规则还处于实验阶段!

除了 appearance: base-select 之外,还有一些新的 HTML 更新。这些功能包括:将选项(option)封装在 datalist 中以进行自定义,以及在选项中添加任意非互动内容(如图片)。你还可以使用新元素 <selectedoption>,该元素会在其自身中反映选项的内容,然后你可以根据自己的需求对其进行自定义。这个元素非常方便。

HTML 复制代码
<selectlist class="country-select">
    <button type=selectlist>
        <selectedoption></selectedoption>
    </button>
    <option value="" hidden>
        <figure></figure>
        <p>Select a country</p>
    </option>
    <option value="andorra">
        <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/19/Flag_of_Andorra.svg/120px-Flag_of_Andorra.svg.png" />
        <p>Andorra</p>
    </option>
    <!-- 省略其他 option -->
</selectlist>
CSS 复制代码
option,
selectedoption {
    display: grid;
    grid-template-columns: 1.5rem 1fr;
    gap: 1rem;
    font-size: 1rem;
    align-items: center;
    padding: 0 1rem;

    & img,
    & figure {
        width: 100%;
    }
}

[hidden] {
    display: none;
}

button {
    background: aliceblue;
    border: 1px solid gray;
}

selectedoption {
    font-style: italic;

    & figure {
        background-image: linear-gradient( 0deg,#fff 0%,#000 0% 20%,#fff 0% 40%, #000 0% 60%,#fff 0% 80%,#000 0% 100%);
        height: 1rem;
        margin: 0;
    }
}

Demo 地址:codepen.io/una/full/KK...

<select> 另一个变化是,在 <select> 中启用了 <hr>(即水平规则元素),这是一个小而实用的功能。虽然这没有太多的语义用途,但确实可以帮助你很好地区分选定列表中的内容,尤其是你不一定想使用优化组(例如占位值)分组的内容。

为此,请将 <hr> 元素添加到选项列表中,如以下 HTML 所示:

HTML 复制代码
<label for="major-select">Please select a major:</label> <br/>

<select name="majors" id="major-select">
    <option value="">Select a major</option>
    <hr>
    <option value="arth">Art History</option>
    <option value="finearts">Fine Arts</option>
    <option value="gdes">Graphic Design</option>
    <option value="lit">Literature</option>
    <option value="music">Music</option>
    <hr>
    <option value="aeroeng">Aerospace Engineering</option>
    <option value="biochemeng">Biochemical Engineering</option>
    <option value="civileng">Civil Engineering</option>
    <option value="compeng">Computer Engineering</option>
    <option value="eleng">Electrical Engineering</option>
    <option value="mecheng">Mechanical Engineering</option>
</select>

手风琴组件

我们可以使用 HTML 的 <details><summary> 元素来构建手风琴组件(Accordion)。它新增了 name 属性,主要是为多个详细信息元素应用相同的名称值(name),从而创建一个关联的详细信息组,类似于一组单选按钮:

HTML 复制代码
<details name="my-accordion">
    <summary>Summary 1</summary>
    <p>Lorem ipsum dolor...</p>
</details>

<details name="my-accordion" open>
    <summary>Summary 2</summary>
    <p>Lorem ipsum ...</p>
</details>
CSS 复制代码
details {
    --border-color: #6300ff;
    border: 0.25em solid var(--border-color);
    overflow: hidden;
    margin: 0;

    > summary {
        background: #ccc;
        padding: 0.5em;
        border-bottom: 0.25em solid var(--border-color);
        cursor: pointer;
    
        transition: background 0.25s ease, color 0.25s ease;
    
        &:hover {
            background: #b4b4b4;
        }
    }

    &[open] {
        border-bottom: 0.25em solid var(--border-color);
    
        > summary {
            background: #b4b4b4;
        }
    }

    > *:not(summary) {
        padding: 1em;
    }
}

details:has(+ details) {
    border-bottom: none;
}
details + details {
    border-top: none;
}

:not(details) + details,
details:first-child {
    border-top-left-radius: 0.5em;
    border-top-right-radius: 0.5em;
}
details:not(details:has(+ details)) {
    border-bottom-left-radius: 0.5em;
    border-bottom-right-radius: 0.5em;

    &:not([open]) summary {
        border-bottom: 0;
    }
}

Demo 地址:codepen.io/web-dot-dev...

:user-valid:user-invalid

在任何界面中,用户输入都是最敏感的问题之一。实用的应用必须帮助用户查看、理解并修正输入中的任何错误。:user-valid:user-invalid 伪类选择器仅在用户更改输入后才提供有关错误的反馈,从而帮助改善输入验证的用户体验。

:user-valid:user-invalid 伪类选择器与现有的 :valid:invalid 伪类类似。两者均根据其当前值是否满足其验证限制来匹配表单控件。不过,新的 :user-valid:user-invalid 伪类的优势在于,它们仅在用户与输入进行明显互动后才匹配表单控件。

必填且空的表单控件将与 :invalid 匹配,即使用户尚未开始与页面互动也是如此。不过,除非用户更改了输入并使其处于无效状态,否则同一表单控件不会与 :user-invalid 匹配。

CSS 复制代码
:root {
     --state-color: black;
     --bg: white;
}

input:user-valid,
select:user-valid,
textarea:user-valid {
    --state-color: green;
    --bg: linear-gradient(45deg in oklch, lime, #02c3ff);
}

input:user-invalid,
select:user-invalid,
textarea:user-invalid {
    --state-color: red;
    --bg: linear-gradient(15deg in oklch, #ea00ff, #ffb472);
}

input,
select,
textarea {
    appearance: none;
    border: 3px solid var(--state-color);
    background: var(--bg);
    border-radius: 0.5rem;
    box-sizing: border-box;
    font: inherit;
    padding: 0.5rem;
    inline-size: 100%;

    &:focus {
        outline: 3px dashed black;
    }
}

Demo 地址:codepen.io/web-dot-dev...

如果没有这些伪类,实现 :user-valid:user-invalid 启用的用户体验需要编写额外的有状态代码。该代码需要跟踪初始值、输入的当前焦点状态、用户对该值的更改程度,运行额外的有效性检查,并最终添加一个用于选择样式的类。你现在可以依靠浏览器自动处理所有这些操作。

field-sizing: content

field-sizing: content 可以应用于表单控件,如输入框(input)和文本区域(textarea),使得输入框和文本区域的大小可以根据内容的多少而增长(或缩小)。field-sizing: content 对于文本区域特别有用,因为你不再需要固定大小,在输入框太小时可能需要滚动才能看到你在提示的前面部分写的内容。

如果没有 field-sizing,若要创建大小合适的输入字段,你必须猜测文本字段输入的平均大小,或使用 JavaScript 计算字符数,并在用户输入文本时增加元素的高度或宽度。换句话说,这并不容易,并且在尝试跟踪用户输入的内容时容易出错。

使用 field-sizing 时,你只需要一行 CSS 来根据内容调整尺寸。这种基于内容的大小样式不仅仅适用于文本区域!

CSS 复制代码
textarea, select, input {
    field-sizing: content;
}

Demo 地址:codepen.io/argyleink/f...

下面列出了 field-sizing 可以处理的 <form> 元素,并总结了这些元素对每个元素的影响。

  • <textarea>:输入会收起为 min-inline-size 或以适应占位符。随着用户输入,输入会沿内嵌方向增大,直到达到内嵌大小上限;届时文本将换行,且输入的块大小也会增大以适应新行

  • <select><select multiple>:选择元素会缩小以适应所选选项。具有 multiple 属性的 select 会扩展以适应最宽的选项,并根据需要设置高以适应选项数量

  • <input type="text"><input type="email"><input type="number">:输入会收起为 min-inline-size 或以适应占位符。随着用户输入,输入沿内嵌方向增大,直到达到 max-inline-size,此时溢出会裁剪输入值

  • <input type="file">:输入框会收起为按钮和预填充的文件名文本。上传文件时,输入的宽度为按钮加上文件名文本的宽度

Demo 地址:codepen.io/web-dot-dev...

使用 field-sizing 意味着你必须执行一些额外的工作,即增加一些防"御式 CSS ",其目标不一定是准确说明某事物的行为或外观,而是防止它进入不理想的视觉状态。以前,输入具有相当多的固定大小,但在应用 field-sizing: content 后,输入可能会变得太小或太大。

以下样式是一个不错的起点。它们有助于确保元素不会收起到一个太小的方框中,并且在 textarea 的情况下也不会变得太大。

CSS 复制代码
textarea {
    min-block-size: 3.5rlh;
    min-inline-size: 20ch;
    max-inline-size: 50ch;
}

select {
    min-inline-size: 5ch;
    min-block-size: 1.5lh;
}

input {
    min-inline-size: 7ch;
}

如果你对"防御式 CSS"话题感兴趣的话,请移步阅读《防御式 CSS 精讲》中的内容!

CSS 嵌套

嵌套是 CSS 处理器(例如 Sass)的核心功能之一,每一位 Web 开发者都希望 CSS 中能有嵌套的功能。因为它可以节省 Web 开发人员一遍又一遍地编写相同选择器的时间。它还允许你直观地为组件代码进行分块,并在这些组件内包含状态和修饰符,例如容器查询和媒体查询。

以前,为了避免因 CSS 选择权重使代码受到冲突,我习惯性将所有查询样式放在组件文件的底部。现在,你可以将它们写在逻辑上合理的位置。

也就是说,CSS 的嵌套可以使CSS 代码更清晰,更容易理解和更易于维护。而且,你现在可以不再依赖 CSS 处理器,直接在 CSS 中使用嵌套的功能。

这个功能,去年就得到所有主流浏览器的支持,只不过,它必须以 & . (类名)、 # (ID)、 @ @ 规则)、 : :: * + ~ > [ 符号开头。 否则,CSS 解析器将会视其无效。

现如今,CSS 的嵌套在语法规则上做出相应的优化,采用了较为宽松的语法规则。即不再需要以 & . (类名)、 # (ID)、 @ @ 规则)、 : :: * + ~ > [ 符号开头。 例如,在嵌套的元素选择器前在不再需要添加 & 符号,例如:

CSS 复制代码
h1 {
    color: red;
    
    span {
        color: blue;
    }
}

它与之前的严谨语法规则等效:

CSS 复制代码
h1 {
    color: red;
    
    & span {
        color: blue;
    }
}

有关于 CSS 嵌套更详细的介绍,请移步阅读《CSS 的嵌套和作用域:&@scope》!

块容器中的内容对齐

我曾在《现代 Web 布局》中花了两节课(《Flexbox 布局中的对齐方式》和《Grid 布局中的对齐方式》)篇幅来介绍 Flex 项目和 Grid 项目在容器中的对齐方式:

如果你曾经接触过 CSS 框对齐相关的特性,你应该知道,该规范中所提到的对齐属性都受限于 Flexbox 和 Grid 布局。换句话说,你首先得在一个 Flexbxo 或 Grid 格式化上下文中,它们才能工作。例如,align-content 属性,它可以使内容垂直居中变得更简化,但其前提是在 Flexbox 或 Grid 环境中:

CSS 复制代码
.flexbox {
    display: flex; /* 或 inline-flex */
    align-content: center;
}

.grid {
    display: grid; /* 或 inline-grid */
    align-content: center;
}

现在,这个规则得到了改善,能够在块容器中使用类似 align-content 的居中机制。这意味着你现在可以在 div 内实现垂直居中,而无需应用 Flexbox 或 Grid 布局,也不会产生因此产生意想不到的副作用,例如阻止外边距合并:

HTML 复制代码
<div>
    <p contenteditable>I am centered in a block context!</p>
    <p contenteditable>CSS is Awesome</p>
</div>
CSS 复制代码
body {
    width: 100vw;
    min-height: 100vh;
    align-content: center;
}
div {
    width: 400px;
    aspect-ratio: 4 / 3;
    margin: 0 auto;
    align-content: center;
}

Demo 地址:codepen.io/airen/full/...

text-wrap: balance 和 pretty

说到布局,Web 上的文本排版得到了很好的改进,CSS 新增了 text-wrap: balancetext-wrap: prettytext-wrap: balance 用于更均匀的文本块,而 text-wrap: pretty 则专注于减少文本最后一行的单个字符。

Demo 地址:codepen.io/web-dot-dev...

虽然 text-wrap: balance 以自动化的方式将平衡文本的艺术带到 Web 中,并借鉴了印刷行业设计师的工作和传统,但平衡文本的使用也是有一定缺陷存在的。比如下面所列的这几个点:

  • 文本平衡不会影响元素的宽度

  • 行数限制

  • 考虑性能

  • white-space 属性的交互

因此,在使用 text-wrap 时需要根据实际情景做出相应的调整。有关于这方面更详细的介绍,请移步阅读《经典排版技术:使用 text-wrap: balance 实现文本平衡换行》!

多语言排版新特性

可能大部分 Web 开发者平时的工作都不太会涉及到多语言的排版与布局,但要处理好多语言的排版和布局并非易事,你需要对这方面的知识和技巧要有一定的了解和掌握。如果你从未接触过这方面的内容,那么我个人建议你花点时间了解一下这方面的知识:

过去一年里,CSS 文本模块级别 4 中的四项新的国际 CSS 功能得到了主流浏览器的支持:

  • 日语短语换行: word-break: auto-phrase

  • 脚本间间距:text-autospace

  • CJK 标点符号字距:text-spacing-trim

日语短语换行:word-break: auto-phrase

某些东亚语言(如中文或日语)不使用空格来分隔字词,并且行可以在任何字符处换行,即使它位于单词中间也是如此。这是这些语言的正常换行行为,但在标题或诗歌等短文本中,最好在自然短语边界处换行(在日语中,这种边界称为"Bunsetsu")。

新的 CSS 功能 word-break: auto-phrase 指定应在此类边界处进行换行

HTML 复制代码
<h1>窓ぎわのトットちゃん<h1>
CSS 复制代码
h1 {
    word-break: auto-phrase;
}

脚本间间距:text-autospace

中文和日文混用了多种文字,包括汉字、拉丁文和 ASCII 数字;对于日语,则还加入了平假名和片假名。在非汉字汉字文字之间切换时,使用少量的空格通常有助于提高可读性。如果你想停用此行为,可以通过 text-autospace 属性控制插入间距。

CSS 复制代码
h1 {
    text-autospace: no-autospace;
}

CJK 标点符号字距:text-spacing-trim

在中文、日语和韩语中,标点字符之间采用字距调整可以提高可读性,并且排版可以产生更美观的视觉体验。目前,大多数印刷材料和文字处理器都采用这种字距调整。

例如,CJK 句号和 CJK 右括号通常用于在字形空间的右半部分设置字形内部间距,以便每个字符都有恒定的推进。

但是,当这些标点符号字符出现在一行中时,这种字形内部空格就会变得过多。在以下两个示例中,第二个是字体正确的排版;应移除 CJK 英文句号的右半部分。

默认行为通常能够带来良好的结果,但开发者可以使用 text-spacing-trim 属性来选择不同的样式,或在某些情况下停用该属性。

相对颜色语法

CSS 颜色模块 Level 5 为 CSS 颜色函数引入相对颜色语法,进一步增强了颜色函数功能。此语法允许你基于另一个颜色定义新颜色。你可以通过首先使用 from 关键字定义起始颜色,然后像往常一样在颜色函数中指定新颜色的通道来使用它。

当你提供起始颜色时,你就可以访问"通道关键字",这些关键字允许你在颜色空间中引用每个通道。关键字取决于你使用的颜色函数。对于 rgb(),你将有 rgb 通道关键字。对于 oklch(),你将有 lch 关键字。对于每个颜色函数,你还有一个透明通道关键字,它引用起始颜色的 Alpha 通道。例如下面这个示例,这些颜色使用基于 OKLCH 的主题。当色调值根据滑块调整时,整个主题也会随之变化。这可以通过相对颜色语法来实现。背景使用基于色调的主色,并调整亮度、色度和色相通道来调整其值。--i 是值渐变列表中的兄弟索引,展示了如何将步进与自定义属性和相对颜色语法相结合来构建主题。

HTML 复制代码
<div class="scoreboard">
    <header>
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
            <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 18.75h-9m9 0a3 3 0 013 3h-15a3 3 0 013-3m9 0v-3.375c0-.621-.503-1.125-1.125-1.125h-.871M7.5 18.75v-3.375c0-.621.504-1.125 1.125-1.125h.872m5.007 0H9.497m5.007 0a7.454 7.454 0 01-.982-3.172M9.497 14.25a7.454 7.454 0 00.981-3.172M5.25 4.236c-.982.143-1.954.317-2.916.52A6.003 6.003 0 007.73 9.728M5.25 4.236V4.5c0 2.108.966 3.99 2.48 5.228M5.25 4.236V2.721C7.456 2.41 9.71 2.25 12 2.25c2.291 0 4.545.16 6.75.47v1.516M7.73 9.728a6.726 6.726 0 002.748 1.35m8.272-6.842V4.5c0 2.108-.966 3.99-2.48 5.228m2.48-5.492a46.32 46.32 0 012.916.52 6.003 6.003 0 01-5.395 4.972m0 0a6.726 6.726 0 01-2.749 1.35m0 0a6.772 6.772 0 01-3.044 0" />
        </svg>
        <h2>Top 5 Players</h2>
    </header>
    <ol>
        <li style="--i: 1">
            <span class="number">1</span>
            <p>Cyberduck</p>
            <span class="score">404</span>
        </li>
        <li style="--i: 2">
            <span class="number">2</span>
            <p>Ladylucifer</p>
            <span class="score">388</span>
        </li>
        <!-- 省略其他 li -->
    </ol>
</div>
CSS 复制代码
@layer demo.colors {
    :root {
        --hue: 230;
        --primary: oklch(70% .2 var(--hue));
        --primary-highlight: oklch(from var(--primary) 97% c h);
        --header-bg: oklch(from var(--primary) 35% .01 h);
        --text: white;
    }
  
    li {
        --_bg: oklch(
            from var(--primary) 
            calc(l - (var(--i) * .05)) 
            calc(c - (var(--i) * .01)) 
            calc(h - (var(--i) + 5))
        );
    }
}

@layer demo.color-usage {
    html {
        background: radial-gradient(
            circle at center, 
            var(--primary-highlight), 
            var(--primary)
        );
    }
  
    .scoreboard {
        & > header {
            background: var(--header-bg);
            color: var(--text);
          
            & > svg {
                color: var(--primary);
            }
        }
        
        & li {
            background: var(--_bg);
            color: var(--text);
        }
        
        & .number {
            background: var(--text);
            color: var(--_bg);
        }
    }
}

Demo 地址:codepen.io/web-dot-dev...

light-dark() 函数

在 CSS 中,除了使用我们熟悉的颜色空间给元素设置颜色之外,还可以使用系统颜色,例如:

CSS 复制代码
body {
    color: CanvasText;
    background-color: Canvas;
}

默认情况下,CanvasText 会生成接近 black 的颜色,而 Canvas 是接近 white 的颜色。实际实现取决于浏览器。例如,Chrome 中的 CanvasText 会生成 #121212,而 Safari 中已将其指定为稍浅的 #1e1e1e

这些系统颜色的隐藏功能在于它们可以响应 color-scheme 属性的计算值。例如,当使用的 color-schemedark 时,CanvasTextCanvas 的值会被反转。

在以下演示中,你可以更改在 :root 上设置的 color-scheme 值,并查看页面如何响应。

Demo 地址:codepen.io/web-dot-dev...

到目前为止,对所用的 color-scheme 值作出反应是系统颜色预留的东西。现在,借助 CSS 颜色模块级别 5 中指定的 light-dark(),你还可以实现相同的功能。

light-dark() 是接受两个参数的函数,且这两个参数都必须为 <color>。系统会根据使用的配色方案选择其中一种。

  • 如果所用配色方案为 light 或未知,则返回第一个值的计算值

  • 如果使用的配色方案为 dark,则返回第二种颜色的计算值

light-dark() 的结果为 <color>。它可在接受 <color> 的 CSS 中使用。例如,在 colorbackground-color 属性中,也可以使用 linear-gradient() 等函数。

在以下示例中,在深色模式下使用的背景颜色为 #333,在浅色模式(或未知模式)下使用的背景颜色为 #ccc

CSS 复制代码
:root {
    color-scheme: light dark;
}

body {
    background-color: light-dark(#ccc, #333);
}

请注意,为了让 light-dark() 正常运行,你需要指定 color-scheme。由于该属性会继承,因此你通常在 :root 上设置它,但如果你希望,可以选择在特定元素上设置它。

以往为了适应深色模式,通常情况下,使用 prefers-color-scheme 媒体条件为自定义属性设置不同的值:

CSS 复制代码
:root {
    --primary-color: #333;
    --primary-background: #e4e4e4;
    --highlight-color: hotpink;
}

@media (prefers-color-scheme: dark) {
    :root {
        --primary-color: #fafafa;
        --primary-background: #121212;
        --highlight-color: lime;
    }
}

现在,使用 light-dark(),可以简化此代码。由于 color-scheme:root 上被设为 light dark,因此在将操作系统从浅色模式更改为深色模式时,这些颜色的值会自动更改,反之亦然。

CSS 复制代码
:root {
    color-scheme: light dark;
    --primary-color: light-dark(#333, #fafafa);
    --primary-background: light-dark(#e4e4e4, #121212);
    --highlight-color: light-dark(hotpink, lime);
}

另一个好处是,可以通过将 color-scheme 设置为 darklight 来强制 DOM 的某个子树仅使用浅色模式或深色模式。在以下示例中,这应用于 :root :

CSS 复制代码
:root {
    color-scheme: light dark;
    --primary-color: light-dark(#333, #fafafa);
    --primary-background: light-dark(#e4e4e4, #121212);
    --highlight-color: light-dark(hotpink, lime);
}

:root {
    &:has(input[name="color-scheme"][value="light dark"]:checked) {
        color-scheme: light dark;
    }
    &:has(input[name="color-scheme"][value="light"]:checked) {
        color-scheme: light;
    }
    &:has(input[name="color-scheme"][value="dark"]:checked) {
        color-scheme: dark;
    }
}

body {
    color: var(--primary-color);
    background-color: var(--primary-background);
    transition: color 0.4s, background-color 0.4s;
}

mark {
    background: var(--highlight-color);
    transition: color 0.4s, background-color 0.4s;
}

Demo 地址:codepen.io/web-dot-dev...

在现代 CSS 中,颜色这一领域新增了不少的特性,有关于这方面更详细的介绍请阅读下面这些内容:

CSS :has() 选择器

:has() 选择器是一个关系型伪类选择器,也被称为函数型伪类选择器,它和:is()、:not() 以及 :where() 函数型选择器被称为 CSS 的逻辑组合选择器 !:has() 已于 2023 年底得到所有主流浏览器的支持!这个新的选择器看起来很小,而且很普通,但你会惊讶于它可以解锁的所有用例:游戏、响应式、内容感知布局、智能组件等。

例如下面这个示例,使用了 :has() 作为父级选择器、结合使用 :has() 与数量查询以及容器查询来构建一个能够在纵向或横向模式下优雅地显示 1-12 个元素的网格布局:

CSS 复制代码
@layer demo {
    .container {
        container: perfect-bento / size;
    }
  
    .always-great-grid {
    
        &:has(> :last-child:nth-child(3)) > :first-child {
            grid-column: span 2;
        }
        
        &:has(> :last-child:nth-child(4)) {
            grid-template-columns: repeat(2, 1fr);
        }
        
        &:has(> :last-child:nth-child(5)) > :first-child {
            grid-column: span 2;
        }
        
        &:has(> :last-child:nth-child(6)) {
            grid-template-columns: repeat(2, 1fr);
        }
        
        &:has(> :last-child:nth-child(7)) > :first-child {
            grid-column: span 2;
            grid-row: span 2;
        }
        
        &:has(> :last-child:nth-child(8)) {
            grid-template-columns: repeat(2, 1fr);
        }
        
        &:has(> :last-child:nth-child(9)) {
            grid-template-columns: repeat(3, 1fr);
        }
        
        &:has(> :last-child:nth-child(10)) {
            grid-template-columns: repeat(2, 1fr);
        }
        
        &:has(> :last-child:nth-child(11)) > :first-child {
            grid-column: span 2;
            grid-row: span 2;
        }
        
        &:has(> :last-child:nth-child(12)) {
            grid-template-columns: repeat(4, 1fr);
        }
        
        @container perfect-bento (orientation: landscape) {
            grid-auto-flow: column;
            grid-auto-columns: 1fr;
          
            &:has(> :last-child:nth-child(3)) {
                grid-template-columns: repeat(4, 1fr);
            }
          
            &:has(> :last-child:nth-child(5)) > :first-child {
                grid-column: initial;
                grid-row: span 2;
            }
          
            &:has(> :last-child:nth-child(6)),
            &:has(> :last-child:nth-child(8)),
            &:has(> :last-child:nth-child(10)),
            &:has(> :last-child:nth-child(12)) {
                grid-template-rows: repeat(2, 1fr);
            }
          
            &:has(> :last-child:nth-child(9)) > :first-child {
                grid-column: span 2;
                grid-row: span 2;
            }
        }
    }
}

Demo 地址:codepen.io/web-dot-dev...

注意,在这个示例中,还应用 CSS 的其他相关特性,例如 CSS 网格布局容器查询视图过渡等!

再来看另一个示例,使用 :has() 和单选按钮创建 Tab UI 组件:

Demo 地址:codepen.io/jh3y/full/M...

HTML 复制代码
<div class="tabs">
    <input type="radio" id="html" name="fav_language" value="HTML" checked>
    <label for="html">HTML</label>
    <input type="radio" id="css" name="fav_language" value="CSS">
    <label for="css">CSS</label>
    <input type="radio" id="javascript" name="fav_language" value="JavaScript">
    <label for="javascript">JavaScript</label>
</div>
CSS 复制代码
.tabs:has(:checked:nth-of-type(1)) { --active: 0; }
.tabs:has(:checked:nth-of-type(2)) { --active: 1; }
.tabs:has(:checked:nth-of-type(3)) { --active: 2; }
.tabs:has(:checked:nth-of-type(4)) { --active: 3; }

.tabs :checked + label { --highlight: 1; }

.tabs:has(input:nth-of-type(2)) { --count: 2; }
.tabs:has(input:nth-of-type(3)) { --count: 3; }
.tabs:has(input:nth-of-type(4)) { --count: 4; }

input:not(:checked) + label:hover {
    --highlight: 0.35;
    background: hsl(0 0% 20%);
}

.tabs::after {
    pointer-events: none;
    content: "";
    width: calc(100% / var(--count));
    height: 100%;
    background: hsl(0 0% 100%);
    position: absolute;
    border-radius: calc(var(--radius) - var(--border));
    mix-blend-mode: difference;
    translate: calc(var(--active, 0) * 100%) 0;
    transition: translate, outline-color;
    transition-duration: var(--speed);
    transition-timing-function: var(--ease, ease);
    outline: 2px solid transparent;
}

.tabs:has(:focus-visible)::after {
    outline-color: red;
}

Tab 组件是一个使用 display: grid 的容器,你可以使用 :has() 选择器来计算容器中选项卡的数量:

CSS 复制代码
.tabs:has(input:nth-of-type(2)) { --count: 2; }
.tabs:has(input:nth-of-type(3)) { --count: 3; }
.tabs:has(input:nth-of-type(4)) { --count: 4; }

一旦知道选项卡的数量,就知道了如何设置指示器的大小:

CSS 复制代码
.tabs::after {
    pointer-events: none;
    content: "";
    width: calc(100% / var(--count));
    height: 100%;
    background: hsl(0 0% 100%);
    position: absolute;
    border-radius: calc(var(--radius) - var(--border));
    mix-blend-mode: difference;
    translate: calc(var(--active, 0) * 100%) 0;
    transition: translate, outline-color;
    transition-duration: var(--speed);
    transition-timing-function: var(--ease, ease);
    outline: 2px solid transparent;
}

这是一个使用 --count 来确定其大小的伪元素。

接下来,使用 :has() 与单选按钮的选中状态来判断哪个选项卡是当前状态:

CSS 复制代码
.tabs:has(:checked:nth-of-type(1)) { --active: 0; }
.tabs:has(:checked:nth-of-type(2)) { --active: 1; }
.tabs:has(:checked:nth-of-type(3)) { --active: 2; }
.tabs:has(:checked:nth-of-type(4)) { --active: 3; }

如果第二个输入被 :checked,设置 --active: 1,然后将伪元素在选项卡上的位置进行平移:

CSS 复制代码
.tabs::after { 
    translate: calc(var(--active, 0) * 100%) 0; 
}

最后的渲染技巧是使用 mix-blend-mode 。选项卡具有黑色的背景颜色,伪元素是白色的,标签文本也是白色的。当你在伪元素上使用 mix-blend-mode: difference 时,它会产生这样的效果,即文本从白色滑动到黑色:

CSS 复制代码
.tabs::after {
    color: hsl(0 0% 100%);
    mix-blend-mode: difference;
}

有关于 :has() 更多详细的介绍,请移步阅读:

容器查询

容器查询允许你根据元素容器的大小、计算样式和状态来应用样式。其最大的特点是: 容器查询允许开发者定义任何一个元素为包含上下文,查询容器的后代元素可以根据查询容器的大小或计算样式、状态的变化来改变风格 !

以容器查询中的尺寸查询为例,容器查询可以使时间轴根据容器宽度从迷你设计(适合移动端,位于窄容器中)更改变全宽度设计(适合桌面端,位于大容器中)。

它所需要的 HTML 结构如下:

HTML 复制代码
<div class="timeline--container">
    <div class="timeline--wrapper">
        <ol class="timeline">
            <li>
                <time>24/12/1994</time>
                <img src="https://picsum.photos/120/120/?random=2" alt="">
                <h3>Creative Director Miami, FL</h3>
                <p>Creative Direction, User Experience, Visual Design, Project Management, Team Leading</p>
            </li>
            <!-- 时间轴上的其他列表项 -->
        </ol>
    </div>
</div>

时间轴组件有三个变体,它们分别用于移动端的 mobile 、平板端的 tablet 和桌面端的 desktop ,我们使用 CSS 的 @layer 来管理它们的级联及样式。

CSS 复制代码
@layer reset, base, components.mobile, components.tablet, components.desktop, layout;

我们主要关注其中的 components.mobilecomponents.tabletcomponents.desktop 的样式。

CSS 复制代码
@layer components.mobile {
    .timeline--wrapper {
        display: grid;
        row-gap: 2rem;
        grid-template-columns: 30px minmax(0, 1fr);
        grid-template-areas: "line lists";
    }

    .timeline--wrapper::before {
        content: "";
        width: 2px;
        grid-area: line;
        background-color: #fff;
        justify-self: center;
    }

    .timeline {
        grid-area: lists;
        display: flex;
        flex-direction: column;
        gap: 2rem;
    }

    .timeline li {
        display: grid;
        align-items: start;
        align-content: start;
        grid-template-columns: 64px minmax(0, 1fr);
        gap: 0.5rem 1.5rem;
        grid-template-areas:
            "avatar time"
            "... title"
            "... description";
        margin-left: 1.5rem;
        position: relative;
    }

    .timeline li::before {
        content: "";
        position: absolute;
        height: 2px;
        top: 31px;
        background-color: #fff;
        width: calc(64px + 1.5rem + 1.5rem + 15px);
        right: calc(100% - 1.5rem - 64px);
    }

    .timeline img {
        grid-area: avatar;
        aspect-ratio: 1;
        border-radius: 50%;
        border: 2px solid rgb(255 210 0);
        z-index: 2;
    }

    .timeline time {
        grid-area: time;
        color: rgb(255 210 0);
        align-self: center;
        font-size: clamp(1rem, 3cqi + 1.25rem, 1.5rem);
        display: flex;
        gap: 1rem;
        align-items: center;
        position: relative;
    }

    .timeline time::before,
    .timeline time::after {
        content: "";
        display: block;
        background-color: rgb(255 210 0);
        width: 24px;
        aspect-ratio: 1;
        border-radius: 50%;
        border: 2px solid #000;
    }

    .timeline time::after {
        right: calc(100% + 64px + 1.5rem + 1.5rem + 2px);
        position: absolute;
    }

    .timeline h3 {
        grid-area: title;
        font-size: clamp(1.25rem, 4cqi + 1.5rem, 2rem);
    }
    
    .timeline p {
        grid-area: description;
        font-size: 85%;
        color: rgb(255 255 255 / 0.8);
    }
}

这个时候,你在窄容器中看到的效果如下:

接下来编写 components.tablet (平板端变体)。在编写该变体样式之间,需要将组件容器 .timeline--container 显式声明为一个查询容器:

CSS 复制代码
.timeline--container {
    container-type: inline-size;
}

这样我们就可以在 @container 中编写平板端的组件样式:

CSS 复制代码
@layer components.tablet {
    .timeline--container {
container-type: inline-size;
}

    @container (width >= 768px) {
        .timeline--wrapper {
            grid-template-columns: auto;
        }
        
        .timeline--wrapper::before {
            content: none;
        }
        
        .timeline {
            display: grid;
            grid-template-columns: minmax(0, 1fr) 30px minmax(0, 1fr);
            justify-content: center;
            column-gap: 0;
        }

        .timeline::before {
            content: "";
            width: 2px;
            grid-area: line;
            background-color: #fff;
            justify-self: center;
            grid-row: 1 / span 12;
            grid-column: 2;
        }

        .timeline li:nth-of-type(2n + 1) {
            grid-column-start: 3;
        }

        .timeline li:nth-of-type(2n) {
            grid-column-start: 1;
            grid-template-columns: minmax(0, 1fr) 64px;

            grid-template-areas:
                "time  avatar"
                "title ..."
                "description ...";
            margin-right: 1.5rem;
            margin-left: 0;
            justify-content: end;
            text-align: end;
        }

        .timeline li:nth-of-type(2n) time {
            flex-direction: row-reverse;
        }

        .timeline li:nth-of-type(2n) time::after {
            left: calc(100% + 64px + 1.5rem + 1.5rem + 2px);
            right: auto;
        }

        .timeline li:nth-of-type(2n)::before {
            left: calc(100% - 1.5rem - 64px);
            right: auto;
        }

        .timeline li:nth-of-type(3) {
            grid-row-start: 3;
        }

        .timeline li:nth-of-type(3) {
            grid-row-start: 5;
        }

        .timeline li:nth-of-type(4) {
            grid-row-start: 7;
        }

        .timeline li:nth-of-type(5) {
            grid-row-start: 9;
        }

        .timeline li:nth-of-type(6) {
            grid-row-start: 11;
        }
    }
}

最后一步是处理 components.desktop 的样式:

CSS 复制代码
@layer components.desktop {
    @container (width >= 1024px) {
        .timeline--wrapper {
            overflow-x: auto;
        }
        
        .timeline {
            grid-template-columns: repeat(6, minmax(360px, 1fr));
            grid-template-rows: auto 30px auto;
        }

        .timeline::before {
            grid-column: 1 / span 6;
            grid-row: 2;
            width: 100%;
            height: 2px;
            justify-self: unset;
            align-self: center;
        }
        
        .timeline li {
            margin-left: 0;
            grid-template-areas:
                "avatar time"
                "avatar title"
                "avatar description";
            grid-template-columns: 64px minmax(0, 1fr);
            grid-template-rows: auto auto minmax(0, 1fr);
            grid-row-start: 1;
            grid-column-start: 1;
        }

        .timeline li:nth-of-type(2n) {
            grid-row-start: 3;
            text-align: start;
            justify-content: start;
            margin-right: 0;
        }

        .timeline li:nth-of-type(2n)::before {
            bottom: 10px;
            top: calc(0% - 2rem - 12px);
        }

        .timeline li:nth-of-type(2n)::after {
            top: calc(0% - 2rem - 30px);
            bottom: 0;
        }

        .timeline li:nth-of-type(2n) time {
            flex-direction: row;
        }

        .timeline li:nth-of-type(2) {
            grid-column-start: 2;
        }

        .timeline li:nth-of-type(3) {
            grid-column-start: 3;
        }

        .timeline li:nth-of-type(4) {
            grid-column-start: 4;
        }

        .timeline li:nth-of-type(5) {
            grid-column-start: 5;
        }

        .timeline li:nth-of-type(6) {
            grid-column-start: 6;
        }
        
        .timeline li::before {
            right: auto;
            bottom: calc(0% - 2rem - 3px);
            top: 12px;
            width: 2px;
            grid-area: avatar;
            justify-self: center;
            height: unset;
            left: auto;
        }

        .timeline li::after {
            content: "";
            display: block;
            background-color: rgb(255 210 0);
            width: 24px;
            aspect-ratio: 1;
            border-radius: 50%;
            border: 2px solid #000;
            grid-area: avatar;
            justify-self: center;
            align-self: end;
            z-index: 2;
            position: absolute;
            bottom: calc(0% - 2rem - 30px);
        }

        .timeline time::before {
            position: absolute;
            right: calc(100% + 32px + 10px);
        }

        .timeline time::after {
            content: none;
        }

        .timeline img {
            place-self: center;
        }
    }
}

最终效果如下:

Demo 地址:codepen.io/airen/full/...

它看起来和使用 CSS 媒体查询构建出来的响应式组件效果一样,但事实上还是有很大差异的,比如,我们将时间轴组件放置在不同的位置,我们可以在不调整视窗就能看到时间轴组件随容器尺寸的变化:

Demo 地址:codepen.io/airen/full/...

有关于容器查询更详细的介绍,请移步阅读:

CSS @property

CSS @property 并不是最新特性,它已经出现很多年了,今年在 Google I/O 大会提到 @property ,那是它已在 Baseline 中了。

简单的回顾一下,@property 是为 CSS 自定义属性(也常称 CSS 变量)提供语义意义的关键功能,并启用了一系列新的交互功能。@property 还允许在 CSS 中进行上下文意义,类型检测、默认值和回退值。为这更加稳健的功能(如范围样式查询)打开了大门。这是以前从未可能实现的功能,现在为 CSS 语言提供了更多深度。

CSS 复制代码
@property --card-bg {
    syntax: "<color>";
    inherits: false;
    initial-value: #c0bae8;
}

@property --shine-1 {
    syntax: "<color>";
    inherits: false;
    initial-value: #ffbbc0;
}

@property --shine-2 {
    syntax: "<color>";
    inherits: false;
    initial-value: #c0aecb;
}

.card {
    border-radius: 1rem;
    max-width: 36ch;
    padding: 2rem;
    background: radial-gradient( 300px circle at 55% 60% in oklab, var(--shine-2), transparent 100% 100% ), radial-gradient( farthest-side circle at 75% 30% in oklab,  var(--shine-1) 0%, var(--card-bg) 100% );
    background-blend-mode: color-burn;
    animation: animate-color-1 8s infinite linear alternate, 4s animate-color-2 1s infinite linear alternate;
}

@keyframes animate-color-1 {
    from {
        --shine-1: initial;
    }
    to {
        --shine-1: orange;
    }
}

@keyframes animate-color-2 {
    from {
        --shine-2: initial;
    }
    to {
        --shine-2: hotpink;
    }
}

Demo 地址:codepen.io/una/full/zY...

如果你对 CSS 自定义属性感兴趣,可以移步阅读:

总结

随着各种强大的全新界面功能在各种浏览器上不断推出,你将获得无限可能。新颖的互动体验,以及滚动驱动的动画和视图转换,让 Web 以前所未有的方式变得更加流畅和交互。有了下一级别的界面组件,你可以比以往更轻松地构建功能强大、自定义美观的组件,而不会破坏完整的原生体验。最后,在架构、布局、排版和响应式设计方面改善生活质量不仅解决了一些小问题,还为开发者提供了所需的工具,以便他们构建适用于各种设备、外形规格和用户需求的复杂界面。

通过这些新功能,你应该能够移除第三方脚本,以实现性能密集型功能(例如,通过锚点定位实现滚动叙述和将元素相互共享)、构建流畅的页面转换、最终设置下拉菜单样式,并以原生方式改进代码的整体结构。

参考文献


如果你觉得该教程对你有所帮助,请给我点个赞。要是你喜欢 CSS ,或者想进一步了解和掌握 CSS 相关的知识,请关注我的专栏,或者移步阅读下面这些系列教程:

相关推荐
Asort1 分钟前
JavaScript 从零开始(六):控制流语句详解——让代码拥有决策与重复能力
前端·javascript
无双_Joney20 分钟前
[更新迭代 - 1] Nestjs 在24年底更新了啥?(功能篇)
前端·后端·nestjs
在云端易逍遥22 分钟前
前端必学的 CSS Grid 布局体系
前端·css
ccnocare23 分钟前
选择文件夹路径
前端
艾小码23 分钟前
还在被超长列表卡到崩溃?3招搞定虚拟滚动,性能直接起飞!
前端·javascript·react.js
闰五月24 分钟前
JavaScript作用域与作用域链详解
前端·面试
泉城老铁28 分钟前
idea 优化卡顿
前端·后端·敏捷开发
前端康师傅28 分钟前
JavaScript 作用域常见问题及解决方案
前端·javascript
司宸29 分钟前
Prompt结构化输出:从入门到精通的系统指南
前端