tailwindcss
tailwindcss copied to clipboard
Add first-class 3d transform utility support
Adds first-class support for 3d transformations using the existing TailwindCSS architecture.
This PR also sees GPU acceleration activated by default, but this can be easily switched to using CPU by default. It may also be useful to trigger some sort of warning if a user uses the transform-cpu
utility but then also attempts to use one of the 3d transform utilities, which would naturally disable 3d transformations.
The new utilities added in this effort are:
Utility | CSS Property/Transform Function | CSS Value |
---|---|---|
rotate-x |
rotateX() |
same as rotate value(s) |
rotate-y |
rotateY() |
same as rotate value(s) |
rotate-z |
rotateZ() |
alias of rotate value(s) |
scale-z |
scaleZ() |
same as scale value(s) |
perspective-self |
perspective() |
incremental t-shirt sizing |
perspective |
perspective |
incremental t-shirt sizing, same as perspective-self |
perspective-origin |
perspective-origin |
same as origin (transform-origin ) |
transform-flat |
transform-style |
flat |
transform-3d |
transform-style |
preserve-3d |
transform-content |
transform-box |
content-box |
transform-border |
transform-box |
border-box |
transform-fill |
transform-box |
fill-box |
transform-stroke |
transform-box |
stroke-box |
transform-view |
transform-box |
view-box |
backface-visible |
backface-visibility |
visible |
backface-hidden |
backface-visibility |
hidden |
Hey thanks!
Just to set expectations, 90% chance we will have to close this for now just because I can't drop everything and make this feature a top priority (especially the documentation effort). As I've mentioned in other PRs it just takes a lot of dedicated time and focus to ensure major new feature PRs are ready to be merged, because I have to go through the process of thinking about the feature from first principles and make sure the final solution matches what I think is the best solution for the framework, so it's always a lot more work than simply looking at the changed files in a PR. So because we don't like to have PRs sit open for a year or more (like they have in the past), we generally close things that we aren't ready to merge basically right away.
That said the big blocker on this feature in the past has been designing a thoughtful scale for perspective
. What is the justification/design story behind these specific values?
perspective: {
none: 'none',
0: '0rem',
xs: '6.25rem', // 100px
sm: '18.75rem', // 300px
md: '31.25rem', // 500px
lg: '50rem', // 800px
xl: '75rem', // 1200px
'2xl': '106.25rem', // 1700px
'3xl': '143.75rem', // 2300px
'4xl': '187.5rem', // 3000px
'5xl': '237.5rem', // 3800px
'6xl': '300rem', // 4800px
'7xl': '375rem', // 6000px
'8xl': '468.75rem', // 7500px
'9xl': '625rem', // 10000px
},
@adamwathan No worries! Always a pleasure. It's too bad there isn't a way to archive these sorts of PRs for a later date, though I suppose you can do that internally and then always re-open them once they surface as priorities.
I'm collecting a laundry list of TailwindCSS plugins on my end and have no issue keeping them all local, but seeing them supported as first-class utilities is always great.
I understand where you're coming from on priorities. I'm a big spec pusher for CSS and interop, and only about 10% of those proposals gain any real traction.
I think 3d transforms are something used quite often, and seeing as they've been around forever and are widely supported, it seemed a no-op to get some of those widely used properties added, but yeah, documentation can end up being 90%+ of the work, and that's no joke.
If your team had some sort of public list of incoming priorities (with preferred impl spec), devs like me might be happy to snipe them off the list to clear up your plates too. Just thinking out loud.
perspective
scale
Re the perspective
scale, that was one I was thinking through quite a bit as well. That was a very rough scale I came up with, and I 100% know it would need some work before anything is finalized. The idea was generally that the perspective values should allow for a lot of granular control ranging between a perspective as small as 100px to as large as 100x that (10,000px).
If we want something with more significance, whether mathematically or humanly, here are two potential options:
Parabolic progression (exponential significance between stop values)
xs sm md lg xl 2xl 3xl 4xl 5xl 6xl 7xl 8xl 9xl
px 100, 250, 500, 900, 1450, 2200, 3150, 4300, 5650, 7200, 8950, 10900, 13050
rem 6.25, 15.625, 31.25, 56.25, 90.625, 137.5, 196.875, 268.75, 353.125, 450, 559.375, 681.25, 815.625,
Below source: Wolframp Alpha
Linear progression (human-predictable stop values)
** less linear for smaller values for greater granularity
I added several breakpoints here to make the number and breadth of this example match that of the "parabolic progression" example.
Contrary to the parabolically progressive example, this example is predominately linear, so each value here is one "step up" as opposed to increasing by some multiplier. As a result, the stop values, drawn out, are linear and not curved, though the stop values might be deemed as more predictable to the typical developer.
xs sm md lg xl 2xl 3xl 4xl 5xl 6xl 7xl 8xl 9xl
px 250, 500, 1000, 1500, 2500, 3750, 5000, 6250, 7500, 8750, 10000, 11250, 12500
rem 15.625, 31.25, 62.5, 93.75, 156.25, 234.375, 312.5, 390.625, 468.75, 546.875, 625, 703.125, 781.25
Below source: Wolframp Alpha
Btw here's a little demo showcasing some of these new utilities: https://play.tailwindcss.com/WJNxN2g2OQ
Here is how it looks:
Source:
<!-- Wrapper -->
<div class="relative grid min-h-screen place-items-center transform [--s:20vmin]">
<!-- Cube -->
<div class="
animate-float h-[--s] w-[--s] transform-3d
[&>div]:absolute [&>div]:left-0 [&>div]:top-0 [&>div]:h-full [&>div]:w-full
[&>div]:border-2 [&>div]:border-black [&>div]:transform-gpu
">
<!-- Faces -->
<div class="bg-red-500/25"></div>
<div class="bg-purple-500/25 transform-origin-left rotate-y-[90deg]"></div>
<div class="bg-blue-500/25 transform-origin-right -rotate-y-[90deg]"></div>
<div class="bg-amber-500/25 transform-origin-top -rotate-x-[90deg]"></div>
<div class="bg-green-500/25 transform-origin-bottom rotate-x-[90deg]"></div>
<div class="bg-cyan-500/25 -translate-z-[--s]"></div>
</div>
</div>
In that example ☝🏼 I moved those classes to the parent using an arbitrary variant that would have otherwise been redundant. When working with a JS framework, those classes would more likely be added to each child using a JS variable instead.
@adamwathan Also, if perspective
is the main blocker here, a Twitter poll and getting community thoughts. However, a perspective scale isn't entirely necessary for the initial addition of 3d transforms anyway.
The example I showed in my previous comment was made with an intrinsic perspective (essentially perspective: infinity
), which works as is, and looks great:
Tailwind Play | scroll to the related comment ☝🏼
I think it would still help to include the perspective
-related properties even without a scale at first, as this would allow easier usage of each of these:
-
perspective
{ perspective: /* arbitrary value */; }
-
perspective-self
{ transform: perspective(/* arbitrary value */); }
-
perspective-origin
{ perspective-origin: /* same config values as transform-origin */ }
So this would still all be valid even without a perspective
scale, and I think this would be justifiable over using an arbitrary property like [perspective:1000px]
because it's (a) first-class supported in nature, and moreover (b) allows the use of perspective()
which wouldn't otherwise be possible using an arbitrary property.
Some notes from my implementation of https://github.com/sambauers/tailwindcss-3d
I went for modern alternatives to transform
by default (there is a legacy mode to only use transform
). Tailwind's use of var
within transform
functions is smart but limited (especially within keyframe
s). I wonder if core would be better served by transitioning to the modern approach at this point in time, rather than extending use of transform
into 3D.
There are some rough edges to browser support here as well. There are various triggers for GPU versus CPU usage that can kick in unexpectedly https://developer.mozilla.org/en-US/docs/Learn/Performance/CSS - there is also still a need to add --webkit-transform
duplicate declarations in many/most cases to ensure broad support.
I do want to see 3D support in core, but maybe the demand can be gauged through stats on the plugin that exists (not huge usage number so far), I can also weed out various issues there. I'm also happy to have your input and support on development there @brandonmcconnell
Side note: @adamwathan sorry for not adhering to the brand/naming conventions for the 3D plugin. I only became aware of that guideline last night - I'll work through renaming soon. UPDATE - I've changed the display name of the plugin on the README and in NPM, I haven't changed the package name yet (it is still tailwindcss-3d
)
On the question of perspective
values. It generally takes big changes in values to see any visible difference in output. In the 3D plugin I used a very small set of defaults expecting that people would use arbitrary values if they want to get specific.
https://github.com/sambauers/tailwindcss-3d/blob/main/src/css-utilities/perspective.ts#L17
@brandonmcconnell what is the thinking behind having seperate perspective
and perspective-self
utilities? Is there a specific use case that would require one over the other? In the 3D plugin I implemented the perspective
property as standard, or the perspective()
transform function in legacy mode, but not both at once.
@sambauers Thanks for sharing your thoughts here!
-
Re the GPU/CPU triggers:
I purposely followed the current TailwindCSS as much as possible, so that should be handled near identically. The natural browser GPU/CPU triggers are intentional and should trigger GPU automatically when needed unless manually overridden using
transform-gpu
/transform-cpu
. -
Re keyframes:
Each of these properties are animatable using
@keyframes
. What limitations are you referring to? -
Re demand:
There are have been at least 4 plugins created over the years for this precise purpose—
- https://github.com/benface/tailwindcss-transforms
- https://github.com/sambauers/tailwindcss-3d
- https://github.com/Kamona-WD/tailwindcss-perspective
- & my own
…and probably others, among hundreds of people who have used these plugins. Having first-class support for 3d transforms would enable all CSS devs to make use of 3d transforms and related properties, a core features of the lang
-
Re
perspective
values:Granularity also makes a great deal of difference on the smaller values like
100px
vs.300px
, which is why I added the100px
value, though I agree (re my recent comment), and I think those values can be all/mostly be left to arbitrary usage. -
Re your question on
perspective
vs.perspective-self
:This PR also uses the
perspective
utility as the standard, butperspective()
(perspective-self
) serves a different purpose than theperspective
property and is an ideal fit for situations where you don't want to enclose your 3d element in a wrapper simply to give it perspective, or where sibling elements require differentperspective
values.MDN makes this distinction here: "The
perspective()
transform function is part of thetransform
value applied on the element being transformed. This differs from theperspective
andperspective-origin
properties which are attached to the parent of a child transformed in 3-dimensional space."
Re the naming conventions note, would you mind sharing the link to those so I can also update my plugins to adhere to the recommended naming conventions? Thanks!
Each of these properties are animatable using @keyframes. What limitations are you referring to?
They are animatable in the most basic keyframe progressions. Imagine a simplified multi-step animation like this:
@keyframes example {
0% {
transform: rotateZ(0deg) translateY(0rem);
}
50% {
transform: translateY(-5rem);
}
100% {
transform: rotateZ(360deg) translateY(0rem);
}
}
At the 50% mark, you have a problem. You need to interpolate the missing rotateZ
value to make this work, and in fact it still might not work as I have found the rotation (on some browsers) will oscillate rather than completely rotate. That is simple for linear progressions but once timing functions are involved it gets more difficult. Using modern properties you can do this without hacking. In the 3D plugin it wasn't possible to make a legacy version of the "bounce and spin" animation, for example.
The other problem we face with the current approach is that CSS variables don't animate. If we could replace the current set of variables with CSS properties they could be animated in keyframes, but that approach may or may not overcome the interpolation problem (I think it might though). CSS properties also have worse browser support than the modern transform properties 😄
Note that core Tailwind uses raw values for transform functions inside keyframes for the above reasons. This creates sub-optimal behaviour when attempting to apply static transforms along with animations. This is partially overcome in the 3D plugin, but not completely due to support for rotateX
and rotateY
being enabled by transform
due to really poor Safari support for x and y values in the rotate
property.
Re the naming conventions note, would you mind sharing the link to those
https://tailwindcss.com/brand
@sambauers Personally, I am all for TailwindCSS switching to the more modern approach, but that's a bigger change which—as you pointed out—would produce breaking changes for those already building around the current approach.
This PR has no conflicts with the current methodology, making it a no-op, whereas introducing the newer transform properties would require at least an opt-in option that defaults to the legacy format to prevent any breaking changes, essentially the inverse of how your package is currently configured.
In that way, this PR is actually the first essential step toward introducing the new transform properties.
Circling back to this briefly just because found myself deep in the perspective
research hole again the other day and don't want to lose my work — right now I think I like this for the perspective scale:
Class | Value |
---|---|
perspective-none |
none |
perspective-dramatic |
100px |
perspective-near |
300px |
perspective-standard or perspective-normal |
500px |
perspective-midrange |
800px |
perspective-distant |
1200px |
I like that this keeps the options really limited and easy to pick through for people who don't want to get lost nitpicking on this stuff, and that we have arbitrary values as an easy escape hatch for anyone who needs a specific value 👍
@adamwathan Yeah, I like that. I updated the PR to reflect that (using "normal", vs. "standard") and also added a DEFAULT
value, which reflects the same normal value.
I think it could be intuitive that using perspective
by itself without a value adds that normal value, but I could see a string case for either removing the normal
value in favor of DEFAULT
(no value needed), or removing DEFAULT
in favor of normal
. Let me know.
Other than that, I think this PR is in pretty good shape. It was a bit hard to test the CPU/GPU opt in/out logic without being able to run this exactly as is in a remote environment. StackBlitz and CodeSandbox don't seem to play nice with Tailwind CSS npm forks, but it builds fine for me locally.
It would just help to have someone on the team do a once-over on it to ensure that CPU/GPU bit works as expected. It might require a small patch.
is there any result ?
Class | Value |
---|---|
perspective-none | none |
perspective-dramatic | 100px |
perspective-near | 300px |
perspective-standard or perspective-normal | 500px |
perspective-midrange | 800px |
perspective-distant | 1200px |
Liking these semantics. Waiting eagerly for this feature
Hey all! Thanks @brandonmcconnell for drafting this, and everyone else for the input. We're implementing this in Tailwind CSS v4 with #13248. As you'll see, we're closely following what was discussed here, with a few small changes:
- All transforms are done using specific CSS properties, rather than
transform
functions. This prevents conflicts updating thetransform
property, and makes it easier to see in the inspector. -
rotate-x
,rotate-y
, androtate-z
contribute to the axis of rotation, rather than specifying separate angles of rotation. See #13248 for more details. - Added
scale-3d
to scale in all three dimensions. - Skipped
perspective-self
since it doesn't correspond to a separate CSS property. Feel free to open a new discussion if it seems like something is missing without this.
Closing this PR in favour of #13248. Please follow along there!