Windi CSS transform 工具类:旋转、缩放、位移
CSS 的 transform 是个复合属性,旋转、缩放、位移、倾斜全挤在一个属性里,多个函数用空格连接。这带来一个实际麻烦:后写的 transform 会整体覆盖先写的,想单独调整其中一项就得把整串重写。Windi CSS 的做法是把每种变换拆成独立的 CSS 变量(--tw-rotate、--tw-scale-x 之类),transform 属性里引用这些变量,于是各个工具类互不干扰,可以自由叠加。
正因为这个实现方式,用变换类时必须额外加一个 transform 类来声明那条引用变量的 transform 属性。只写 rotate-45 而不写 transform,变量设置了但没人用,页面上看不出任何效果——这是最常见的踩坑点。下面示例里 :class 数组带着 'transform ...' 就是为了这个。
另外要留意变换的执行顺序。transform 里函数从左到右依次应用,但视觉上的效果是从右往左叠加的,rotate 之后再 translate,位移方向会跟着旋转后的坐标系走。Windi 生成的 transform 属性里函数顺序是固定的(translate → rotate → skew → scale),所以你写类名的先后不影响结果,这点和手写 CSS 不一样。
下面是各类变换的工具类与对应 CSS 属性的对照表:
| 简写 | 对应属性 |
|---|---|
origin-[left right top bottom lt lb rt rb] | transform-origin: center; |
rotate-45 rotate-y-45 rotate-x-45 rotate-z-45 | transform-rotate: 45deg; |
scale-45 scale-y-45 scale-x-45 scale-z-45 | transform-scale: 0.45; |
skew-x-45 skew-y-45 | transform-skew: 45deg; |
translate-x-45px translate-y-10px | translate: 10px; |
逐条说几个容易混淆的地方。
origin-* 控制变换的基准点,也就是旋转围绕哪个点转、缩放朝哪个方向缩。默认是 center,改成 origin-top-left 之后 rotate-45 就变成绕左上角旋转,视觉位置差别很大。表格里的 lt、lb、rt、rb 是左上、左下、右上、右下的缩写形式。
rotate-* 的数值单位是度。带轴向的 rotate-x-45、rotate-y-45 是绕 X、Y 轴的三维旋转,光加这个类效果会很弱甚至看不出来——三维旋转需要透视才有立体感,得给父元素加 perspective 相关的类,否则等于正交投影下的压缩。不带轴向的 rotate-45 等价于 rotate-z-45,是平面内旋转,用得最多。
scale-* 的数值是百分比而非倍数,scale-45 对应 scale(0.45),scale-150 才是放大到 1.5 倍。这点和 rotate 的直接对应关系不同,很容易记错写成 scale-1.5。
translate-* 支持写具体单位,translate-x-45px 就是 45 像素。也可以用百分比 translate-x-1/2,百分比是相对元素自身尺寸计算的——所以经典的居中技巧 left-1/2 -translate-x-1/2 才成立:left 的 50% 相对父容器,translate 的 50% 相对自身。数值前加 - 表示反方向。
示例
下面这个 Vue 组件用嵌套循环把各组变换类批量渲染出来,每个方块显示自己应用的类名,方便直观对比效果差异:
<template>
<div class="flex justify-around align-middle flex-wrap bg-black">
<div
v-for="(value, index) in borderCfg"
:key="index"
class="flex align-middle justify-between w-screen mt-10px"
>
<div
v-for="(item, subIndex) in value"
:key="subIndex"
:style="{ width: 100 / value.length + '%' }"
>
<div
:class="[baseClass, item, 'transform bg-contain bg-no-repeat']"
style="background-image: url(https://6d69-miofly-k1xjk-1303051262.tcb.qcloud.la/images/glnz/1.jpg)"
>
{{ item }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const baseClass = ref(
'w-150px h-150px leading-150px bg-red-500 m-auto text-white whitespace-nowrap',
);
const borderCfg = ref({
origin: [
'origin-center rotate-45',
'origin-top rotate-45',
'origin-top-right rotate-45',
'origin-right rotate-45',
'origin-bottom-right rotate-45',
'origin-bottom rotate-45',
'origin-bottom-left rotate-45',
'origin-left rotate-45',
'origin-top-left rotate-45',
],
rotate: ['rotate-0', 'rotate-45', 'rotate-90', 'rotate-135', 'rotate-180'],
rotateX: [
'rotate-x-0',
'rotate-x-45',
'rotate-x-90',
'rotate-x-135',
'rotate-x-180',
],
rotateY: [
'rotate-y-0',
'rotate-y-45',
'rotate-y-90',
'rotate-y-135',
'rotate-y-180',
],
rotateZ: [
'rotate-z-0',
'rotate-z-45',
'rotate-z-90',
'rotate-z-135',
'rotate-z-180',
],
scale: ['scale-90', 'scale-50', 'scale-x-50', 'scale-y-50', 'scale-z-50'],
skew: ['skew-x-45', 'skew-x-10', 'skew-y-50', 'skew-y-10'],
translate: [
'translate-x-45px',
'translate-x-10px',
'translate-y-50px',
'translate-y-10px',
],
});
</script>
这段代码里有个值得注意的细节:所有类名都是以完整字符串的形式写在 borderCfg 数组里的,没有用 `rotate-${deg}` 这种拼接。原子化 CSS 靠静态扫描源文件来决定生成哪些规则,拼接出来的类名扫不到,样式不会被生成。写成完整字符串放数组里是安全的,因为字符串确实出现在文件中了。
whitespace-nowrap 加在 baseClass 里是为了让方块中的类名文本不折行,否则长类名会把方块撑变形,影响对比。leading-150px 让行高等于容器高度,文字垂直居中,这是单行文字居中最省事的写法。
实际项目里我很少这样堆一屏做对比,更常见的用法是配合 hover: 前缀做交互反馈,比如 hover:scale-105 transition-transform 做卡片悬浮微放大——比写 keyframes 动画轻量得多,而且 transform 属性走 GPU 合成,不触发重排。

还没有留言,来发表第一条吧~