目录
一、弧形边框选项卡
二、零宽字符
三、目录滚动时自动高亮
四、高亮关键字
五、文字描边
六、按钮边框的旋转动画
七、视频文字特效
八、立体文字特效+让文字立起来
九、文字连续光影特效
十、重复渐变的边框
十一、磨砂玻璃效果
十二、FLIP动画
一、弧形边框选项卡
<style>
.tab {
width: 150px;
height: 40px;
color: #fff;
text-align: center;
line-height: 40px;
margin: 0 auto;
background: #ed6a5e;
border-radius: 10px 10px 0 0;
position: relative;
transform: perspective(30px) rotateX(13deg);
transform-origin: center bottom; /* 以tab最下方的线为中心进行旋转 */
}
.tab::before,
.tab::after {
content: "";
position: absolute;
width: 10px;
height: 10px;
bottom: 0;
background: #000;
}
.tab::before {
left: -10px;
background: radial-gradient(
circle at 0 0,
transparent 10px,
#ed6a5e 10px
); /* 左边以左上角为圆点进行径向渐变 */
}
.tab::after {
right: -10px;
background: radial-gradient(
circle at 100% 0,
transparent 10px,
#ed6a5e 10px
); /* 右边以右上角为圆点进行径向渐变 */
}
</style>
二、零宽字符
<script>
// 判断字符串中是否包含零宽字符
function containsZeroWidthCharacter(str) {
// 正则表达式匹配零宽字符
const zeroWidthRegex = /[\u200B\u200C\u200D\uFEFF]/g;
return zeroWidthRegex.test(str);
}
console.log(containsZeroWidthCharacter("Hello\u200B World")); // true
console.log(containsZeroWidthCharacter("Hello World")); // false
</script>
常见的零宽字符包括:
1、零宽空格【Unicode:U+200B】
这是一个没有宽度的空格,可以在两个字符之间插入,但不会在视觉上产生间隔。
2、零宽非连接符【Unicode:U+200C】
用于阻止字符连接。在阿拉伯语或印地语中,用来控制哪些字符应该连接,哪些不应连接。
3、零宽连接符【Unicode:U+200D】
用于强制某些字符连接在一起,这通常在一些复合字符或表情符号中起作用。与2相反
4、零宽非换行空格【Unicode:U+FEFF】
用于文件中的字节顺序标记,但也可以用作零宽空格的一种形式。作用是防止换行。
尽管零宽字符对用户不可见,但它们会占用存储空间,通常用于文本隐写、防止链接自动化处理、格式化和排版。比如文本处理时加上零宽字符,可以防止文本被盗窃,解码后是自己的名称。
三、目录滚动时自动高亮
<style>
body {
display: flex;
margin: 0;
}
/* 左侧目录 */
.sidebar {
width: 250px;
background-color: #333;
color: white;
padding: 20px;
height: 100vh;
position: fixed;
top: 0;
left: 0;
overflow-y: auto;
}
.sidebar a {
color: white;
text-decoration: none;
display: block;
padding: 10px;
margin: 5px 0;
}
.sidebar a:hover {
background-color: #575757;
}
.highlight {
background-color: #ffcc00; /* 高亮颜色 */
}
/* 右侧内容 */
.content {
margin-left: 280px; /* 留出左侧目录栏空间 */
padding: 20px;
width: calc(100% - 260px);
}
h1 {
color: #333;
}
.section {
margin-bottom: 30px;
height: 750px;
border: 1px solid yellowgreen;
}
.section h2 {
color: #444;
}
</style>
<body>
<div class="sidebar toc">
<h2>目录</h2>
<a href="#section1">部分 1</a>
<a href="#section2">部分 2</a>
<a href="#section3">部分 3</a>
<a href="#section4">部分 4</a>
</div>
<div class="content">
<div class="section" id="section1">
<h2>部分 1</h2>
<p>第一部分内容。</p>
</div>
<div class="section" id="section2">
<h2>部分 2</h2>
<p>第二部分内容。</p>
</div>
<div class="section" id="section3">
<h2>部分 3</h2>
<p>第三部分内容。</p>
</div>
<div class="section" id="section4">
<h2>部分 4</h2>
<p>第四部分内容。</p>
</div>
</div>
<script>
function highlight(id) {
document
.querySelectorAll("a.highlight")
.forEach((a) => a.classList.remove("highlight"));
if (id instanceof HTMLElement) {
id.classList.add("highlight");
return;
}
if (id.startsWith("#")) {
id = id.substring(1);
}
document.querySelector(`a[href="#${id}"]`).classList.add("highlight");
}
const links = document.querySelectorAll('.toc a[href^="#"]');
const titles = [];
for (const link of links) {
link.addEventListener("click", () => {
highlight(link.getAttribute("href").substring(1));
});
const url = new URL(link.href);
const dom = document.querySelector(url.hash);
if (dom) {
titles.push(dom);
}
}
function debounce(fn, delay = 100) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
const scrollHandler = debounce(() => {
const rects = titles.map((title) => title.getBoundingClientRect());
const range = 300;
for (let i = 0; i < titles.length; i++) {
const title = titles[i];
const rect = rects[i];
// 标题区域在指定范围内就高亮
if (rect.top >= 0 && rect.top <= range) {
highlight(title.id);
break;
}
// 当前内容标题在展示视口之上,并且下一个标题在展示视口之下,此时高亮此标题
if (
rect.top < 0 &&
rects[i + 1] &&
rects[i + 1].top > document.documentElement.clientHeight
) {
highlight(title.id);
break;
}
}
}, 100);
window.addEventListener("scroll", scrollHandler);
</script>
</body>
四、高亮关键字
<style>
.highlight {
color: red;
font-weight: bold;
}
</style>
<body>
<div>
<input type="text" class="txtKeyword" />
<ul></ul>
</div>
<script>
const ul = document.querySelector("ul");
const txtKeyword = document.querySelector(".txtKeyword");
function setHTML(lists) {
ul.innerHTML = lists
.map((l) => {
let cname = l.cname;
// 如果输入框有内容,则进行高亮匹配
if (txtKeyword.value) {
const reg = new RegExp(txtKeyword.value, "ig");
cname = cname.replace(reg, function (key) {
return `<span class="highlight">${key}</span>`;
});
}
return `<li><span>${cname}</span></li>`;
})
.join(""); // 使用 join 合并生成的 HTML 字符串
}
const NameLists = [
{ cname: "前端" },
{ cname: "后端" },
{ cname: "测试员" },
{ cname: "运维师" },
];
// 筛选包含关键字的元素
function filterList() {
const keyword = txtKeyword.value.trim();
if (keyword) {
const filtered = NameLists.filter((item) =>
item.cname.match(new RegExp(keyword, "i"))
);
setHTML(filtered);
} else {
setHTML(NameLists);
}
}
setHTML(NameLists);
// 给输入框添加监听事件,以便动态更新
txtKeyword.addEventListener("input", filterList);
</script>
</body>
五、文字描边
第一种text-shadow:给8个方向即可,但是连接处有瑕疵(见红色边缘部分)
@mixin text-stroke($color: #fff, $width: 1px) {
text-shadow: 0 -#{$width} #{$color}, #{$width} 0 #{$color},
0 #{$width} #{$color}, -#{$width} 0 #{$color}, #{$width} #{$width} #{$color},
-#{$width} -#{$width} #{$color}, #{$width} -#{$width} #{$color},
-#{$width} #{$width} #{$color};
}
p {
font-size: 50px;
font-weight: bold;
@include text-stroke(red, 2px);
// color: transparent; 不支持文字透明
}
p {
font-size: 50px;
font-weight: bold;
text-shadow: 0 -2px gold, 2px 0 gold, 0 2px gold, -2px 0 gold,
/* 上、右、下、左 */ 2px 2px gold, -2px -2px gold, 2px -2px gold,
-2px 2px gold; /* 四个对角线 */
}
第二种-webkit-text-stroke不仅边缘平滑,并且支持透明
p {
font-size: 50px;
font-weight: bold;
-webkit-text-stroke: 2px red;
color: transparent; //支持文字透明
position: relative;
}
// -webkit-text-stroke是居中描边,原来字体变小了,可以在"画一层"盖上去
p::after {
content: attr(data-text);
position: absolute;
left: 0;
top: 0;
-webkit-text-stroke: 0;
}
六、按钮边框的旋转动画
原理:在按钮层级下加一个矩形,围绕按钮中心进行360度旋转,多余矩形隐藏
<style>
button {
width: 100px;
height: 50px;
color: white;
outline: none;
z-index: 1;
border-radius: 10px;
cursor: pointer;
background: black;
/* outline: 4px solid gold; */
position: relative;
overflow: hidden;
}
button::before {
content: "";
position: absolute;
width: 200%;
height: 200%;
background: blue;
z-index: -2;
left: 50%;
top: 50%;
transform-origin: left top;/* 圆点在左上角 */
animation: rotation 2s linear infinite;
}
button::after {
content: "";
position: absolute;
--g: 4px;
width: calc(100% - var(--g) * 2);
height: calc(100% - var(--g) * 2);
background: black;
left: var(--g);
top: var(--g);
border-radius: inherit;
z-index: -1;
}
@keyframes rotation {
to {
transform: rotate(360deg);
}
}
</style>
七、视频文字特效
<style>
.txt{
position:absolute;
inset: 0;
background: #fff;
display: flex;
justify-content: center;
align-items: center;
mix-blend-mode: screen; /* 增强亮度,使图像或元素显得更加明亮 */
}
</style>
<body>
<div class="container">
<video src="./fire.mp4" autoplay muted></video>
<div class="txt">大前端</div>
</div>
</body>
八、立体文字特效+让文字立起来
放大看是叠加出来的,真正要做得建模
<style>
body {
background-color: brown;
color: #fff;
padding: 30px;
}
.text1 {
font-size: 5em;
text-shadow: -1px 1px #bbb, -2px 2px #bbb, -3px 3px #bbb, -4px 4px #bbb,
-5px 5px #bbb, -10px 10px 3px #0008;
}
.text2 {
font-weight: 700;
position: relative;
}
.text2::after {
content: "DARKNESS";
position: absolute;
left: 0;
top: 0;
color: #000;
transform: translate(-25px, 2px) scale(0.9) skew(50deg);
z-index:-1;
filter: blur(2px);
mask:linear-gradient(transparent,#000)
}
</style>
<body>
<h1 class="text1">立体文字</h1>
<h1 class="text2">DARKNESS</h1>
</body>
九、文字连续光影特效
span {
color: #faebd7;
animation: colorChange 1s infinite alternate;
}
@keyframes colorChange {
to {
color: #ff0266;
}
}
@for $i from 1 through 7 {
span:nth-child(#{$i}) {
animation-delay: ($i - 1) * 0.1s;
}
}
十、重复渐变的边框
<style>
.card {
width: 217px;
margin: 0 auto;
color: #333;
line-height: 1.8;
border-radius: 10px;
background: repeating-linear-gradient(
-45deg,
#e8544d 0 10px,
#fff 10px 20px,
#75adf8 20px 30px,
#fff 30px 40px
) -20px -20px/200% 200%;
padding: 5px;
transition: 0.5s;
}
.card:hover {
background-position: 0 0;
}
.container {
background: #fff;
border-radius: inherit;
}
</style>
<body>
<div class="card">
<div class="container">
重复渐变的边框原理:<br/>
设置背景为重复的线性渐变。渐变角度为-45度,包含四个颜色区块。
</div>
</div>
</body>
十一、磨砂玻璃效果
<style>
.wrap {
text-align: center;
color: white;
}
.modal {
background: rgba(255, 255, 255, 0.4); /* 半透明背景 */
backdrop-filter: blur(10px); /* 模糊背景 */
border-radius: 15px; /* 圆角 */
padding: 40px;
width: 300px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); /* 增加阴影 */
font-size: 24px;
}
</style>
<body>
<div class="wrap">
<div class="modal">磨砂玻璃效果</div>
</div>
</body>
十二、各种FLIP动画
参考:https://zhuanlan.zhihu.com/p/712766286
<style>
.box {
width: 60px;
height: 60px;
background-color: skyblue;
color: white;
font-size: 30px;
margin: 10px;
}
</style>
<body>
<div class="container" style="display: flex">
<div class="box" key="1">1</div>
<div class="box" key="2">2</div>
<div class="box" key="3">3</div>
<div class="box" key="4">4</div>
<div class="box" key="5">5</div>
</div>
<button onclick="shuffle()">打乱</button>
<script>
function shuffle() {
const container = document.querySelector(".container");
const boxes = Array.from(container.children);
// First: 记录每个盒子的起始位置
const startPositions = boxes.reduce(
(result, box) => ({
...result,
[box.getAttribute("key")]: box.getBoundingClientRect(),
}),
{}
);
// 随机打乱盒子顺序,然后把打乱好的盒子放回 DOM
boxes.sort(() => Math.random() - 0.5);
boxes.forEach((box) => container.appendChild(box));
// Last: 记录每个盒子的最终位置
const endPositions = boxes.reduce(
(result, box) => ({
...result,
[box.getAttribute("key")]: box.getBoundingClientRect(),
}),
{}
);
// Invert: 计算 “反向” 偏移量
boxes.forEach((box) => {
const key = box.getAttribute("key");
const start = startPositions[key];
const end = endPositions[key];
// 注意,此时 DOM 已经处于最终位置,所以它的 translate 是 “反向” 的
// 所以要用 first 来减去 last
const deltaX = start.left - end.left;
const deltaY = start.top - end.top;
// 如果元素 “原地不动”,那么跳过后续流程
if (deltaX === 0 && deltaY === 0) {
return;
}
// 让元素通过 transform 偏移回到起点
box.style.transition = null; // 暂时屏蔽掉过渡,实际生产此处需完善
box.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
// Play: 在重绘之前,撤掉 transform 偏移,播放 “归位” 过渡动画
requestAnimationFrame(() => {
box.style.transition = `transform 2s`;
box.style.transform = "";
});
// FLIP 动画完成后,清理残余样式
box.addEventListener(
"transitionend",
() => {
box.style.transition = null;
box.style.transform = null;
},
{ once: true }
);
});
}
</script>
</body>
其中的“Invert” 和 “Play” 步骤可以使用 Web Animation API 进行简化
<style>
.box {
width: 60px;
height: 60px;
color: white;
font-size: 30px;
margin: 10px;
box-sizing: border-box;
background-color: skyblue;
border: 2px black solid;
transition: width 500ms, height 500ms;
}
.scale {
position: absolute;
top: 90px;
left: 10px;
width: 120px;
height: 120px;
z-index: 10;
}
</style>
<body>
<div class="container" style="display: flex">
<div class="box" key="1">1</div>
<div class="box" key="2">2</div>
<div class="box" key="3">3</div>
<div class="box" key="4">4</div>
<div class="box" key="5">5</div>
</div>
<script>
const container = document.querySelector('.container')
const boxes = Array.from(container.children)
boxes.forEach(box => {
box.addEventListener('click', () => {
// First: 记录每个盒子的起始位置
const startPositions = boxes.reduce(
(result, box) => ({
...result,
[box.getAttribute('key')]: box.getBoundingClientRect(),
}),
{}
)
box.classList.toggle('scale')
// Last: 记录每个盒子的最终位置
const endPositions = boxes.reduce(
(result, box) => ({
...result,
[box.getAttribute('key')]: box.getBoundingClientRect(),
}),
{}
)
// Invert: 计算 “反向” 偏移量
boxes.forEach(box => {
const key = box.getAttribute('key')
const start = startPositions[key]
const end = endPositions[key]
// 注意,此时 DOM 已经处于最终位置,所以它的 transform 是 “反向” 的
// 所以要用 first 来减去 last
const deltaX = start.left - end.left
const deltaY = start.top - end.top
// 如果元素 “原地不动”,那么跳过后续流程
if (deltaX === 0 && deltaY === 0) {
return
}
// 将盒子通过 transform 移至初始位置
box.style.transition = ''
box.style.transform = `translate(${deltaX}px, ${deltaY}px)`
// Play: 播放动画应用变换
requestAnimationFrame(() => {
box.style.transition = `all 500ms`
box.style.transform = ''
})
// FLIP 动画完成后,清理残余样式
box.addEventListener(
'transitionend',
() => {
box.style.transition = null
box.style.transform = null
},
{ once: true }
)
})
})
})
</script>
</body>
Vue 内置组件 <TransitionGroup>
已经实现了 FLIP 动画
<template>
<TransitionGroup style="display: flex" name="flip" tag="div">
<div class="box" v-for="item of list" :key="item">
{{ item }}
</div>
</TransitionGroup>
<button @click="shuffle">打乱</button>
</template>
<script setup>
import { reactive } from 'vue'
const list = reactive([1, 2, 3, 4, 5])
const shuffle = () => void list.sort(() => Math.random() - 0.5)
</script>
<style scoped>
.box {
width: 60px;
height: 60px;
background-color: skyblue;
color: white;
font-size: 30px;
margin: 10px;
}
.flip-move {
transition: all 2s;
}
</style>
react-flip-toolkit
工具是一个用于实现组件 FLIP 动画的 React 库。