外观
从 300 处方向判断到省 70%:CSS 逻辑属性 RTL 改造实录
问题背景:RTL 不是把文本右对齐那么简单
一个面向阿语、希伯来语市场的 Vue3 + VitePress 文档站,最初只在 <html dir="rtl"> 后补了一套覆盖样式。短期看似可用,但随着组件增加,问题迅速放大:卡片图标间距要镜像、弹窗关闭按钮位置要镜像、代码块圆角和分隔线要镜像,甚至部分 padding-left、right: 0、margin-left: auto 在不同语言包之间互相覆盖。
传统写法把“左、右、上、下”写死为物理方向,而 RTL 的核心是书写方向与流方向变化。继续维护 [dir="rtl"] 覆盖层,会产生大量重复代码。统计一次业务组件后,约 300 处方向相关声明需要人工判断;改造为逻辑属性并配合 aspect-ratio 后,方向相关样式减少约 70%,媒体容器也不再依赖 JS 测量。
方案对比:物理属性、覆盖层与逻辑属性
| 方案 | 写法特征 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 物理属性 | left/right、margin-left | 直观 | RTL 需覆盖,容易漏改 | 纯 LTR 页面 |
[dir=rtl] 覆盖 | 为 RTL 重写一套样式 | 上手快 | 维护成本高、优先级混乱 | 临时兼容旧项目 |
| CSS 逻辑属性 | inset-inline-start、padding-inline | 自动随书写方向变化 | 需建立团队命名习惯 | 多语言、长期项目 |
| JS 判断方向 | 根据 dir 计算 class 或样式 | 灵活 | 渲染成本高、状态易不同步 | 极少数复杂交互 |
逻辑属性不是新命名游戏,而是把样式绑定到“行向(inline)”和“块向(block)”。在英文页面中,行向起点是左;在阿语页面中,行向起点是右。浏览器自动完成映射,业务代码无需反复判断。
改造流程:先定方向模型,再替换组件样式
第一步不是全局替换,而是区分“方向相关”和“方向无关”。例如阅读箭头、步骤序号通常要镜像;但播放按钮、公司 Logo、勾选图标未必应该镜像。建议先在设计规范中标注 mirror、keep、text 三类资源。
常用映射关系
css
/* 物理属性到逻辑属性的典型映射 */
.card {
/* 旧:左内边距固定,RTL 下需写 padding-right 覆盖 */
padding-inline-start: 20px;
/* 旧:margin-left: auto,在 RTL 中应把元素推到另一侧 */
margin-inline-start: auto;
/* 旧:left: 16px; top: 16px; right: 16px */
inset-inline: 16px;
inset-block-start: 16px;
/* 边框、圆角、定位、尺寸都可使用逻辑方向 */
border-inline-start-width: 4px;
border-start-start-radius: 12px;
}常见对应关系如下:left 对应 inset-inline-start,right 对应 inset-inline-end;margin-left 对应 margin-inline-start;padding-right 对应 padding-inline-end;width 对应 inline-size;height 对应 block-size。
Vue3 组件实战:卡片、图标与分隔线自动镜像
下面示例可直接放入 Vue3 项目。组件不判断当前语言,只表达“图标位于文本起点、说明位于尾部”的布局语义。
vue
<template>
<article class="doc-card">
<div class="doc-card__icon" aria-hidden="true">
<slot name="icon">📘</slot>
</div>
<div class="doc-card__body">
<h3>{{ title }}</h3>
<p>{{ description }}</p>
<a class="doc-card__link" :href="href">
{{ linkText }}
</a>
</div>
</article>
</template>
<script setup lang="ts">
defineProps<{
title: string
description: string
href: string
linkText: string
}>()
</script>
<style scoped>
.doc-card {
display: flex;
/* gap 不区分左右,天然适合双向布局 */
gap: 14px;
padding: 20px;
border: 1px solid #e2e8f0;
border-radius: 16px;
background: #fff;
}
.doc-card__icon {
/* 关键:不要写 margin-right,交给行向起点和终点 */
flex: 0 0 auto;
inline-size: 44px;
block-size: 44px;
display: grid;
place-items: center;
border-radius: 12px;
background: #eef2ff;
}
.doc-card__body {
min-inline-size: 0;
}
.doc-card__link {
display: inline-flex;
align-items: center;
gap: 6px;
color: #4f46e5;
font-weight: 600;
text-decoration: none;
}
.doc-card__link::after {
content: "→";
/* 箭头属于方向性图标,RTL 下自动翻转 */
transform: scaleX(1);
}
:global([dir="rtl"]) .doc-card__link::after {
transform: scaleX(-1);
}
</style>这里唯一保留的 RTL 规则是箭头镜像,因为字符 → 本身携带物理方向。卡片间距、定位、文本流均由逻辑属性处理。若图标库支持 direction 风格变量,也可以进一步把这类规则沉淀到基础样式中。
VitePress 配置:按语言切换 dir 与 lang
VitePress 的多语言配置应在路由元信息中声明语言方向,避免运行时手动操作 DOM。
ts
// .vitepress/config.ts
import { defineConfig } from 'vitepress'\nexport default defineConfig({
locales: {
root: {
label: 'English',
lang: 'en',
// 英文默认从左到右
},
ar: {
label: 'العربية',
lang: 'ar',
// VitePress 会把该字段输出到 html 元素
dir: 'rtl',
link: '/ar/',
themeConfig: {
nav: [
{ text: 'دليل', link: '/ar/guide/' },
{ text: 'واجهة برمجية', link: '/ar/api/' }
]
}
}
},
themeConfig: {
socialLinks: [{ icon: 'github', link: 'https://github.com/example/docs' }]
}
})自定义主题中还可以封装一个组合式函数,用于极少数必须感知方向的场景:
ts
// composables/useDirection.ts
import { computed } from 'vue'
import { useData } from 'vitepress'
export function useDirection() {
const { lang } = useData()
const rtlLanguages = new Set(['ar', 'he', 'fa', 'ur'])
const dir = computed<'ltr' | 'rtl'>(() =>
rtlLanguages.has(lang.value) ? 'rtl' : 'ltr'
)
// 返回语义化值,组件不要直接拼 .rtl / .ltr class
const isRTL = computed(() => dir.value === 'rtl')
return { dir, isRTL }
}原则仍然是:CSS 能解决的不要交给 JS。这个组合式函数只给画布拖拽、第三方播放器、地图覆盖物等无法使用逻辑属性的场景使用。
aspect-ratio:让图片、视频封面在双向布局中稳定占位
RTL 改造常顺带暴露媒体容器问题:旧代码用 padding-top: 56.25% 做 16:9 占位,或在图片 onload 后计算高度。前者需要额外定位层,后者会引入布局抖动。现代 CSS 的 aspect-ratio 可以直接声明比例。
css
.media-box {
/* 宽度跟随容器,高度按 16:9 自动计算 */
aspect-ratio: 16 / 9;
inline-size: 100%;
border-radius: 16px;
overflow: hidden;
background: #f1f5f9;
}
.media-box img,
.media-box video {
inline-size: 100%;
block-size: 100%;
/* 保持内容裁剪,不拉伸;RTL/LTR 表现一致 */
object-fit: cover;
display: block;
}
.avatar {
/* 正方形头像:用逻辑尺寸避免宽高写死后再被覆盖 */
inline-size: 48px;
aspect-ratio: 1;
border-radius: 50%;
}在 Vue 列表中,还可以通过 CSS 变量传入不同比例,但要做白名单校验,避免任意样式注入:
vue
<template>
<figure class="ratio-card">
<div class="ratio-card__media" :style="mediaStyle">
<img :src="src" :alt="alt" loading="lazy" />
</div>
<figcaption v-if="caption">{{ caption }}</figcaption>
</figure>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
src: string
alt: string
caption?: string
ratio?: '16/9' | '4/3' | '1'
}>()
// 只允许设计系统中存在的比例,避免用户输入进入 style
const ratioMap = {
'16/9': '16 / 9',
'4/3': '4 / 3',
'1': '1'
} as const
const mediaStyle = computed(() => ({
'--media-ratio': ratioMap[props.ratio ?? '16/9']
}))
</script>
<style scoped>
.ratio-card__media {
inline-size: 100%;
aspect-ratio: var(--media-ratio);
overflow: hidden;
border-radius: 12px;
background: #f8fafc;
}
.ratio-card__media img {
inline-size: 100%;
block-size: 100%;
object-fit: cover;
}
</style>工程化检查:用 Stylelint 阻止物理方向回流
一次性迁移并不难,难的是后续不再写回 margin-left、right: 0。可以在 Stylelint 中增加约束,对业务样式给出警告或错误。
js
// stylelint.config.js
export default {
extends: ['stylelint-config-standard', 'stylelint-config-recommended-vue'],
rules: {
// 业务组件默认使用逻辑属性;特殊场景在行内说明原因
'declaration-property-value-disallowed-list': [
{
'/^margin/': ['left'],
'/^padding/': ['right']
},
{
message: 'RTL 项目请使用 margin-inline-* 或 padding-inline-*'
}
],
'property-disallowed-list': [
['left', 'right'],
{
message: '定位请使用 inset-inline-start/end,并确认是否真的需要物理方向'
}
]
}
}如果旧项目规模较大,不建议第一天就开启 error。可先在 CI 中输出报告,按页面或组件目录渐进迁移;新组件目录先强制,旧代码通过 .stylelintrc 局部放宽。
避坑总结:RTL 改造中最容易踩的 5 个坑
把所有
left/right机械替换成逻辑属性。 常见现象是返回按钮、步骤连线方向正确了,但产品 Logo、勾选图标也被翻转。修正方法是先区分方向性资源与非方向性资源:阅读箭头使用镜像或逻辑定位,品牌标识、检查状态、播放控件通常保持原样。只改 margin 和 padding,遗漏定位、圆角与边框。 页面在窄屏下出现“圆角跑到错误一侧”或激活竖线位置不一致。应同步检查
left/right/top/bottom、border-radius、border-left、text-align、box-shadow等属性,分别替换为inset-*、border-*-radius、border-inline-*。在 JS 中根据
isRTL拼大量 class。 典型错误是维护.rtl-mt-12、.ltr-pl-4这类类名,导致样式和语言状态耦合。优先使用 CSS 逻辑属性;只有第三方画布、拖拽坐标、定位算法才读取方向,并在计算层统一转换坐标。aspect-ratio与固定宽高同时使用导致比例失效。 当元素同时声明width、height和aspect-ratio,在常规块级布局中宽高会优先生效,比例被覆盖。建议只固定行向尺寸,例如inline-size: 100%,让块向尺寸由比例推导;若内容可能溢出,再配合min-block-size或max-block-size验证。忽略数字、代码和表格的双向算法。 阿语文本中夹杂英文 API 名称时,标点可能跑到错误位置;复制出来的代码顺序也可能异常。涉及代码、路径、数字编号时使用
dir="ltr"与direction: ltr隔离,但容器位置仍交给逻辑属性,避免整个组件脱离 RTL 文档流。
完成改造后,建议把 LTR 和 RTL 截图纳入视觉回归,对导航、卡片、弹窗、表格、代码块、媒体卡片六类高频组件建立固定用例。这样 RTL 不再是上线前的一次性翻译检查,而会成为组件设计和工程规范的一部分。
免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。