tailwindcss icon indicating copy to clipboard operation
tailwindcss copied to clipboard

Add first-class 3d transform utility support

Open brandonmcconnell opened this issue 1 year ago • 15 comments

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

brandonmcconnell avatar Apr 11 '23 10:04 brandonmcconnell

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 avatar Apr 11 '23 10:04 adamwathan

@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.

brandonmcconnell avatar Apr 11 '23 11:04 brandonmcconnell

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

brandonmcconnell avatar Apr 13 '23 02:04 brandonmcconnell

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.

brandonmcconnell avatar Apr 17 '23 00:04 brandonmcconnell

@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.

brandonmcconnell avatar Apr 17 '23 18:04 brandonmcconnell

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 keyframes). 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)

sambauers avatar Apr 17 '23 22:04 sambauers

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

sambauers avatar Apr 17 '23 22:04 sambauers

@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 avatar Apr 17 '23 23:04 sambauers

@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 the 100px 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, but perspective() (perspective-self) serves a different purpose than the perspective 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 different perspective values.

    MDN makes this distinction here: "The perspective() transform function is part of the transform value applied on the element being transformed. This differs from the perspective and perspective-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!

brandonmcconnell avatar Apr 17 '23 23:04 brandonmcconnell

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 avatar Apr 18 '23 00:04 sambauers

@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.

brandonmcconnell avatar Apr 18 '23 01:04 brandonmcconnell

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 avatar Sep 07 '23 20:09 adamwathan

@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.

brandonmcconnell avatar Sep 21 '23 01:09 brandonmcconnell

is there any result ?

ShingLi avatar Dec 10 '23 06:12 ShingLi

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

thejskhan avatar Feb 04 '24 12:02 thejskhan

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:

  1. All transforms are done using specific CSS properties, rather than transform functions. This prevents conflicts updating the transform property, and makes it easier to see in the inspector.
  2. rotate-x, rotate-y, and rotate-z contribute to the axis of rotation, rather than specifying separate angles of rotation. See #13248 for more details.
  3. Added scale-3d to scale in all three dimensions.
  4. 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!

KrisBraun avatar Mar 15 '24 18:03 KrisBraun