Gradio 主题系统实战:从 300+ CSS 变量到发布,构建 Python 主题的完整指南
Gradio 的主题(Theme)是一套纯 Python 的主题系统:一个继承自 gradio.themes.Base 的类,就能控制应用的配色、字体、间距、阴影与暗色模式,并编译为 CSS 自定义属性(custom properties)注入页面。本文以 Gradio 仓库内置的主题构建技能文档为骨架,结合 主题基类源码、颜色/字号/字体工具模块 的源码证据,完整讲解主题架构、变量引用系统、Custom CSS 陷阱、审美避坑与发布流程。读完你可以独立完成一个从骨架到发布的 Gradio 主题。
一、主题架构:Python 类如何变成页面 CSS
主题控制的不是零散的组件样式,而是整个应用的视觉身份:颜色(colours)、字体(typography)、间距(spacing)、阴影(shadows)和暗色模式(dark mode)。
完整数据流如下:
Python 类(gradio.themes.Base 子类)
→ _get_theme_css()
→ CSS :root { --var: val; }
→ 通过 /theme.css 下发
→ Svelte 组件用 var(--name) 消费
这个流程可以直接在源码中得到验证。ThemeClass._get_theme_css() 遍历实例的所有非下划线属性,把属性名中的下划线替换为连字符后输出到 :root,暗色变量单独输出到 :root.dark, :root .dark 块中:
# gradio/themes/base.py 中的关键输出逻辑(简化)
css_code = (
":root {\n"
+ "\n".join([f" --{attr}: {val};" for attr, val in css.items()])
+ "\n}"
)
dark_css_code = (
"\n:root.dark, :root .dark {\n"
+ "\n".join([f" --{attr}: {val};" for attr, val in dark_css.items()])
+ "\n}"
)
这里有两个关键的实现细节:
- 暗色变量回退:如果某个变量只设置了亮色值,暗色 CSS 会复用亮色值(
if attr not in dark_css: dark_css[attr] = val);但显式把某个_dark变量设为None表示"暗色下继承亮色值",而把非 dark 变量设为None会直接抛出ValueError。 custom_css的位置:自定义 CSS 被追加在全部变量之后(base.py#L98-L99),这意味着它可以覆盖变量之外的表现层样式,但不能重新定义:root变量。
另外两个架构事实需要牢记:
- 主题 CSS 注入到
<gradio-app>的 Shadow DOM 内,而页面<body>在 Shadow DOM 之外(light DOM),由body_background_fill变量单独绘制(布局层会应用body { background: var(--body-background-fill) })。 - 完整变量清单就在 gradio/themes/base.py 中(约 2000 行,300+ 个变量),按变量名搜索即可。每个变量都可带可选的
_dark后缀。
二、核心原则:动笔写主题前的五条纪律
技能文档给出的五条原则,是区分"能跑"和"能用"的分界线:
- 文字对比度不可妥协(Text contrast is non-negotiable)。每个文本元素都必须在其实际背景上可读——正文、彩色标签填充上的标签文字、按钮填充上的按钮文字、占位符文字、选中复选框文字、错误文字、链接文字。发布前必须逐一审查所有"文字/背景"配对。
- 暗色模式必须独立设计,绝不能自动反色。每个
_dark变量都要为深色背景专门挑选。具体规则:- 深色模式下字体权重略降(350 代替 400)——浅色文字在深色背景上视觉更重;
- 降低强调色饱和度——高亮度下高色相纯度会显得刺眼;
- 用更浅的表面色做层级(elevation),而不是更重的阴影;
- 永远不要用纯黑
#000,使用类似#0a0a14、带微弱色相倾向的深色。
- 承诺一个审美方向。极繁与极简都成立,半吊子才失败。选定一种气质(editorial、brutal、glass、retro、organic、playful、industrial……),然后让每个变量都为它服务。
- 用变量引用(
*name)保持一致性。一个值需要跟随另一个值时就引用它,这样主题可维护,用户改构造参数(色相、尺寸)时变更会级联传播。 - 两种模式都要测试。亮色和暗色分别验证:body、blocks、inputs、buttons、labels、checkboxes、tables、focus 状态、hover 状态、selected 状态。
三、主题类骨架:可直接复制的起点
from __future__ import annotations
from collections.abc import Iterable
from gradio.themes.base import Base
from gradio.themes.utils import colors, fonts, sizes
class MyTheme(Base):
def __init__(
self,
*,
primary_hue: colors.Color | str = colors.blue,
secondary_hue: colors.Color | str = colors.violet,
neutral_hue: colors.Color | str = colors.slate,
spacing_size: sizes.Size | str = sizes.spacing_md,
radius_size: sizes.Size | str = sizes.radius_md,
text_size: sizes.Size | str = sizes.text_md,
font: fonts.Font | str | Iterable[fonts.Font | str] = (
fonts.GoogleFont("Instrument Sans", weights=(400, 500, 600, 700)),
"ui-sans-serif", "system-ui", "sans-serif",
),
font_mono: fonts.Font | str | Iterable[fonts.Font | str] = (
fonts.GoogleFont("JetBrains Mono"),
"ui-monospace", "Consolas", "monospace",
),
):
super().__init__(
primary_hue=primary_hue, secondary_hue=secondary_hue,
neutral_hue=neutral_hue, spacing_size=spacing_size,
radius_size=radius_size, text_size=text_size,
font=font, font_mono=font_mono,
)
self.name = "my_theme"
super().set(
# 在此覆盖变量
)
骨架说明:
- 构造参数是用户可定制的旋钮:三个色相(primary/secondary/neutral)、三档尺寸(spacing/radius/text)、两套字体。把旋钮留在构造函数里,
__init__内再通过super().set(...)用这些旋钮推导具体变量,就能实现"改一个色相,全站级联"。 - 变量覆盖全部通过 Base.set() 传入,它接受约 300 个关键字参数(
body_background_fill、button_primary_background_fill、block_title_*等),支持在__init__中任意调用。 self.name必须设置,它是主题序列化与 Hub 发布时的标识名。- 字体权重必须显式声明:
GoogleFont(name, weights=(400, 600))中 默认权重就是(400, 600)(见 fonts.py)。只要使用默认之外的字重,就必须显式写weights=(...),否则浏览器会 fake-bold(伪粗体),效果很差。
四、基础构件:颜色、尺寸、字体
颜色调色板
gradio.themes.utils.colors 提供 22 个命名调色板,每个 11 个色阶(c50 最浅 → c950 最深):
slate、gray、zinc、stone、neutral、red、orange、amber、yellow、lime、green、emerald、teal、cyan、sky、blue、indigo、violet、purple、fuchsia、pink、rose。
其背后是 Color 类:构造时接收 c50…c950 共 11 个十六进制值,并提供 expand() 展开为列表。使用方式:
from gradio.themes.utils import colors
colors.blue.c500 # "#3b82f6"
f"{colors.violet.c800}60" # alpha 十六进制写法(37.5% 不透明度)
第二个技巧值得注意:#RRGGBB 后面拼两位 alpha(60 ≈ 0x60/0xFF ≈ 37.5% 不透明度)是 CSS 8 位十六进制颜色。文档建议:大面 alpha 使用通常意味着调色板不完整,应当为每个上下文定义明确的覆盖色;alpha 只在 focus ring 和磨砂玻璃场景下可以接受。
尺寸刻度
gradio/themes/utils/sizes.py 中 Size 类定义 7 级刻度(xxs–xxl),内置 8 组预设:
| 预设 | xxs | xs | sm | md | lg | xl | xxl |
|---|---|---|---|---|---|---|---|
radius_none |
0px | 0px | 0px | 0px | 0px | 0px | 0px |
radius_md |
1px | 2px | 4px | 6px | 8px | 12px | 22px |
radius_xxl |
6px | 8px | 10px | 20px | 24px | 28px | 32px |
spacing_md |
1px | 2px | 4px | 6px | 8px | 10px | 16px |
text_md |
9px | 10px | 12px | 14px | 16px | 22px | 26px |
完整列表还包括 radius_sm、radius_lg、spacing_sm、spacing_lg、text_sm、text_lg。需要自定义时用 Size(xxs="…", xs="…", …) 构造(7 个参数全部必填)。选刻度而非写死值的好处:用户把 spacing_size 从 spacing_md 换成 spacing_lg,所有引用该刻度的间距变量同步放大。
字体
gradio/themes/utils/fonts.py 提供三种字体表示:
GoogleFont(name, weights=(...)):默认权重(400, 600);生成 Google Fonts 的@importURL(形如css2?family=Name:wght@400;500&display=swap)。若所需字体与字重在本地静态目录中存在,会自动降级为LocalFont(离线可用),这是源码中的实现细节(fonts.py#L108-L112)。LocalFont(name, weights=(...)):生成指向打包 woff2 字体的@font-face规则。- 纯字符串:系统字体(如
"system-ui")。
惯例是始终用元组提供回退链:(GoogleFont("Instrument Sans", weights=(400, 500, 600, 700)), "ui-sans-serif", "system-ui", "sans-serif")。
五、变量引用系统:*name 的级联魔法
主题变量值里可以用 *variable_name 引用其他主题变量,引用在生成 CSS 时解析。源码中这只是一条正则替换:
# gradio/themes/base.py 的 _get_theme_css() 内
pattern = r"(\*)([\w_]+)(\b)"
def repl_func(match):
word = match.group(2).replace("_", "-")
return f"var(--{word})"
也就是说 "*shadow_drop" 最终编译为 CSS 原生级联引用 var(--shadow-drop),解析是浏览器运行时递归完成的。典型用法:
input_shadow="*shadow_drop"
button_cancel_text_color="*button_secondary_text_color"
暗色引用的自动解析(最常见的报错来源)
引用暗色变量时不要加 _dark 后缀——引用会自动跟随所在变量的明暗模式:
input_shadow_focus_dark="0 0 0 3px *primary_900" # 正确
input_shadow_focus_dark="0 0 0 3px *primary_900_dark" # 错误——直接抛异常
这是源码里硬编码的防御(base.py#L51-L64):_get_theme_css() 在解析时发现引用以 _dark 结尾就抛出 ValueError,提示"dark variable references are automatically used for dark mode attributes"。另外还有一种情况也会报错:把 xxx_dark 设置成引用 *xxx(即自己对应亮色变量的同名引用),此时源码提示"如果明暗值相同,把暗色版本设为 None"。
还有一个从源码结构可以确认的辅助方法:_get_computed_value() 会递归解析引用链(上限 100 层,检测到循环引用时发出警告),并且对暗色属性优先取 xxx_dark 的值、取不到再回退亮色值——这与"引用自动解析明暗"的语义一致。
变量接受任意 CSS 值
颜色、渐变、阴影、transform、transition、none、calc()、间距 token(*spacing_md)都可以。
容易踩坑的变量(Non-obvious variables)
这些变量语义不直观,值得逐个记住:
block_label_*(媒体元素标题,如 "Image"、"Audio" 的 label)与block_title_*(表单元素标题,如 Textbox 的 label)是两套不同的变量,需要一起设计才能视觉统一。body_background_fill绘制的是页面真实的<body>(整个视口),不是 Gradio 容器。想让容器本身透明,另设background_fill_primary="transparent"。button_transform_hover/button_transform_active:做translateY(-2px)抬升效果,需搭配button_*_shadow_hover才有正确的纵深感。button_{size}_*(large/small)控制每个尺寸的 padding/圆角/字号;button_{variant}_*(primary/secondary/cancel)控制每个变体的颜色/阴影。两个维度正交。checkbox_label_*是复选框外围的胶囊按钮(pill button),checkbox_*才是方框本身。stat_background_fill接受渐变——常用于置信度条(confidence bars)。
六、Custom CSS:变量表达不了的东西
主题可以在 __init__ 中设置 self.custom_css,它与变量一起注入主题 CSS,发布到 Hub 时也会随主题一起分发:
class MyTheme(Base):
def __init__(self, ...):
super().__init__(...)
self.name = "my_theme"
self.custom_css = """
/* 任意 CSS */
"""
super().set(...)
适合 custom_css 的场景(变量无法表达的):backdrop-filter、平铺背景图、自定义滑杆拇指、伪元素装饰、定位特定 Gradio DOM(.label-wrap、button.secondary、.reset-button、input[type="range"])。
关键陷阱:Shadow DOM 作用域
主题 CSS 注入在 <gradio-app> Shadow DOM 内部,指向 html 或 body 的选择器不会生效——它们活在 light DOM(真实页面文档)里。要绘制页面背景,必须用 body_background_fill 变量(布局 Svelte 组件会把它应用到真实 <body>),而不要试图在 custom_css 里写 body { ... }:
# 正确——绘制真实 <body>,覆盖整个视口
body_background_fill="linear-gradient(...)"
# 错误——选择器在 Shadow DOM 内解析不到,渐变永远画不出来
self.custom_css = "body { background: linear-gradient(...) }"
自定义滑杆拇指(含前缀与 !important 要求)
自定义 slider 拇指必须同时覆盖 webkit 与 moz 前缀,并需要 !important 才能压过 Gradio 默认样式:
input[type="range"]::-webkit-slider-thumb,
input[type="range"]::-moz-range-thumb {
appearance: none !important;
width: 30px !important;
height: 30px !important;
background: url("data:image/png;base64,...") no-repeat center / contain !important;
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
}
可依赖的 Gradio DOM 选择器
以下类名没有 Svelte 哈希,可放心用于 custom_css:.gradio-container、.block、.panel、.form、.wrap、.label-wrap、button.primary、button.secondary、.reset-button、input[type="range"]。暗色模式用 .dark .xxx 前缀。其他选择器请在活的 DOM 中检查——带哈希的类名会随版本变化。
七、审美质量:避开"AI 生成感"
技术正确只是及格线。一个主题可以每个变量都设置完美,却依然显得平庸。技能文档给出一组可操作的自检:
"AI 泔水"测试(The "AI Slop" Test)
把这个主题拿给人看并说"这是 AI 做的"——对方会不会立刻相信?如果是,就是问题所在。有辨识度的主题应该让人问"这怎么做到的",而不是"哪个 AI 做的"。
调色板陷阱
- 近黑背景 + 青色强调——默认的"AI 赛博朋克"观感;
- 紫到蓝的渐变——过度使用且过时;
- 暗色模式的霓虹光晕——不需要真实设计决策就显得"酷";
- 标题/指标上的渐变文字——纯装饰,无意义;
- 到处都是 glassmorphism——backdrop-blur 当装饰而非功能;
- 纯黑
#000或纯白#fff——自然界不存在;一切颜色都应带色相倾向(哪怕 chroma 0.005–0.01 也显得自然); - 无倾向的中性色(直接用
colors.gray、colors.zinc)——中性色应暗示品牌色相以获得潜意识统一。强调色偏冷用colors.slate,偏暖用colors.stone; - 滥用 alpha(到处
rgba(...))——通常意味着调色板不完整,应为每个上下文定义显式覆盖色;仅 focus ring 和磨砂玻璃场景可接受,其他地方都值得怀疑; - 彩色背景上的灰色文字——会显得浑浊,应改用背景色的更深深阶。
字体陷阱
- Inter、Roboto、Open Sans、Lato、Montserrat——这些"隐形默认"字体是"AI 生成"的信号。工具型主题尚可,追求辨识度的主题上是致命的;
- 文档推荐的 Google 字体替代:无衬线——Instrument Sans、Plus Jakarta Sans、Outfit、Onest、Figtree、DM Sans、Source Sans 3;衬线/编辑风——Fraunces、Newsreader、Lora;技术感——Chakra Petch、Space Grotesk、JetBrains Mono;
- 等宽字体作为偷懒的"技术感"符号——只有在它真的传递信息时才用等宽;
- 字号过多且过于接近(12/13/14/15/16)——层级浑浊。应减少字号数量、拉大对比(1.25–1.5× 比例)。
视觉细节陷阱
- 通用投影(
0 2px 4px rgba(0,0,0,0.1))——安全但无记忆点。原则:如果投影清晰可见,就太强了。要么承诺粗重阴影,要么完全不用; - 完全相同的卡片网格——每个 block 形状与权重一致会造成视觉单调;
- 均匀间距——用紧凑分组与宽松留白交替制造节奏。
多维度构建层级
层级在 {大小、字重、颜色、位置、空间} 中 2–3 个维度同时变化时最强。单独放大 label 是弱层级;放大 + 加粗 + 上方留白才是强层级。对应到变量就是 block_label_* 与 block_title_* 与 section_header_* 的组合设计。
八、从参考图构建主题
对齐截图的四步工作流:
- 提取(Extract):背景(纯色/渐变/纹理、精确颜色)、卡片样式(边框、圆角、投影)、文字权重/颜色、强调色相、字体气质、标志性元素。
- 映射(Map):背景 →
body_background_fill;卡片 →block_*;按钮 →button_*(复杂渐变/光晕用custom_css补充);强调色 → 没有现成调色板匹配时自定义Color()。 - 构建顺序(Build order):背景 → blocks → 按钮 → 输入框/标签 → 细节(滑杆拇指、focus ring)。
- 已知坑(Pitfalls):
- 大圆角 + Gradio 的
overflow: hidden会裁切内容,圆角上限约 20px; - 复杂多段按钮渐变需要
custom_css加!important; backdrop-filter在 Firefox 默认不生效。
- 大圆角 + Gradio 的
九、发布前检查清单
__init__中设置了self.name;- 文字对比度审计(最先做):
- 正文文字 vs body/block 背景;
- 彩色 label 填充上的 label 文字(对比对象是填充色,不是页面背景);
- 按钮填充上的按钮文字(primary/secondary/cancel 三种变体全覆盖);
- 占位符文字——可见且与已输入文字区分(白底上至少到
#999级别); - 选中态 checkbox/radio 文字 vs 选中填充色;
- 错误文字 vs 错误背景;
- 链接文字 vs body 背景;
- 亮色模式:body、blocks、inputs、buttons、labels、checkboxes、tables;
- 暗色模式:同样元素,独立设计(而非自动反色);
- focus、hover、active、selected 状态 × 全部三种按钮变体;
- 审美质量复查:AI 泔水测试、无调色板/字体陷阱、眯眼距离下层级依然成立;
- 所有字体字重都通过
weights=(...)显式加载; - 用
gr.themes.builder()做交互式预览(builder() 在 gradio/themes/init.py#L42 中定义,内部启动 builder_app.py 的预览 Demo)。
十、本地持久化与 Hub 发布
本地序列化
theme.dump("my_theme.json") # 保存为 JSON
theme = Theme.load("my_theme.json") # 加载
源码实现见 ThemeClass.load() 与 ThemeClass.dump():JSON 中字体对象通过 FontEncoder 编码为带 __gradio_font__ 标记的字典,load 时用 fonts.as_font 还原为 GoogleFont/LocalFont/Font 实例,所以字体信息不会丢失。
发布与拉取
theme.push_to_hub(
repo_name="my-theme",
org_name="my-org",
version="0.0.1",
description="A bold theme for data dashboards.",
)
# 加载
theme = gr.themes.Theme.from_hub("my-org/my-theme@1.2.0")
custom_css会自动随主题打包。- push_to_hub() 的完整签名还支持
token、theme_name、private参数,需要 HuggingFace 账号。 - from_hub() 的
repo_name格式为<author>/<theme-name>@<语义化版本表达式>,省略@版本时拉取最新版;下载公开主题不需要账号,私有主题需token。
十一、注册一个内置主题
如果要让主题成为 gr.themes.Xxx 的一等公民(而非仅本地使用):
- 创建 gradio/themes/ 下的新模块,如
gradio/themes/my_theme.py; - 在 gradio/themes/init.py 中加入
from gradio.themes.my_theme import MyTheme,并把"MyTheme"加进__all__(当前__all__已列出Default、Soft、Glass、Neon、Cyberpunk、Monochrome、Ember、Ocean、Citrus、Origin、Mario等); - 在
__init__中设置self.name = "my_theme"。
十二、参考:典范主题文件
仓库自带多个风格各异的内置主题,读源码学具体模式,不要重复实现已存在的:
| 主题文件 | 风格 | 值得学习的技术点 |
|---|---|---|
| gradio/themes/soft.py | 极简、柔和 | 基于阴影的层级、无 block 边框、圆角 label |
| gradio/themes/cyberpunk.py | 大胆、霓虹 | 自定义 hex 深色背景、霓虹光晕阴影、alpha 颜色 |
| gradio/themes/neon.py | 活泼、凸起 | 底边阴影、transform hover/active、胶囊形状 |
| gradio/themes/ember.py | 温暖、精致 | 覆盖全面、focus ring 阴影 |
| gradio/themes/ocean.py | 渐变、流动 | 按钮 + checkbox label 上的 CSS 渐变、scale transform |
| gradio/themes/glass.py | 编辑风、克制 | 输入框/按钮上的渐变填充、系统字体 |
| gradio/themes/monochrome.py | 锐利、无彩 | 全中性色相、衬线字体、锐利圆角、粗边框 |
| gradio/themes/default.py | 均衡、标准 | 橙+蓝双色相、stat 渐变、错误色 |
实际目录中还有 citrus.py、mario.py、origin.py 可作为额外风格样本。
小结
Gradio 主题系统的精髓在于:用一个 Python 类管理 300+ 个 CSS 变量,用 *引用 让值之间形成级联,用 _dark 后缀让暗色模式独立设计,用 custom_css 补上变量表达不了的表现层细节。配合本文的骨架代码、避坑清单(Shadow DOM 作用域、字体权重、暗色引用后缀)与审美检查,你可以在 gr.themes.builder() 的实时预览中完成一个既技术正确、又有辨识度的主题,并通过 push_to_hub 分享给其他 Gradio 应用。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00