
说实话Jellyfin这玩意儿功能没得挑但默认那套界面吧第一眼凑合用久了是真想动手改一改。默认蓝配深灰不能说丑就是差点意思尤其是家人共用的时候想让他们一眼找到片子不想看那排密密麻麻的文字按钮。所以很多人折腾完Jellyfin的第一步就是给它换件衣服而官方其实留了个非常正经的口子——“自定义CSS”设置项。这篇文章就围绕Jellyfin的CSS主题定制把我自己折腾下来的一套方法、原理、成品代码和踩坑记录都盘一遍给想在NAS或Docker部署的Jellyfin上做界面美化的朋友一个可以直接抄作业的参考。我会尽量讲清楚每个改动背后的逻辑而不是扔一堆代码让你瞎贴。毕竟CSS这东西理解选择器和优先级之后剩下的就是审美问题。1. 为什么选择CSS定制而不是换皮肤插件先聊方案选型。Jellyfin的界面定制社区里流传的做法大概有三条路一是直接替换前端静态资源文件把dist目录里的CSS拆开改二是装第三方皮肤插件三就是用官方留出的Dashboard自定义CSS功能。我一开始也动过替换静态资源的脑筋后来发现维护成本高得离谱每次Jellyfin一升级dist目录就被覆盖改的东西全没得重新来一遍。皮肤插件呢不少质量参差不齐有些跟Jellyfin版本绑定很死版本一更新插件就歇菜风险也不小。所以最终还是回到了官方支持的自定义CSS这条路。这个功能就藏在管理后台的“控制台-高级-自定义样式”里官方文档管它叫Custom CSS位置稳定所有Jellyfin版本都有升级也不容易被默认重置成本最低。而且它可以同时填CSS和JavaScript——是的那个框里理论上支持脚本注入这意味着玩法就多了比如按时间段切换主题、根据页面状态动态改样式之类的后面我会专门讲。这个方案的适用范围也很广不只是给服务器管理员的。你定义好一套主题CSS之后只要通过网页端访问Jellyfin的用户打开同一个地址都会自动加载这套样式。也就是说我改造好这个“主题”我家里人打开电视浏览器或者iPad上的网页端看到的就是我定制后的界面。不过要注意安卓TV、iOS客户端这类原生App外壳通常不加载网页端的自定义CSS所以如果你主要用客户端直连那定制对象得另说。1.1 CSS定制能改哪些区域Jellyfin的前端界面看起来是一个完整的Web应用实际上它的DOM结构并不复杂主要是几个大区域左侧导航栏sidebar、顶部标题栏header、内容滚动区域main、还有各种弹窗和详情页。CSS的作用范围就是这些只要你能在浏览器开发者工具里定位到的元素都能改。我自己改造的重点主要有四个方向全局风格比如背景颜色、背景图、字体、基础色值还有页面的圆角风格。导航栏和标题栏让它跟整体风格统一该透明的透明该毛玻璃的毛玻璃。媒体卡片和海报墙这是用户第一眼看到的东西海报间距、悬停效果、标题样式都在这里改。按钮、进度条、滚动条这类细节改色和尺寸让界面看起来更精致。1.2 定制前必须要养成的两个习惯在正式动手之前有两条实操经验必须先说不然你后面大概率要摔跟头。第一改之前一定先备份。不是备份整个服务器而是把Docker的Jellyfin配置目录整个复制一份或者至少把当前自定义CSS输入框里的内容导出留底。因为我见过不少人改着改着某个选择器语法写错整个页面样式崩了想回退又找不到原来的代码只能干瞪眼。我个人的做法是在本地建一个Git仓库专门管理这套CSS文件每次改动都提交一次出问题直接回滚舒服得很。第二多用浏览器开发者工具。我调整Jellyfin样式时几乎全程开着Chrome的F12尤其是Elements面板和Console面板。因为Jellyfin是一个动态渲染的React应用页面里的很多元素是JS运行时生成的类名也不是特别语义化你光靠猜根本猜不准。最可靠的方式就是右键点击想改的元素选择“检查”看它到底用了哪些class然后再去自定义CSS里覆盖它。2. 核心原理为什么你的CSS能生效很多人会好奇为什么自定义CSS里写一条样式就能把Jellyfin默认的样式盖过去这就涉及到CSS的层叠机制和Jellyfin的样式加载顺序。Jellyfin前端有一个核心样式文件大概是叫app.css或者类似名字所有默认样式都定义在里边。而你在自定义CSS框里填的内容会被原封不动地追加到页面的head里以一个新建的style标签存在加载顺序在所有默认静态资源之后。CSS层叠规则里同等优先级的情况下后出现的样式会覆盖先出现的样式。所以理论上你只要选择器写得和默认样式一样准就一定能覆盖成功。但现实往往没那么温柔。Jellyfin的默认样式里用了大量层级选择器比如.skinHeader-withBackground .headerButton这种一层套一层优先级算下来不低。你要是只写一个.headerButton { color: red; }优先级不够高覆盖不了。这时候就得明白优先级这个事儿。2.1 选择器优先级与覆盖策略CSS优先级不是一个抽象概念是可以量化的。简单说行内样式最高其次是ID选择器然后是类、属性、伪类选择器最后是标签选择器。Jellyfin默认样式大量使用类选择器所以你在覆盖的时候要么用更具体的类选择器多个类叠加要么用!important强行提升优先级。我个人的习惯是先用!important解决80%的问题再用更精确的选择器去收尾。原因很现实Jellyfin更新频率不低每次升级可能调整某些类名或者嵌套结构你花半天时间去写那种层层嵌套的精确选择器版本一升可能就失效了。而用!important虽然显得粗暴但隔离性好——它明确告诉浏览器“这个属性就用我这值”其他优先级都不重要了。当然这有个前提就是你得确保只针对想改的那个元素别写得太宽泛伤及无辜。2.2 别忽略缓存问题这条我必须单拎出来讲。Jellyfin的前端资源默认是带缓存的你改了自定义CSS之后打开网页却经常看不到变化。这不是你代码写错了很可能是浏览器缓存了旧的CSS文件。我踩坑之后养成了条件反射每次改完样式第一件事就是按CtrlF5强制刷新或者在开发者工具里勾选Disable cache把Network面板打开再刷新。另外如果你在用Nginx反代还要注意Nginx层是否缓存了静态资源如果开了需要在对应的location里加一句proxy_hide_header Cache-Control或者配置合适的缓存策略否则改完样式总是“灵异不生效”。3. 可以直接抄作业的Jellyfin主题改造案例理论说了一堆现在上实战。下面这几组CSS是我目前在用的适配过Jellyfin 10.8和10.9两个大版本整体走的是“深色沉浸毛玻璃微动效”的风格核心思路是让海报墙成为绝对焦点其余UI全部退后。你可以直接复制到自定义CSS框里再根据自己的口味微调。3.1 全局暗色背景与背景图设置Jellyfin默认主题是纯深灰面对一整墙海报时其实还算低调。但你可能想加点个人印记比如放一张自己拍的风景照当背景。这里有一个容易踩的坑直接给body加背景图你会发现部分区域的背景是纯色把图片遮得严严实实。因为Jellyfin在根容器和各个面板区域上都设置了不透明的背景色。我当时的解决办法是分层处理把顶层背景色覆盖为透明然后让body显示图片/* 全局背景图 */ html, body { background: #0f1014 url(https://你的图片地址/background.jpg) no-repeat center center fixed !important; background-size: cover !important; } /* 让内容区域和侧边栏透出背景 */ .mainContainer, .skinBody, .backdropContainer { background: transparent !important; } /* 各面板卡片背景适当半透明 */ .cardBox, .detailPageContent { background: rgba(15, 16, 20, 0.7) !important; backdrop-filter: blur(10px); }这里用blur(10px)做毛玻璃能让背景图片不至于干扰前景文字阅读尤其在海报墙滚动的场景下这个模糊度我认为比较平衡。有人可能会顾虑backdrop-filter的性能实测下来普通NAS跑个JellyfinCPU占用多不了多少放心用。3.2 导航栏与标题栏毛玻璃改造毛玻璃是现在媒体类项目很喜欢用的一种质感Jellyfin的侧边栏和顶部标题栏也可以做得非常“通透”。我用的是下面这组样式/* 侧边导航栏毛玻璃 */ .skinBody.sidebarVisible .sidebar { background: rgba(16, 16, 22, 0.45) !important; backdrop-filter: blur(28px); border-right: 1px solid rgba(255, 255, 255, 0.08); } /* 顶部标题栏毛玻璃 */ .skinHeader { background: rgba(16, 16, 22, 0.35) !important; backdrop-filter: blur(28px); border-bottom: 1px solid rgba(255, 255, 255, 0.06); background-size: cover; }注意这里我特意加了border-right和border-bottom的1px半透明白线。因为纯毛玻璃和背景之间如果完全没有分割线会很“糊”边界不清晰加一条极淡的白边反而有精致的分割感。这个细节是从一些高端UI设计的经验里学来的实测视觉提升明显。3.3 海报墙卡片细节优化这一块是用户感知最明显的部分。默认状态下的海报卡片其实已经不错但我想让它的悬停反馈更明显、间距更舒适。核心改动有这几个/* 卡片间距和圆角 */ .card { padding: 8px !important; } .cardBox, .cardScalable { border-radius: 12px !important; overflow: hidden; } /* 悬停抬高和阴影 */ .card:hover .cardBox { transform: translateY(-6px); box-shadow: 0 12px 28px rgba(0, 0, 0, 0.6); transition: transform 0.25s ease, box-shadow 0.25s ease; } /* 图片上的播放按钮悬停时淡入 */ .card .cardScalable .cardPadder ~ .cardImageContainer, .card .cardImage { transition: filter 0.25s ease, opacity 0.25s ease; } .card:hover .cardImage { filter: brightness(0.85); } .card .cardOverlayButton { opacity: 0; transform: scale(0.8); transition: opacity 0.2s ease, transform 0.2s ease; } .card:hover .cardOverlayButton { opacity: 1; transform: scale(1); }这里有几个细节值得说明一下。transform: translateY(-6px)是让卡片在鼠标悬停时轻微上浮配合阴影能模拟一种“从墙面上浮起来”的感觉scale(0.8)到scale(1)的过渡是让播放按钮有个“由小变大且逐渐清晰”的动画避免一下子弹出来的生硬感。而且我确保所有过渡都加了transition这样状态切换才顺滑否则会像抽搐一样。另外一个很实用的细节是海报标题显示两行省略号默认情况下标题太长会换行撑乱卡片高度。用这个样式可以强制两行截断.cardText { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.4em; min-height: 2.8em; white-space: normal !important; }3.4 进度条、按钮和滚动条美化再往细节走一层把进度条换成品牌色滚动条换成细窄风格按钮统一圆角这个界面就比较“整体”了。/* 媒体卡片进度条 */ .cardProgressbar { background: rgba(255, 255, 255, 0.12) !important; } .cardProgressbar div { background: linear-gradient(90deg, #e64980, #ff6b9d) !important; box-shadow: 0 0 8px rgba(255, 107, 157, 0.5); } /* 细窄滚动条 */ ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.18); border-radius: 6px; } ::-webkit-scrollbar-track { background: transparent; } /* 按钮统一圆角 */ .button { border-radius: 8px !important; }进度条这里用了渐变色从偏紫的粉色过渡到大粉比默认的绿色或者纯色更有个性。box-shadow是给进度条加了一层淡淡的辉光夜间模式看很舒服。滚动条做成6px宽度手感上既不像默认那么粗笨也没有细到不好点。4. 进阶玩法用JavaScript实现自动换肤前面提到自定义CSS框里其实可以注入JavaScript。这是很多用户没注意到的隐藏能力。利用这一点可以实现不少原来觉得“得装插件才能做到”的玩法比如按时间自动切换明暗主题或者根据用户偏好应用不同风格。我自己的做法是在自定义CSS框里把样式和脚本一起填进去。样式部分用CSS变量也叫自定义属性来控制主题色脚本部分定时检测当前时间给html标签加上一个>style :root { --theme-primary: #e64980; --theme-bg: rgba(15, 16, 20, 0.8); } [data-themeauto-night] { --theme-primary: #9d4edd; --theme-bg: rgba(5, 5, 12, 0.9); } [data-themeauto-day] { --theme-primary: #2f89fc; --theme-bg: rgba(245, 245, 245, 0.85); } body, .mainContainer { background: var(--theme-bg) !important; } /style script (function() { function applyTheme() { var hour new Date().getHours(); var theme (hour 19 || hour 7) ? auto-night : auto-day; document.documentElement.setAttribute(data-theme, theme); } applyTheme(); setInterval(applyTheme, 60000); })(); /script这段脚本逻辑很简单晚上7点到早上7点之间给页面加一个>/* 登录页容器 */ .loginPage { background: rgba(0, 0, 0, 0.45) !important; backdrop-filter: blur(20px); } /* 登录框 */ .loginFormContainer { background: rgba(20, 22, 30, 0.7) !important; border-radius: 16px !important; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); }这一改登录页和主界面看起来就成套了。我个人觉得登录页是一个媒体服务器最容易被访客“打分”的页面做得好看一点观感差异非常大。5.3 Jellyfin升级之后样式失效怎么办每个Jellyfin大版本发布后社区的Discourse或者Reddit上总能看到有人在喊“我的自定义主题失效了”。这个原因很直接前端代码重构后部分DOM节点的类名变了你的CSS选择器指向了不存在的元素自然就失效了。应对策略有几个层次。第一尽量用语义化的、长期稳定的类名比如body、html、.card、.button这些基础类名比.section0、.pageWithAbsoluteTabs这类内部索引类名稳定得多。第二每次升级前把自定义CSS内容复制出来留底升级后逐项核对。第三升级后出现样式错乱不要急着逐行改先用开发者工具检查主要区块的类名是否发生变化再把变化的地方标注出来统一修改。我自己在Jellyfin 10.8到10.9升级时遇到过一次大规模失效主要原因是卡片类名从.card变成了.cardBox的层级调整整体选择器全部需要重写。那次之后我学乖了把CSS拆成了“稳定性高的基础样式”和“可能随版本更新的增强样式”两部分基础样式用通用标签选择器增强样式集中在文件后部修改时只动后半部分省事很多。5.4 NAS与Docker部署时的一个小提示说到部署方式很多人现在都是用Docker Compose跑Jellyfin这也是目前最推荐的部署方式方便迁移和备份。但有一个点容易被忽略自定义CSS的内容是存在Jellyfin的配置目录里的也就是说只要Docker volume里/config目录映射到了NAS上的一个目录那么你换机器重来、重新部署容器只要把旧的/config挂载回来CSS配置就会原封不动地恢复。所以建议在备份Jellyfin时除了备份媒体库元数据也顺带把整个/config目录一起备份。我见过有人只备份了媒体库索引重装之后CSS和一堆插件设置全丢了悔得肠子都青了。6. 让主题从“能用”到“好看”的个人经验CSS主题这件事说到底是审美工程不是单纯写代码。贴在上面的代码能让你得到一个“不丑”的界面但想让它“好看”有几个原则是我自己在反复折腾中总结出来的分享给大家当作参考。第一克制。Jellyfin的主功能是展示媒体库所以界面上的任何元素都在跟海报抢注意力。背景图不要太花哨动效不要太多颜色不要超过三个主色。我见过有人把背景图换成了高饱和度的海报拼贴再配上彩虹进度条结果整个页面晃得根本没法用。界面美化是辅助主体永远是内容。第二对比度必须达标。很多自定主题翻车都是因为字看不清。背景是深色文字也调成深色或者半透明背景和文字叠在一起糊成一团。一个比较保险的检查方法把页面截个图转成灰度如果背景和文字在灰度下对比不明显说明视觉效果会打折扣。文字阴影也能起到很好的辅助作用在深色背景上给文字加一点暗色的text-shadow能显著提升可读性。第三技巧要沉淀成文件。不要只在Jellyfin控制台的输入框里维护CSS。我的习惯是本地用VS Code写CSS维护一个独立的主题文件每次改完之后完整粘贴到控制台保存。这样既方便版本管理将来换服务器、升级版本之后的恢复成本也最低。最后再分享一个我在实际使用中发现的小技巧如果你想让某条CSS只在特定页面生效可以利用Jellyfin给不同页面加的body类名。比如详情页的body上通常会有一个itemDetailPage类你可以在选择器前面加上这个类实现页面级差异化。这样就不用担心某个改动影响了全局的观感灵活性会高很多。这个技巧是我折腾了几个月之后才摸索出来的对做细粒度定制特别有用。