这个 HTML 文件实现了一个极简的"咖啡飞入购物车"交互动画页面,点击任意一杯咖啡,咖啡图标会沿抛物线轨迹飞入底部购物车,同时购物车数字实时累加,全程无框架、无图片精灵、仅用 1 行 JS 计数。
大家复制代码时,可能会因格式转换出现错乱,导致样式失效。建议先少量复制代码进行测试,若未能解决问题,私信回复源码两字,我会发送完整的压缩包给你。
演示效果

HTML&CSS
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.items {
--y: 100px;
&:not(.cart) {
transform: translate(var(--x), calc(-1 * var(--y)));
}
&.espresso {
--x: 0px;
}
&.cappuccino {
--x: -100%;
}
&.flat-white {
--x: 100%;
}
input:checked {
transform: translate(calc(-1 * var(--x)), var(--y)) scale(0);
transition: transform .6s linear;
}
}
@layer layout {
main {
width: 450px;
aspect-ratio: 1.5;
contain: layout;
.items {
width: 140px;
aspect-ratio: 1;
position: absolute;
inset: auto 0 0;
margin-inline: auto;
&.cart {
width: 80px;
border-radius: 50%;
contain: layout;
output {
display: block;
width: 30px;
aspect-ratio: 1;
position: absolute;
offset: content-box 6% 0deg / -10px 0;
border-radius: inherit;
}
}
input {
position: absolute;
inset: 0;
}
}
&:has(input:focus-visible) [hidden] {
display: initial;
}
&:not(:has(input:checked)) .cart::after {
display: none;
}
}
}
@layer theme {
.cart {
font-size: 32px;
text-align: center;
align-content: center;
color: transparent;
text-shadow: 0 0 0 lightsteelblue;
background-color: rgb(250, 250, 251);
box-shadow: 2px 2px 6px rgb(230, 230, 231), -2px -2px 6px rgb(250, 250, 251), inset 2px 2px 3px #fff;
output {
font-size: 12px;
align-content: inherit;
color: white;
background-color: rgba(0, 0, 70, 0.2);
}
}
input {
appearance: none;
background: center/contain;
cursor: pointer;
.espresso & {
background-image: url("https://www.coca-cola.com/content/dam/onexp/in/en/home-page-test-img/brands/costa/costa-resized/cappuccino.png");
}
.cappuccino & {
background-image: url("https://www.coca-cola.com/content/dam/onexp/in/en/home-page-test-img/brands/costa/costa-resized/espresso.png");
}
.flat-white & {
background-image: url("https://www.coca-cola.com/content/dam/onexp/in/en/home-page-test-img/brands/costa/costa-resized/flat%20white.png");
}
&:focus-visible {
outline: none;
.items:has(&) {
border: 2px solid;
border-radius: 4px;
}
}
}
}
body {
height: 100vh;
margin: 0;
display: grid;
place-content: center;
user-select: none;
-webkit-user-select: none;
cursor: default;
font: 14px 'poppins';
}
</style>
</head>
<body>
<main>
<div class="items cart" data-count="0" aria-live="assertive">
🛒<output data-count="0" aria-label="Number of items in cart is">0</output>
</div>
<div class="items cappuccino">
<input type="radio" name="r1" title="Cappuccino">
<input type="radio" name="r1" title="Cappuccino">
</div>
<div class="items espresso">
<input type="radio" name="r2" title="Espresso">
<input type="radio" name="r2" title="Espresso">
</div>
<div class="items flat-white">
<input type="radio" name="r3" title="Flat White">
<input type="radio" name="r3" title="Flat White">
</div>
</main>
<script>
let n = 0;
const CART_CNT = document.querySelector("output");
document.querySelectorAll("[type='radio']").forEach(radio => {
radio.onclick = () => {
CART_CNT.innerText = ++n;
CART_CNT.setAttribute("arial-label", `${n}`)
}
});
</script>
</body>
</html>
HTML
- main:固定 450×300 盒子,居中展示。
- .cart:圆形气泡购物车,内部 output 实时显示数量。
- .items.*:每组咖啡放 两个同名 radio 保证点击后仍能继续点击同一组。
- aria-live:屏幕阅读器实时播报购物车数量变化。
CSS
变量驱动的抛物线动画
css
.items{
--y:100px; /* 垂直位移 */
--x:0/100%/-100%; /* 水平位移 */
transform:translate(var(--x),calc(-1*var(--y))); /* 起点 */
}
input:checked{
transform:translate(calc(-1*var(--x)),var(--y)) scale(0);
transition:transform .6s linear; /* 抛物线 + 缩小消失 */
}
- 自定义属性 --x / --y 把三组咖啡的初始位置一次性定义完。
- 选中后反向位移 → 形成「飞进购物车」的错觉。
气泡购物车
css
.cart{
border-radius:50%;
background:rgb(250,250,251);
box-shadow:
2px 2px 6px rgb(230,230,231),
-2px -2px 6px rgb(250,250,251),
inset 2px 2px 3px #fff;
}
- 三层阴影:外凸 + 内凹 → 软软的气泡感。
- text-shadow:0 0 0 lightsteelblue 让 🛒 图标呈现单色。
响应式 & 可访问性
css
@layer layout{ /* 结构 */ }
@layer theme { /* 皮肤 */ }
@media(max-width:768px){ ... }
@media(max-width:414px){ ... }
- CSS 原生分层(@layer)实现「皮肤」与「布局」解耦。
- 小屏去掉左右「耳朵圆角」,避免被裁切。
JavaScript
JavaScript
let n = 0;
const CART_CNT = document.querySelector('output');
document.querySelectorAll('[type="radio"]').forEach(radio => {
radio.onclick = () => {
CART_CNT.innerText = ++n;
CART_CNT.setAttribute('aria-label', `${n}`);
};
});
- ++n:利用「同名 radio 仍可重复点击」的特性,持续累加。
- aria-label:同步给读屏器,确保无障碍。
- 一行核心 JS 完成计数。
各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!