1. 这不是技术选型指南,而是一份前端团队三年踩坑实录
我带过三个不同规模的前端团队,从百人电商中台到二十人初创 SaaS,再到十人内部工具组。2021 年我们还在用 BEM 写 CSS,2022 年强行上 CSS-in-JS 被 React Server Components 打得措手不及,2023 年在 Tailwind 上重构了三套系统后,终于敢说:没有“最好”的 CSS 方案,只有“最不痛”的当下解法。标题里那个“2026 年到底该怎么选”,问得特别实在——不是问理论优劣,是问你现在手头这个项目、这个团队、这个交付周期,明天早上 standup 时该拍板用什么。BEM 不是过时了,是它解决的问题(命名冲突、样式隔离)在组件化时代被拆解了;CSS-in-JS 也没死,只是它承诺的“样式即逻辑”在服务端渲染和微前端里变成了负债;Tailwind 更不是银弹,当你的设计系统有 47 种按钮变体、12 套主题配色、3 层响应式断点嵌套时,class="bg-blue-500 hover:bg-blue-600 active:bg-blue-700 disabled:opacity-50 sm:px-4 md:px-6 lg:px-8"这种写法会直接拖垮代码审查效率。真正决定选型的,从来不是框架文档里的性能 benchmark,而是你团队里 junior 开发者改一个按钮圆角要花多少分钟、designer 提供的 Figma 变体能否一键映射到 class、CI 流水线里样式 diff 的误报率有多高。这篇文章不讲“BEM vs Tailwind”的哲学辩论,只记录我们如何用真实项目数据(不是 benchmark,是 Jira 工单耗时、Code Review 拒绝率、Lighthouse 样式加载阻塞占比)倒推选型逻辑。如果你正为新项目纠结技术栈,或者被老项目里混杂的三种 CSS 方案折磨得睡不着,这篇就是为你写的。
2. 架构演进不是线性升级,而是问题域的迁移与拆解
2.1 BEM 的黄金年代:解决的是“全局污染”这个具体痛点
2015 年我们做第一个大型后台系统时,CSS 最大的敌人是#header .nav li a:hover这种选择器。当时团队刚从 jQuery 单页应用转型,HTML 结构由后端模板拼接,前端只负责局部交互。一个common.css文件被全站引入,设计师给的 UI 稿里“用户列表页的删除按钮”和“订单详情页的删除按钮”长得一模一样,但开发随手写了.delete-btn { color: red; },结果首页的“删除购物车商品”按钮也变红了。BEM 的价值根本不是“语义化”,而是用命名规则制造物理隔离:.user-list__delete-btn和.order-detail__delete-btn在源码层面就不可能冲突。我们当时定的规则极其粗暴:
- Block 名必须是业务模块名(
user-list,product-card) - Element 名必须是 Block 内原子级元素(
__header,__body,__footer) - Modifier 名必须是布尔状态或枚举值(
--disabled,--size-large)
这套规则能跑通,是因为我们刻意回避了两个难题:一是动态样式生成(比如根据用户等级显示不同颜色的 badge),二是跨 Block 复用(比如所有卡片都需要统一的阴影效果)。前者我们用 JS 动态切换 modifier 类名解决,后者我们复制粘贴.card--shadow到每个 Block 里。这听起来很蠢,但在当时,避免样式泄漏的收益远大于维护成本。BEM 的本质是“用命名空间换确定性”,它把 CSS 的不确定性(选择器权重、继承链)锁死在文件边界内。直到 2020 年我们接入微前端,主应用和子应用共用同一份 CSS,BEM 的命名空间才第一次失效——子应用的.user-list__delete-btn依然能覆盖主应用的同名类。
2.2 CSS-in-JS 的爆发:当组件成为第一公民,样式必须跟着搬家
2021 年我们启动新电商平台,技术栈全面转向 React + TypeScript。这时 BEM 的裂缝开始扩大:一个ProductCard组件需要根据库存状态显示不同背景色,设计师要求“缺货时背景渐变色从左到右由灰变透明”。BEM 要求我们写.product-card--out-of-stock,但这个 modifier 的触发条件是props.inventory <= 0,逻辑和样式耦合在 JS 里,却要靠 class 名传递状态。更糟的是,我们用了 Storybook 做组件库,设计师在 Storybook 里看到的ProductCard样式,和实际页面里因为父组件传入不同 props 渲染出的效果不一致——因为 BEM 的 class 是静态的,而状态是动态的。CSS-in-JS 就是为解决这个矛盾诞生的。我们选了 Emotion,不是因为它性能最好,而是它的css函数能无缝嵌入 JSX:
const ProductCard = ({ inventory }) => { const bgColor = inventory <= 0 ? 'linear-gradient(90deg, #999, transparent)' : '#fff'; return ( {/* content */} ); };这里的关键不是“样式写在 JS 里”,而是样式逻辑和组件逻辑共享同一套状态变量。当inventory改变时,bgColor自动重算,无需手动管理 class 切换。我们还用 Emotion 的@media支持实现了真正的响应式:
css` @media (min-width: 768px) { display: grid; grid-template-columns: repeat(3, 1fr); } @media (min-width: 1024px) { grid-template-columns: repeat(4, 1fr); } `这比媒体查询写在独立 CSS 文件里强得多——组件的响应式逻辑完全内聚。但代价很快显现:2022 年我们上线 SSR,发现 Emotion 生成的 class 名在服务端和客户端不一致,导致 FOUC(Flash of Unstyled Content);2023 年接入微前端,子应用的 Emotion 样式注入顺序无法控制,经常覆盖主应用的全局重置样式。CSS-in-JS 的核心假设是“组件即应用”,当这个假设被微前端、SSR、Web Components 打破时,它的优势就变成了枷锁。
2.3 Tailwind 的崛起:不是放弃抽象,而是把抽象层移到设计系统
2023 年我们重构内部 BI 系统,团队里来了两位资深 UI 工程师。他们看了我们用 Emotion 写的DashboardCard组件,第一句话是:“你们的css函数里写了 12 行样式,其中 8 行是padding,margin,border-radius,shadow—— 这些本该是设计系统的原子规则,为什么每个组件都要重复写?” 这句话点醒了我们。Tailwind 的本质不是“写 class 而不是写 CSS”,而是把 CSS 的抽象层级从开发者手中,交还给设计系统。我们不再定义.card { padding: 1rem; border-radius: 0.5rem; box-shadow: 0 2px 4px rgba(0,0,0,0.1); },而是让设计系统规定:
p-4=padding: 1rem(对应设计规范里的“标准间距”)rounded-lg=border-radius: 0.5rem(对应“大圆角”)shadow-md=box-shadow: 0 2px 4px rgba(0,0,0,0.1)(对应“中等阴影”)
然后在组件里直接组合:
销售概览
¥24,567 查看详情 这种写法的革命性在于:样式决策权从开发者回归到设计师。当设计师说“所有卡片统一用大圆角”,我们不需要 grep 全局代码改border-radius,只需要确保rounded-lg的定义不变;当需要新增“超大圆角”时,设计系统扩展rounded-xl,所有用到它的组件自动生效。我们用 Tailwind 的@layer机制把设计系统规则分层:
/* src/styles/tailwind.css */ @tailwind base; @tailwind components; @tailwind utilities; @layer components { .btn-primary { @apply px-4 py-2 bg-blue-600 text-white rounded font-medium hover:bg-blue-700 transition-colors; } .btn-secondary { @apply px-4 py-2 border border-gray-300 text-gray-700 rounded font-medium hover:bg-gray-50; } }这样既保留了 Tailwind 的原子性,又通过@apply封装了业务语义。Tailwind 不是消灭 CSS,而是把 CSS 的复杂度转移到设计系统治理上——这正是 2026 年前端团队的核心能力。
3. 2026 年的选型决策树:用四个真实维度代替“哪个更好”
3.1 维度一:团队成熟度——别让 juniors 为架构买单
我们做过一个残酷实验:让 5 名入职不满 6 个月的 junior 开发者,分别用 BEM、Emotion、Tailwind 实现同一个“带状态切换的 Tab 组件”。结果如下:
| 方案 | 平均完成时间 | Code Review 拒绝率 | 主要问题 |
|---|---|---|---|
| BEM | 42 分钟 | 60% | class 名拼错(tab__item--active写成tab__item-active)、modifier 未覆盖所有状态 |
| Emotion | 58 分钟 | 80% | css函数里忘记加;导致编译失败、媒体查询嵌套错误、样式优先级冲突(!important滥用) |
| Tailwind | 28 分钟 | 20% | flex和grid混用导致布局错乱、响应式断点写错(md:写成sm:) |
关键发现:Tailwind 对 junior 最友好,但前提是团队有清晰的 Tailwind 配置和设计系统文档。我们给新人配的不是“Tailwind 教程”,而是三样东西:
- 一份
tailwind.config.js注释版,标出哪些配置是设计系统强制项(如spacing: { '4': '1rem', '8': '2rem' }) - 一张 A4 纸打印的常用 class 速查表(
p-4,m-2,text-lg,bg-red-500等 50 个高频 class) - Storybook 里每个原子组件的 Tailwind class 映射表(如
Button组件的variant="primary"对应bg-blue-600 hover:bg-blue-700)
而 BEM 要求新人理解 Block/Element/Modifier 的哲学,Emotion 要求新人掌握 CSS-in-JS 的生命周期(服务端渲染时css函数执行时机)。2026 年的现实是:团队扩张速度远快于架构演进速度。如果你的团队平均司龄低于 18 个月,Tailwind 是唯一能降低认知负荷的选择。反之,如果团队有 3 名以上 CSS 专家,BEM 的可预测性反而能减少线上样式 bug——我们某个金融后台系统坚持用 BEM,因为它的样式变更必须经过 QA 逐像素验收,任何意外样式泄露都是 P0 事故。
3.2 维度二:交付节奏——紧急需求下,哪套方案能最快上线
去年双十一前两周,运营突然要求首页增加“限时秒杀”横幅,要求 48 小时上线。我们对比了三种方案的落地路径:
BEM 方案:
- 新建
promo-bannerBlock - 定义
__content,__timer,__ctaElements - 为
--countdown,--sold-outModifiers 写 CSS - 在首页 HTML 中插入
耗时:6.5 小时(主要卡在 BEM 命名评审和 CSS 重置兼容性测试)
Emotion 方案:
新建PromoBanner组件用css函数写样式,动态计算倒计时用useEffect处理倒计时逻辑耗时:3.2 小时(但上线后发现 SSR 下倒计时初始值为空,导致首屏无内容,紧急 hotfix)Tailwind 方案:
直接在首页 JSX 里写: 🔥 限时秒杀 最后 2 小时 立即抢购 耗时:47 分钟(包括设计师确认配色是否符合品牌规范)结论很现实:Tailwind 在紧急需求下胜出,但它的胜利建立在设计系统已就绪的前提下。如果这是你第一次用 Tailwind,临时查文档、试配色、调间距,时间可能翻倍。而 BEM 的“慢”恰恰是它的优势——强制你思考 Block 边界,避免未来三个月的维护债。2026 年的项目管理现实是:80% 的需求变更发生在上线前 72 小时。你的 CSS 架构必须回答一个问题:当产品经理凌晨两点发来新需求截图,你的团队是能立刻开干,还是先开一场架构评审会?
3.3 维度三:技术栈约束——别让 CSS 方案拖垮整个技术决策
我们曾为一个政府项目选型,技术栈要求:
必须支持 IE11(虽然 2026 年这很荒谬,但真实存在)后端是 Java Spring Boot,前端资源走 CDN,不允许 JS 动态注入样式安全审计要求所有 CSS 必须静态分析,禁止运行时生成 class这三条直接封杀了 CSS-in-JS 和 Tailwind 的 JIT 模式。我们最终用 BEM + PostCSS:
BEM 解决命名冲突PostCSS 插件postcss-custom-properties处理 CSS 变量降级postcss-preset-env自动添加 IE11 兼容前缀所有样式文件预编译,CDN 返回纯 CSS另一个极端案例:我们接入一个基于 Web Components 的遗留系统。它的 Shadow DOM 隔离了样式,BEM 的 class 名在 Shadow Root 里完全无效;Emotion 的css函数生成的 class 无法穿透 Shadow DOM;只有 Tailwind 的@layer base可以通过:host伪类注入全局样式:
@layer base { :host { --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); } }2026 年的技术现实是:前端不再是单一技术栈,而是混合生态。你的 CSS 方案必须能回答:它能否在微前端子应用里独立运行?能否在 SSR 场景下保证首屏样式稳定?能否与 Web Components 共存?能否被 Cypress 的样式快照测试覆盖?这些不是“高级功能”,而是 2026 年的 baseline 要求。
3.4 维度四:长期维护成本——用数据量化“技术债”
我们统计了过去三年三个项目的样式相关工单:
项目技术栈平均每月样式相关 bug 数平均修复时间主要类型电商后台(BEM)BEM + Sass2.318 分钟class 名冲突、modifier 漏写、媒体查询覆盖不全SaaS 平台(Emotion)Emotion + React5.742 分钟SSR 样式不一致、动态样式计算错误、CSS-in-JS 依赖版本冲突BI 系统(Tailwind)Tailwind + Next.js1.19 分钟响应式断点错用、设计系统更新后 class 未同步、JIT 模式缓存失效关键洞察:Tailwind 的 bug 数最少,但它的“bug”本质是设计系统治理问题。比如设计师更新了主色调,要求所有bg-blue-500改为bg-indigo-600,我们需要全局替换并验证所有组件。这看起来是运维工作,实则是设计系统健康度的晴雨表。而 BEM 的 bug 多是人为失误,Emotion 的 bug 多是框架行为不可控。我们为此建立了样式健康度仪表盘:
BEM 项目:监控class属性中 BEM 命名违规率(用 ESLint 插件eslint-plugin-bem)Emotion 项目:监控 SSR 与 CSR 的样式哈希差异率(自定义 Lighthouse audit)Tailwind 项目:监控未使用的 class 占比(tailwindcss的--watch模式日志)和设计系统 token 覆盖率(用@tailwindcss/typography的 token 检查器)2026 年的选型,本质上是在选择哪种技术债你更愿意承担:BEM 的债是人力成本(需要专人维护命名规范),Emotion 的债是不确定性(框架升级可能破坏样式),Tailwind 的债是治理成本(需要设计系统工程师)。没有免费的午餐,只有更透明的账单。
4. 实操指南:2026 年的混合架构落地策略
4.1 拒绝非此即彼:BEM + Tailwind 的共生模式
我们当前主力项目采用“BEM 为骨架,Tailwind 为血肉”的混合模式。核心逻辑是:BEM 管理 Block 边界和业务语义,Tailwind 管理原子样式。例如一个UserProfileCardBlock:

张三
张三 zhangsan@example.com 编辑资料 这里user-profile-card是 BEM Block,它定义了组件的业务语义和 DOM 结构边界;w-16 h-16 rounded-full是 Tailwind 原子样式,负责视觉表现;user-profile-card__action-btn--edit是 BEM Modifier,用于 JS 控制状态切换。这种混合的优势在于:
可测试性:BEM class 名稳定,E2E 测试可以精准定位元素(cy.get('.user-profile-card__action-btn--edit'))可维护性:Tailwind class 可以随时调整设计系统,不影响 BEM 结构可扩展性:新增user-profile-card--compactModifier 时,只需在 CSS 中定义.user-profile-card--compact .user-profile-card__header { display: none; },无需改动 JSX我们用 PostCSS 插件postcss-bem自动为 BEM class 添加前缀,避免与第三方库冲突;同时用@apply封装常用 Tailwind 组合:
/* src/styles/bem-tailwind.css */ @layer components { .user-profile-card__header { @apply flex flex-col items-center; } .user-profile-card__action-btn { @apply px-4 py-2 bg-blue-500 text-white rounded font-medium hover:bg-blue-600 transition-colors; } }这样既保持了 Tailwind 的原子性,又通过@apply提供了语义化封装。
4.2 CSS-in-JS 的现代用法:只在必要处注入逻辑
我们并没有抛弃 CSS-in-JS,而是把它降级为“逻辑驱动样式”的专用工具。场景非常明确:
动态计算样式:图表组件根据数据范围自动调整坐标轴颜色主题切换:深色/浅色模式下,CSS 变量无法满足的复杂渐变动画状态:需要 JS 控制的复杂贝塞尔曲线动画例如一个实时数据图表:
import { keyframes } from '@emotion/react'; const pulse = keyframes` 0% { opacity: 0.6; } 50% { opacity: 1; } 100% { opacity: 0.6; } `; const ChartPoint = ({ value, isLive }) => { // 只有 isLive 为 true 时才注入动画逻辑 const animationStyle = isLive ? { animation: `${pulse} 2s infinite` } : {}; return ( 100 ? '#ef4444' : '#10b981'}; ${animationStyle} `} style={animationStyle} /> ); };关键原则:CSS-in-JS 只处理“CSS 无法表达的逻辑”,其他一切交给 Tailwind 或 BEM。我们甚至写了一个 ESLint 规则,禁止在css函数里出现px,rem,color字面量——所有原子值必须来自设计系统 token。这样 CSS-in-JS 就从“样式编写方式”变成了“逻辑样式胶水”,大幅降低了它的使用频率和风险。
4.3 Tailwind 的企业级配置:超越tailwind.config.js
4.3 Tailwind 的企业级配置:超越tailwind.config.jsTailwind 在企业级项目中的成败,90% 取决于配置。我们的真实配置包含五个层次:
第一层:设计系统 token 映射
// tailwind.config.js module.exports = { theme: { extend: { colors: { // 严格对应设计系统 Figma 变量 primary: { DEFAULT: 'var(--color-primary)', 50: 'var(--color-primary-50)', 100: 'var(--color-primary-100)', // ... up to 900 }, // 业务语义色 status: { success: 'var(--color-status-success)', warning: 'var(--color-status-warning)', error: 'var(--color-status-error)', } }, spacing: { // 设计系统间距阶梯 '1': '0.25rem', // 4px '2': '0.5rem', // 8px '3': '0.75rem', // 12px '4': '1rem', // 16px '6': '1.5rem', // 24px '8': '2rem', // 32px } } } }第二层:响应式断点治理
// 我们不用默认的 sm/md/lg,而是按设备类型定义 theme: { screens: { mobile: '320px', tablet: '768px', desktop: '1024px', large: '1440px', // 业务特定断点 print: { raw: 'print' }, } }第三层:插件生态管控
plugins: [ require('@tailwindcss/forms'), // 表单重置 require('@tailwindcss/typography'), // 富文本样式 // 自研插件:防止滥用 function({ addUtilities }) { // 禁止直接使用 text-xs/text-sm/text-base,必须用设计系统字号 addUtilities({ '.text-body-sm': { 'font-size': '0.875rem', 'line-height': '1.25rem' }, '.text-body-md': { 'font-size': '1rem', 'line-height': '1.5rem' }, '.text-heading-lg': { 'font-size': '1.5rem', 'line-height': '2rem' }, }) } ]第四层:JIT 模式安全加固
// 生产环境禁用 JIT,用预编译保证稳定性 mode: process.env.NODE_ENV === 'production' ? 'aot' : 'jit', // 但开发环境启用 JIT,并限制扫描路径 content: [ './src/**/*.{js,jsx,ts,tsx}', // 明确排除 node_modules 和第三方库 '!./node_modules/**/*', '!./src/lib/**/*', // 第三方封装库 ],第五层:CI/CD 集成
Git Hook 检查:提交前运行npx tailwindcss -c tailwind.config.js --content ./src --check,确保所有 class 在配置中存在Storybook 集成:每个组件 Story 自动检测未使用的 Tailwind classLighthouse 自定义 audit:测量 Tailwind 生成的 CSS 文件体积,超过阈值(120KB)自动告警这套配置让 Tailwind 从“快捷键”变成了“企业级样式引擎”。
5. 常见问题与实战避坑指南
5. 常见问题与实战避坑指南5.1 “Tailwind 生成的 CSS 太大!”——这是配置问题,不是框架缺陷
5.1 “Tailwind 生成的 CSS 太大!”——这是配置问题,不是框架缺陷我们最初上线 Tailwind 时,生产 CSS 体积达 1.2MB,Lighthouse 样式加载评分 23。排查发现三个致命配置错误:
错误一:content 配置过于宽泛
// ❌ 错误:扫描整个 node_modules content: ['./src/**/*.{js,jsx,ts,tsx}', './node_modules/**/*'] // ✅ 正确:精确到组件目录,排除第三方 content: [ './src/components/**/*.{js,jsx,ts,tsx}', './src/pages/**/*.{js,jsx,ts,tsx}', './src/layouts/**/*.{js,jsx,ts,tsx}', '!./src/lib/**/*', '!./node_modules/**/*', ]错误二:启用了未使用的插件
// ❌ 错误:导入了所有插件 plugins: [ require('@tailwindcss/forms'), require('@tailwindcss/typography'), require('@tailwindcss/aspect-ratio'), require('@tailwindcss/container-queries'), ] // ✅ 正确:按需导入,且 typography 只用于富文本区域 plugins: [ require('@tailwindcss/forms')({ strategy: 'class' }), // typography 只在 markdown 渲染器中启用 // require('@tailwindcss/typography'), ]错误三:未启用 PurgeCSS 的深度清理
// tailwind.config.js module.exports = { // ✅ 强制启用 PurgeCSS 的深度模式 purge: { enabled: true, content: [...], // 同上 // 关键:启用深度匹配 safelist: [ // 保留动态生成的 class(如 `bg-${color}-500`) /^bg-(red|blue|green|yellow|purple|pink|indigo|gray)-[0-9]{2,3}$/, /^text-(red|blue|green|yellow|purple|pink|indigo|gray)-[0-9]{2,3}$/, // 保留 JS 动态 class /data-theme/, /dark:/, ], } }修复后,CSS 体积降至 86KB,加载时间从 1.2s 降到 180ms。Tailwind 的体积问题,99% 是配置不当,不是框架本身。
5.2 “BEM 类名太长,影响可读性!”——用 PostCSS 自动缩写
5.2 “BEM 类名太长,影响可读性!”——用 PostCSS 自动缩写BEM 类名user-profile-card__header__title--large确实冗长。我们的解决方案不是放弃 BEM,而是用 PostCSS 插件postcss-bem自动缩写:
/* 输入 */ .user-profile-card__header__title--large { font-size: 1.5rem; } /* 输出(生产环境) */ .u-p-c__h__t--l { font-size: 1.5rem; }插件配置:
// postcss.config.js module.exports = { plugins: [ require('postcss-bem')({ separator: '__', modifier: '--', // 保留前 2 个字母,但避免歧义(如 user -> u, profile -> p) abbreviate: (name) => { if (name === 'user') return 'u'; if (name === 'profile') return 'p'; if (name === 'card') return 'c'; if (name === 'header') return 'h'; if (name === 'title') return 't'; return name.substring(0, 2); } }) ] }这样既保持了 BEM 的结构语义,又解决了可读性问题。更重要的是,缩写规则是团队共识,不是个人习惯,所有开发者都遵循同一套映射表。
5.3 “CSS-in-JS 在 SSR 下样式丢失!”——Emotion 的正确 SSR 配置
5.3 “CSS-in-JS 在 SSR 下样式丢失!”——Emotion 的正确 SSR 配置Emotion 的 SSR 问题根源在于:服务端渲染时css函数生成的 class 名,和客户端 hydration 时生成的 class 名不一致。解决方案是服务端和客户端共享同一份 cache:
// _app.tsx (Next.js) import createCache from '@emotion/cache'; import { CacheProvider } from '@emotion/react'; // 创建服务端 cache const serverCache = createCache({ key: 'css', stylisPlugins: [] }); // 在 getServerSideProps 中注入 export async function getServerSideProps(context) { const cache = createCache({ key: 'css', stylisPlugins: [] }); // 渲染时使用此 cache const html = renderToString( ); // 序列化 cache 到 HTML const styles = extractCritical(cache); return { props: { emotionCache: cache, emotionStyles: styles, } }; } // 客户端 hydrate function MyApp({ Component, pageProps, emotionCache, emotionStyles }) { return ( ); }关键点:服务端和客户端必须使用同一个 cache 实例,不能各自 new 一个。我们还用@emotion/server的renderStylesToString替代手动序列化,避免遗漏。
5.4 “Tailwind 响应式写错了,移动端样式错乱!”——用 Storybook 的响应式测试
5.4 “Tailwind 响应式写错了,移动端样式错乱!”——用 Storybook 的响应式测试Tailwind 的sm:,md:断点容易写错。我们的防御措施是:
Storybook 插件@storybook/addon-viewport:为每个组件 Story 预设 mobile/tablet/desktop 视口自定义 decorator:自动注入 Tailwind 断点检查// .storybook/preview.js export const decorators = [ (Story) => ( {/* 自动显示当前断点 */} {window.innerWidth < 640 ? 'mobile' : window.innerWidth < 768 ? 'tablet' : 'desktop'} )