#萌萌专注钟:用 React + Motion 打造一个治愈系番茄钟
先说结论:专注不一定要苦大仇深。
市面上的番茄钟应用,要么极简到只剩数字,要么功能臃肿像企业软件。我想做一个不一样的——一个让你看一眼就想开始学习的番茄钟。它有猫耳朵、会眨眼、会脸红,专注完成的时候还会撒彩带。
这就是「萌萌专注钟」。
在线体验:AI Studio 应用
##它能做什么
###三种模式
| 模式 | 默认时长 | 配色 | 用途 |
|---|---|---|---|
| 专注 | 25 分钟 | 珊瑚粉 #F4A896click to copy | 深度工作 |
| 短休 | 5 分钟 | 薄荷绿 #B8E0D2click to copy | 放松眼睛 |
| 长休 | 15 分钟 | 薰衣草紫 #E2D1F9click to copy | 彻底休息 |
###核心功能
- ●可视化计时器:SVG 圆形进度条 + 猫脸表情动画
- ●任务管理:添加、完成、删除,实时统计完成度
- ●深色模式:一键切换,所有颜色自适应
- ●完成彩带:专注结束时
canvas-confetticlick to copy撒花 - ●每日名言:随机展示一句激励话语
- ●自定义时长:三种模式的时间都可以调节
##技术栈
React 19 + TypeScript 5.8
Tailwind CSS 4.1 + Motion 12(动画)
Lucide React(图标)+ Canvas Confetti(彩带)
Vite 6(构建)+ Express 4(服务端)
click to copy
整个应用只有一个 App.tsxclick to copy,约 700 行,没有路由、没有状态管理库、没有后端 API。全部状态用 useStateclick to copy 管理,动画交给 Motion。
这是有意为之的——一个专注工具不应该让自己变得复杂。
##设计:治愈系的视觉系统
###配色方案
四种颜色撑起整个界面:
珊瑚色 #F4A896 → 专注模式主色,温暖有活力
薄荷色 #B8E0D2 → 短休模式主色,清爽放松
薰衣草 #E2D1F9 → 长休模式主色,柔和梦幻
金色 #D4A574 → 装饰色,点缀用
click to copy
每种模式切换时,所有 UI 元素——进度条、按钮、卡片、甚至背景中漂浮的装饰圆点——都会跟着变色。这不是简单的换色,而是整个氛围的转变。
###猫脸计时器
这是整个应用最核心的视觉元素。一个 320px 的圆形,里面藏着:
- ●猫耳朵:两个倾斜的白色圆角三角,顶部露在圆外
- ●眼睛:两个小黑点,专注时会有节奏地眨眼(
scaleYclick to copy在 1 和 0.1 之间循环) - ●腮红:眼睛下方的半透明椭圆,颜色跟随当前模式
- ●嘴巴:底部的小弧线,专注时会微微张合
- ●进度环:SVG 圆环,用
strokeDashoffsetclick to copy动画显示剩余时间
外围还有缓慢旋转的虚线装饰环和上下漂浮的彩色圆点,营造梦幻感。
###玻璃态卡片
任务卡片使用毛玻璃效果:
/* 亮色模式 */
.glass-card {
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.06);
}
半透明白底 + 背景模糊 + 柔和阴影,让卡片看起来漂浮在渐变背景之上。
###字体选择
--font-heading: 'Playfair Display'; /* 标题,优雅衬线 */
--font-body: 'LXGW WenKai'; /* 正文,霞鹜文楷 */
--font-mono: 'JetBrains Mono'; /* 时间数字,等宽 */
霞鹜文楷是关键。它给整个应用定下了「手写感、亲切感」的基调,和猫脸的设计语言完美契合。
##技术实现:关键细节
###1. 计时器逻辑
用 useEffectclick to copy + setIntervalclick to copy 实现,每一秒 timeLeftclick to copy 减 1。归零时触发完成处理:
useEffect(() => {
if (isActive && timeLeft > 0) {
timerRef.current = setInterval(() => {
setTimeLeft((prev) => prev - 1);
}, 1000);
} else if (timeLeft === 0) {
handleTimerComplete();
}
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, [isActive, timeLeft]);
handleTimerCompleteclick to copy 做三件事:暂停计时器、播放提示音、撒彩带。
###2. SVG 圆形进度条
这是纯数学 + CSS 动画的组合:
const circumference = 2 * Math.PI * 140; // 半径 140 的圆周长
const progress = ((totalTime - timeLeft) / totalTime) * circumference;
// strokeDasharray = 圆周长,strokeDashoffset = 圆周长 - 已走过长度
<motion.circle
strokeDasharray={circumference}
animate={{ strokeDashoffset: circumference - progress }}
transition={{ duration: 0.5, ease: 'easeOut' }}
/>
用 Motion 的 animateclick to copy 属性驱动 strokeDashoffsetclick to copy 的变化,每次更新都有 0.5 秒的缓动过渡,进度条不会突然跳变。
###3. 模式切换动画
模式切换按钮使用 Motion 的 layoutIdclick to copy 实现共享布局动画:
{mode === m && (
<motion.div
className="absolute inset-0 rounded-xl bg-gradient-to-br gradient opacity-10"
layoutId="activeMode"
transition={{ type: 'spring', bounce: 0.2, duration: 0.6 }}
/>
)}
当你从「专注」切到「短休」,高亮背景会以弹性动画滑过去,而不是突然消失再出现。
###4. 任务列表的增删动画
任务项使用 AnimatePresenceclick to copy + layoutclick to copy 属性:
<AnimatePresence mode="popLayout">
{tasks.map((task) => (
<motion.div
key={task.id}
layout // 自动处理列表重排动画
initial={{ opacity: 0, x: -20, height: 0 }}
animate={{ opacity: 1, x: 0, height: 'auto' }}
exit={{ opacity: 0, scale: 0.95, height: 0 }}
>
新增任务从左滑入,删除时缩小消失,列表重排时自动平滑过渡。
###5. 彩带庆祝
专注完成时的彩带效果:
if (mode === 'focus') {
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.6 },
colors: ['#FDA4AF', '#F0ABFC', '#67E8F9', '#F4A896', '#B8E0D2']
});
}
注意颜色数组,用的是和主题一致的五种色彩,而不是默认的红黄蓝。细节决定质感。
###6. 深色模式
所有颜色通过 isDarkModeclick to copy 状态三元表达式切换:
isDarkMode
? 'bg-[#2A2A2A] hover:bg-[#3A3A3A] text-[#B8ADA3]'
: 'bg-white/60 hover:bg-white text-[#8B7E74]'
深色模式没有简单地反转颜色,而是用了一套独立的暗色调色板。背景变为暖灰,文字变为暖白,保持「治愈感」不变。
##动画编排
整个页面的入场动画是精心编排的:
0.0s Header 标题从左滑入
0.1s Header 按钮从右滑入
0.2s 时钟从中心缩放淡入
0.3s 模式切换器淡入
0.3s 右侧任务卡片从右滑入
0.4s 控制按钮从下滑入
0.5s 名言卡片淡入
0.8s 页脚淡入
click to copy
每个元素都有延迟,形成从中心向外扩散的入场节奏。
##响应式布局
使用 12 列 Grid 系统:
<main className="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-12">
{/* 左侧计时器:7列 */}
<div className="lg:col-span-7 flex flex-col items-center">
{/* 时钟 */}
</div>
{/* 右侧任务+名言:5列 */}
<div className="lg:col-span-5 space-y-6">
{/* 任务卡片 */}
{/* 名言卡片 */}
</div>
</main>
桌面端左右分栏,移动端上下堆叠。时钟尺寸也做了响应式调整:w-72 h-72 md:w-80 md:h-80click to copy。
##项目结构
src/
├── main.tsx # 入口,挂载 React
├── App.tsx # 全部逻辑和组件(~700行)
└── index.css # 全局样式、自定义动画关键帧
click to copy
是的,只有一个组件文件。对于这种规模的应用,拆分反而增加认知负担。所有常量(MODE_CONFIGclick to copy、QUOTESclick to copy)都定义在文件顶部,类型定义紧跟其后,组件嵌套在主函数内部。
##写在最后
这个项目的核心设计理念很简单:让专注成为一件愉快的事。
它不是一个追求功能大而全的生产力工具。它只想做一件事——在你决定专注的那一刻,给你一个温暖的、有生命力的陪伴。
猫耳朵会动,腮红会变色,专注结束会撒花。这些「无用」的细节,恰恰是让一个工具从「能用」变成「喜欢用」的关键。
技术是实现手段,不是目的。选择 React + Motion 是因为它们能让我用最少的代码实现最流畅的动画。选择单文件架构是因为对于这个规模的应用,简单就是最好的架构。
如果你也想做一个让自己开心的 Side Project,我的建议是:别想太多,先让它跑起来,然后把它做得好看。
项目基于 Apache 2.0 协议开源,使用 React 19 + TypeScript + Tailwind CSS 4 + Motion 构建。