<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UI 工艺</title>
<style>
@import url('https://unpkg.com/normalize.css') layer(normalize);
@layer normalize, base, demo;
@layer demo {
body {
background: light-dark(#fff, #000);
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 1rem;
padding-block: 2rem;
}
h1,
p {
margin: 0;
}
h1.fluid {
--font-size-min: 22;
--font-level: 4.25;
}
h3 {
white-space: nowrap;
margin: 0;
}
body > p {
width: 74ch;
max-width: calc(100% - 4rem);
text-wrap: balance;
font-family: monospace;
margin-bottom: 4rem;
line-height: 1.5;
opacity: 0.8;
font-weight: 400;
@media (max-width: 768px) {
text-align: center;
}
}
li :is(svg, h3) {
opacity: 0.6;
transition: opacity calc(var(--speed) * 1.2) var(--easing);
}
li :is(a, p) {
opacity: 0;
transition: opacity calc(var(--speed) * 1.2) var(--easing);
width: fit-content;
}
li img {
filter: grayscale(1) brightness(1.5);
scale: 1.1;
transition-property: filter, scale;
transition-duration: calc(var(--speed) * 1.2);
transition-timing-function: var(--easing);
}
[data-active='true'] :is(a, p, h3, svg) {
opacity: var(--opacity, 1);
}
[data-active='true'] :is(a, p) {
transition-delay: calc(var(--speed) * 0.25);
}
[data-active='true'] img {
filter: grayscale(0) brightness(1);
scale: 1;
transition-delay: calc(var(--speed) * 0.25);
}
article {
width: calc(var(--article-width) * 1px);
height: 100%;
position: absolute;
font-family: monospace;
top: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: 1rem;
padding-inline: calc(var(--base) * 0.5 - 9px);
padding-bottom: 1rem;
overflow: hidden;
h3 {
position: absolute;
top: 1rem;
left: calc(var(--base) * 0.5);
transform-origin: 0 50%;
rotate: 90deg;
font-size: 1rem;
font-weight: 300;
text-transform: uppercase;
font-family: monospace;
}
svg {
width: 18px;
fill: none;
}
p {
font-size: 13px;
text-wrap: balance;
line-height: 1.25;
--opacity: 0.8;
}
a {
position: absolute;
bottom: 1rem;
height: 18px;
line-height: 1;
color: inherit;
&:is(:focus-visible, :hover) {
outline: none;
span {
text-decoration: underline;
text-underline-offset: 4px;
}
}
span {
display: inline-block;
line-height: 18px;
translate: calc(var(--base) * 0.5);
font-weight: 500;
}
}
img {
position: absolute;
pointer-events: none;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
mask: radial-gradient(100% 100% at 100% 0, #fff, #0000);
}
}
:root {
--gap: 8px;
--base: clamp(2rem, 8cqi, 80px);
--easing: linear(
0 0%,
0.1538 4.09%,
0.2926 8.29%,
0.4173 12.63%,
0.5282 17.12%,
0.6255 21.77%,
0.7099 26.61%,
0.782 31.67%,
0.8425 37%,
0.8887 42.23%,
0.9257 47.79%,
0.9543 53.78%,
0.9752 60.32%,
0.9883 67.11%,
0.9961 75%,
1 100%
);
--speed: 0.6s;
}
ul {
display: grid;
container-type: inline-size;
grid-template-columns: 10fr 1fr 1fr 1fr 1fr 1fr 1fr;
gap: var(--gap);
list-style-type: none;
justify-content: center;
padding: 0;
height: clamp(300px, 40dvh, 474px);
margin: 0;
width: calc(
((var(--items) - 1) * var(--base)) + ((var(--items) - 1) * var(--gap)) +
var(--ideal)
);
width: 820px;
max-width: calc(100% - 4rem);
transition: grid-template-columns var(--speed) var(--easing);
}
li {
background: light-dark(#fff, #000);
position: relative;
overflow: hidden;
min-width: var(--base);
border-radius: 8px;
border: 1px solid color-mix(in hsl, canvas, canvasText 50%);
}
.footer {
margin-top: 2rem;
font-size: 12px;
color: color-mix(in hsl, canvasText, transparent 40%);
text-align: center;
padding: 1rem 0;
}
}
@layer base {
:root {
--font-size-min: 16;
--font-size-max: 20;
--font-ratio-min: 1.2;
--font-ratio-max: 1.33;
--font-width-min: 375;
--font-width-max: 1500;
}
html {
color-scheme: light dark;
}
[data-theme='light'] {
color-scheme: light only;
}
[data-theme='dark'] {
color-scheme: dark only;
}
:where(.fluid) {
--fluid-min: calc(
var(--font-size-min) * pow(var(--font-ratio-min), var(--font-level, 0))
);
--fluid-max: calc(
var(--font-size-max) * pow(var(--font-ratio-max), var(--font-level, 0))
);
--fluid-preferred: calc(
(var(--fluid-max) - var(--fluid-min)) /
(var(--font-width-max) - var(--font-width-min))
);
--fluid-type: clamp(
(var(--fluid-min) / 16) * 1rem,
((var(--fluid-min) / 16) * 1rem) -
(((var(--fluid-preferred) * var(--font-width-min)) / 16) * 1rem) +
(var(--fluid-preferred) * var(--variable-unit, 100vi)),
(var(--fluid-max) / 16) * 1rem
);
font-size: var(--fluid-type);
}
*,
*:after,
*:before {
box-sizing: border-box;
}
body {
display: grid;
place-items: center;
min-height: 100vh;
font-family: 'SF Pro Text', 'SF Pro Icons', 'AOS Icons', 'Helvetica Neue',
Helvetica, Arial, sans-serif, system-ui;
}
body::before {
--size: 45px;
--line: color-mix(in hsl, canvasText, transparent 70%);
content: '';
height: 100vh;
width: 100vw;
position: fixed;
background: linear-gradient(
90deg,
var(--line) 1px,
transparent 1px var(--size)
)
50% 50% / var(--size) var(--size),
linear-gradient(var(--line) 1px, transparent 1px var(--size)) 50% 50% /
var(--size) var(--size);
mask: linear-gradient(-20deg, transparent 50%, white);
top: 0;
transform-style: flat;
pointer-events: none;
z-index: -1;
}
.bear-link {
color: canvasText;
position: fixed;
top: 1rem;
left: 1rem;
width: 48px;
aspect-ratio: 1;
display: grid;
place-items: center;
opacity: 0.8;
}
:where(.x-link, .bear-link):is(:hover, :focus-visible) {
opacity: 1;
}
.bear-link svg {
width: 75%;
}
/* 实用工具类 */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
}
</style>
</head>
<body>
<h1 class="fluid">UI 工艺</h1>
<p>
解锁界面开发的艺术与科学。这不仅仅是关于像素堆叠或遵循文档——而是关于掌握工具、理解细微差别,并有意识地塑造用户体验。
</p>
<ul>
<li data-active="true">
<article>
<h3>工艺</h3>
<p>
获得构建任何您想象内容的信心,将动效、交互和设计原则转化为第二天性。
</p>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M6 3h12l4 6-10 13L2 9Z" />
<path d="M11 3 8 9l4 13 4-13-3-6" />
<path d="M2 9h20" />
</svg>
<a href="#">
<span>立即观看</span>
</a>
<img src="https://picsum.photos/720/720?random=12" alt="" />
</article>
</li>
<li>
<article>
<h3>CSS 动画</h3>
<p>
从您的第一组 @keyframes 开始掌握 CSS 动画,直到其他人从未教过您的技巧。
</p>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M7 3v18" />
<path d="M3 7.5h4" />
<path d="M3 12h18" />
<path d="M3 16.5h4" />
<path d="M17 3v18" />
<path d="M17 7.5h4" />
<path d="M17 16.5h4" />
</svg>
<a href="#"><span>立即观看</span></a>
<img src="https://picsum.photos/720/720?random=17" alt="" />
</article>
</li>
<li>
<article>
<h3>SVG 滤镜</h3>
<p>
预算内的着色器。学习如何利用噪点优势,同时制作火焰和贴纸效果。
</p>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
</svg>
<a href="#"><span>立即观看</span></a>
<img src="https://picsum.photos/720/720?random=19" alt="" />
</article>
</li>
<li>
<article>
<h3>滚动动画</h3>
<p>
通过精美的滚动动画带领用户踏上旅程。您甚至可能不需要 JavaScript。
</p>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M19 17V5a2 2 0 0 0-2-2H4" />
<path
d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"
/>
</svg>
<a href="#"><span>立即观看</span></a>
<img src="https://picsum.photos/720/720?random=42" alt="" />
</article>
</li>
<li>
<article>
<h3>驾驭 Canvas</h3>
<p>
掌握如何驾驭像素游乐场以及何时使用它。同时实践"性能驱动开发"。
</p>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="13.5" cy="6.5" r=".5" fill="currentColor" />
<circle cx="17.5" cy="10.5" r=".5" fill="currentColor" />
<circle cx="8.5" cy="7.5" r=".5" fill="currentColor" />
<circle cx="6.5" cy="12.5" r=".5" fill="currentColor" />
<path
d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"
/>
</svg>
<a href="#"><span>立即观看</span></a>
<img src="https://picsum.photos/720/720?random=128" alt="" />
</article>
</li>
<li>
<article>
<h3>布局技巧</h3>
<p>
您真的需要一个库来实现那个功能吗?有时退一步重新思考问题会产生一个巧妙的解决方案。
</p>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72"
/>
<path d="m14 7 3 3" />
<path d="M5 6v4" />
<path d="M19 14v4" />
<path d="M10 2v2" />
<path d="M7 8H3" />
<path d="M21 16h-4" />
<path d="M11 3H9" />
</svg>
<a href="#"><span>立即观看</span></a>
<img src="https://picsum.photos/720/720?random=56" alt="" />
</article>
</li>
<li>
<article>
<h3>掌握时间</h3>
<p>
不仅仅是缓动和组合。时间在各种 UI 模式中扮演着关键角色,这些模式起初可能并不明显。
</p>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 22h14" />
<path d="M5 2h14" />
<path
d="M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22"
/>
<path
d="M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2"
/>
</svg>
<a href="#"><span>立即观看</span></a>
<img src="https://picsum.photos/720/720?random=39" alt="" />
</article>
</li>
</ul>
<a
class="bear-link"
href="https://twitter.com/intent/follow?screen_name=jh3yy"
target="_blank"
rel="noreferrer noopener"
>
<svg
class="w-9"
viewBox="0 0 969 955"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="161.191"
cy="320.191"
r="133.191"
stroke="currentColor"
stroke-width="20"
></circle>
<circle
cx="806.809"
cy="320.191"
r="133.191"
stroke="currentColor"
stroke-width="20"
></circle>
<circle
cx="695.019"
cy="587.733"
r="31.4016"
fill="currentColor"
></circle>
<circle
cx="272.981"
cy="587.733"
r="31.4016"
fill="currentColor"
></circle>
<path
d="M564.388 712.083C564.388 743.994 526.035 779.911 483.372 779.911C440.709 779.911 402.356 743.994 402.356 712.083C402.356 680.173 440.709 664.353 483.372 664.353C526.035 664.353 564.388 680.173 564.388 712.083Z"
fill="currentColor"
></path>
<rect
x="310.42"
y="448.31"
width="343.468"
height="51.4986"
fill="#FF1E1E"
></rect>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M745.643 288.24C815.368 344.185 854.539 432.623 854.539 511.741H614.938V454.652C614.938 433.113 597.477 415.652 575.938 415.652H388.37C366.831 415.652 349.37 433.113 349.37 454.652V511.741L110.949 511.741C110.949 432.623 150.12 344.185 219.845 288.24C289.57 232.295 384.138 200.865 482.744 200.865C581.35 200.865 675.918 232.295 745.643 288.24Z"
fill="currentColor"
></path>
</svg>
</a>
<div class="footer">
闽ICP备2025097315号-1
</div>
<script type="module">
import { Pane } from 'https://cdn.skypack.dev/tweakpane@4.0.4'
// 配置对象
const config = {
theme: 'system', // 默认主题为系统设置
}
// 创建控制面板
const ctrl = new Pane({
title: '配置',
expanded: true,
})
// 更新主题函数
const update = () => {
document.documentElement.dataset.theme = config.theme
}
// 同步变化函数
const sync = (event) => {
// 如果不支持视图过渡或不是主题变更,直接更新
if (
!document.startViewTransition ||
event.target.controller.view.labelElement.innerText !== 'Theme'
)
return update()
// 使用视图过渡效果更新
document.startViewTransition(() => update())
}
// 添加主题选择控件
ctrl.addBinding(config, 'theme', {
label: '主题',
options: {
系统: 'system',
浅色: 'light',
深色: 'dark',
},
})
// 监听变化事件
ctrl.on('change', sync)
// 初始更新
update()
// 获取列表和项目元素
const list = document.querySelector('ul')
const items = list.querySelectorAll('li')
// 设置活动项索引
const setIndex = (event) => {
const closest = event.target.closest('li')
if (closest) {
const index = [...items].indexOf(closest)
// 创建新的网格列模板
const cols = new Array(list.children.length)
.fill()
.map((_, i) => {
// 设置活动状态
items[i].dataset.active = (index === i).toString()
return index === i ? '10fr' : '1fr' // 活动项占10份,其他占1份
})
.join(' ')
// 应用新的网格布局
list.style.setProperty('grid-template-columns', cols)
}
}
// 添加事件监听器
list.addEventListener('focus', setIndex, true)
list.addEventListener('click', setIndex)
list.addEventListener('pointermove', setIndex)
// 重新同步项目宽度
const resync = () => {
const w = Math.max(
...[...items].map((i) => {
return i.offsetWidth
})
)
list.style.setProperty('--article-width', w)
}
// 窗口大小变化时重新同步
window.addEventListener('resize', resync)
// 初始同步
resync()
</script>
</body>
</html>
index.html
style.css
index.js