To implement effects based on the Pointer's position relative to the hovered element, one needs to resort to JavaScript. See example libraries like Tilt.js, Atropos, etc. that provide this functionality.
I've given all of this some thought, which might come in handy in possible future discussions on this.
Nesting
Should nesting be allowed or not?
Either way: I've have the children only gain access to the positional info from nearest parent with positional tracking activated.
Why a pseudo-class, and why :hover-3d?
😬 As the relative Pointer Position can only be calculated while hovering I started off from :hover and built further upon that. Seemed like a logical thing to do.
Advantages of using a pseudo-class other than :hover:
Rendering Engines should not unnecessarily calc the pointer's position for all elements, but only for those that request it.
Rendering Engines need to know which element to calculate against
E.g. is it the position of the mouse on top of the inside the
, or the
itself?
Disadvantages of using a pseudo-class:
You can't use the calculated values in non :hover-3d selectors
(Clunky) Workaround: use Custom Properties and have the Env Vars overwrite their values.
🤔 Or would the calculated values also be applied to the targeted element (sans :hover-3d) itself?
If so: then :hover-3d loses some value, as the code could as well go in the regular selector.
div {
transform: rotate3d(
env(--pointer-y, 0),
env(--pointer-x, 0),
0,
-15deg
);
}
div:hover-3d {
/* 🤔 What would be put here now? Or would we simply need a different way to activate positional tracking? */
}
The choice for :hover-3d was purely based on the fact that I saw a 3D demo which sparked this idea. Don't have any strong opinion on this name. It can as well be :hover-with-position-info-yolo-web3-crypto if that's more in line with how things are named.
Coordinate System
I played a bit with the Coordinate System that could be used with this. Main question I had here was: Should the origin be at center-center (X/Y Coordinate System), or at top-left (Page Coordinate System)?
The choice of Coordinate System has some side-effects for CSS Authors:
Many things in CSS use the Page Coordinate System, so using a Page Coordinate System is handy for those type of things
For 3D-rotations (my initial intent) a X/Y Coordinate system with the Origin at the center-center of the element is more handy as the default transform-origin is 50% 50%
I've compared various possible systems, but am personally leaning to the X/Y Coordinate System (XYCS) + range [-1,1] system.
Page Coordinate System (PCS)
Params
Origin 0,0 sits at top-left of the box
Side-Effect: center-center is is at [0.5,0.5]
Range of values for --pointer-x and --pointer-y: [0,1]
div {
/* Coordinates that indicate the center of the element */
--pointer-x: 0.5;
--pointer-y: 0.5;
background: transparent
radial-gradient(
25vw 25vw
at
calc(var(--pointer-x) * 100%) /* ✅ Values can be used directly, as bg-position also uses PCS */
calc(var(--pointer-y) * 100%)
,
lightblue,
rebeccapurple
)
no-repeat 0 0
;
transform: rotate3d(
calc((-1 * var(--pointer-y)) + 0.5), /* ❌ Values need to be manually offset by 0.5 due to origins of transform and PCS not aligning */
calc((1 * var(--pointer-x)) - 0.5),
0,
-15deg
);
}
div::before {
/* ❌ Polar Coordinates don't make any sense here?! */
}
Advantages:
Handy for nested stuff that also use PCS (positioned elements, backgrounds, etc.)
Disadvantages:
Less handy for 3D effects, as the used Origin (top-left) differs the default transform-origin
Values need to be offset by 0.5 before they can be used.
Calculating Polar Coordinates from this seems a bit useless here?
X/Y Coordinate System (XYCS) + range [-0.5,0.5]
Params
Origin 0,0 sits at center-center of the box
Range of values for --pointer-x and --pointer-y: [-0.5,0.5]
div {
/* Coordinates that indicate the center of the element */
--pointer-x: 0;
--pointer-y: 0;
background: transparent
radial-gradient(
25vw 25vw
at
calc((var(--pointer-x) + 0.5) * 100%) /* ❌ Values need to be manually offset by 0.5 due to origins of background (which uses PCS) not aligning with XYCS */
calc((-1 * (var(--pointer-y)) + 0.5) * 100%)
,
lightblue,
rebeccapurple
)
no-repeat 0 0
;
transform: rotate3d(
calc(var(--pointer-y) * 2), /* ⚠️ Values need to be multiplied by 2 if you want them to go from "nothing" to "full". Otherwise you'd end up with -7.5deg */
calc(var(--pointer-x) * 2),
0,
-15deg
);
}
div::before {
content: '';
pointer-events: none;
display: block;
height: 1px;
width: 50%;
background: red;
position: absolute;
top: 50%;
left: 50%;
transform-origin: 0 50%;
transform:
rotate(calc(var(--pointer-angle, 0) * -1deg)) /* ✅ Values can be used directly */
scaleX(var(--pointer-distance))
;
}
Advantage(s):
Easy for 3D effects, as the used Origin is equal to the default transform-origin
0.5 (or 50%) resembles the actual distance that was travelled from the origin
E.g. A --pointer-x value of 0.5 is also “50% of the width”
Handy when used with cqw 😎
The resulting Polar Coordinates are handy
Disadvantages:
A value of 0.5 is not-handy for scaling purposes: you need to multiply the values by 2.
Values needs manual adjustment to be used with Things™ that use the Page Coordinate System
E.g. background-position, top, left, …
X/Y Coordinate System (XYCS) + range [-1,1]
Params
Origin 0,0 sits at center-center of the box
Range of values for --pointer-x and --pointer-y: [-1,1]
div {
/* Coordinates that indicate the center of the element */
--pointer-x: 0;
--pointer-y: 0;
background: transparent
radial-gradient(
25vw 25vw
at
calc(((var(--pointer-x) / 2) + 0.5) * 100%) /* ❌ Values need to be manually divided by 2 and be offset by 0.5 due to origins of background (which uses PCS) not aligning with XYCS */
calc((-1 * (var(--pointer-y) / 2) + 0.5) * 100%)
,
lightblue,
rebeccapurple
)
no-repeat 0 0;
transform: rotate3d(
var(--pointer-y), /* ✅ Values can be used directly */
var(--pointer-x),
0,
-15deg
);
}
div::before {
content: '';
pointer-events: none;
display: block;
height: 1px;
width: 50%;
background: red;
position: absolute;
top: 50%;
left: 50%;
transform-origin: 0 50%;
transform:
rotate(calc(var(--pointer-angle, 0) * -1deg)) /* ✅ Values can be used directly */
scaleX(var(--pointer-distance))
;
}
Advantage(s):
Easy for 3D effects, as the used Origin is equal to the default transform-origin
A max-value of 1 is handy for scaling purposes
The resulting Polar Coordinates are handy
Disadvantages:
1 does not resemble the actual distance that was travelled from the origin
E.g. A --pointer-x value of 1 is not “100% of the width”, but 50%.
Not so handy when used with cqw 😎
Values needs manual adjustment to be used with Things™ that use the Page Coordinate System
E.g. background-position, top, left, …
Can't we just do this with Houdini?
While the calculations to determine the position can indeed be done via Houdini, there's no way to bounce those calculated values back to the CSS. Custom Properties are Input for Worklets, not Output.
(Feel free to correct me on this, would love to see that 🤩)
Custom properties are solely for use by authors and users; CSS will never give them a meaning beyond what is presented here.
Also, sometimes you use var() and sometimes env(). Environment variables must have the same value everywhere, can't depend on the element.
Then, pseudo-classes are just a way to select elements. They shouldn't which CSS features you can use in these elements.
I don't think there is a clear choice for the coordinates, either. You mention center of the box, but that could even change depending on whether we consider the content area, padding area, border area...
Note that saying that -1 is the left, 0 is the center, and 1 is the right doesn't imply that the range of values is [-1,1]. Because an element can be hovered when the pointer is outside of its border area (in front of an overflowing descendant).
Also, I guess that most usecases would need the sizes of the element. Or want the coordinates with respect to the screen, or the containing block.
Overall, it seems to me that this is better fitted for JS.
Yeah, this isn't a var() value, but that's a small concern; we can just assume it's a different function.
env() is possible, but only for whole-window pointer position. env() values aren't allowed to change based on context.
So this could be done, just as a new pair of functions, for x and y, that each returned a <length>. Since the rest of the CSS uses "0,0 is top-left of box", it would work the same way. They'd probably take a <box> value to determine which box they're measuring relative to. Maybe another value to control whether they return a <length> or a <percentage>, since I can see use-cases for both and you can't easily convert between them.
Ah yes. Started off with Custom Properties in my explorations, and then worked my way back to something proposal-y which uses Env Vars but forgot to remove the -- prefix in the process.
Also, sometimes you use var() and sometimes env()
In the part where I compare the various coordinate systems I indeed use var() as they were code explorations built with Custom Properties. These snippets are more meant to illustrate the side-effects of the used coordinate system.
Environment variables must have the same value everywhere, can't depend on the element.
Oh, that I did not know. This would indeed make env vars not part of a possible solution here.
pseudo-classes are just a way to select elements. They shouldn't which CSS features you can use in these elements.
I started off from hover and built from there. I'm sure WG has better solutions to this that align with all other existing things :)
@tabatkins
env() values aren't allowed to change based on context
Time for a new function? j/k 🙃
Maybe another value to control whether they return a <length> or a <percentage>, since I can see use-cases for both and you can't easily convert between them.
In my experimentation I found that "percentage expressed as a float" hit the sweet spot:
Mutiply by 100% to get a percentage
Multiply by qi/qb to get a length
Could of course be that I'm overlooking things here.
Since we can nowadays define alternative animation timelines with animation-timeline, a slightly different approach to this issue could be to create some sort of HoverTimeline which authors can use.
@property --pointer-x { … }
@property --pointer-y { … }
@keyframes track-x-value {
from { --pointer-x: -1; }
to { --pointer-y: 1; }
}
@keyframes track-y-value {
from { --pointer-y: -1; }
to { --pointer-y: 1; }
}
el {
animation: track-x-value auto linear, track-y-value auto linear;
animation-timeline: hover(vertical), hover(horizontal); /* 👈 THIS */
transform: rotate3d(
var(--pointer-y),
var(--pointer-x),
0,
-15deg
);
}
Thanks, @bramus! I think we may need a new hover-timeline property for this to scope the range to an element, probably with a corresponding -inset property as well.
Only thing that's a bit different here is that the analogous of ScrollTimeline here is hover() timeline that matches the viewport and not an element, or perhaps this could simply be done with:
I also came across this mouse mask effect, which relies on JS for other things as well, but can be reduced to a version that only needs the relative position of the cursor.
I was thinking of using an ::after that takes the x and y of the mouse and draws a triangle to the target, with a delay on the triangle updating. Though I imaging triggering paint on the after would be heavy, so if there is a native solution in the works that would be way better (and flee like less of a hack).
As for syntax it almost feels like this should be wrapped in with anchor somehow.
div {
/* Coordinates that indicate the center of the element */
anchor-name: --pointer;
background: transparent radial-gradient(25vw 25vw at calc(((anchor(--pointer pointer-x) / 2) + 0.5) * 100%)
/* ❌ Values need to be manually divided by 2 and be offset by 0.5 due to origins of background (which uses PCS) not aligning with XYCS */
calc((-1 * (anchor(--pointer pointer-y)/ 2) + 0.5) * 100%),
lightblue,
rebeccapurple) no-repeat 0 0;
transform: rotate3d(anchor(--pointer pointer-y),
/* ✅ Values can be used directly */
anchor(--pointer pointer-x),
0,
-15deg);
}
(Which after writing this I see is already discussed in #8639)
Introduction
To implement effects based on the Pointer's position relative to the hovered element, one needs to resort to JavaScript. See example libraries like Tilt.js, Atropos, etc. that provide this functionality.
I would like CSS to have this ability built in: i.e. have CSS use the Pointer's position, without needing to rely on JavaScript nor a clever but nasty hack that relies on injecting a few 100 extra elements.
This position information could be used for 3D effects, popover information boxes, Houdini code that takes the mouse position as input, etc.
Proposed Solution
The proposed solution is two-fold:
I came up with these:
:hover-3d
pseudo-class--pointer-x
: X-position of the pointer--pointer-y
: Y-position of the pointer--pointer-angle
: Angle from the Origin to the Pointer Position--pointer-distance
: Distance from the Origin to the Pointer PositionSyntax
Demo
Here's a demo that exposes the 4 proposed env vars as Custom Properties, to see what you can do with them
https://codepen.io/bramus/full/porJLgR/250186328bafbb5e63bb1a6f6f2ada044
Considerations / Questions
I've given all of this some thought, which might come in handy in possible future discussions on this.
Nesting
Why a pseudo-class, and why
:hover-3d
?😬 As the relative Pointer Position can only be calculated while hovering I started off from
:hover
and built further upon that. Seemed like a logical thing to do.:hover
:Disadvantages of using a pseudo-class:
You can't use the calculated values in non
:hover-3d
selectors(Clunky) Workaround: use Custom Properties and have the Env Vars overwrite their values.
🤔 Or would the calculated values also be applied to the targeted element (sans
:hover-3d
) itself?If so: then
:hover-3d
loses some value, as the code could as well go in the regular selector.The choice for
:hover-3d
was purely based on the fact that I saw a 3D demo which sparked this idea. Don't have any strong opinion on this name. It can as well be:hover-with-position-info-yolo-web3-crypto
if that's more in line with how things are named.Coordinate System
I played a bit with the Coordinate System that could be used with this. Main question I had here was: Should the origin be at center-center (X/Y Coordinate System), or at top-left (Page Coordinate System)?
The choice of Coordinate System has some side-effects for CSS Authors:
transform-origin
is50% 50%
I've compared various possible systems, but am personally leaning to the X/Y Coordinate System (XYCS) + range
[-1,1]
system.Page Coordinate System (PCS)
Params
0,0
sits at top-left of the box[0.5,0.5]
--pointer-x
and--pointer-y
:[0,1]
Demo (using Custom Properties): https://codepen.io/bramus/pen/4b04dbf201c6a542d276506503a56e68
Advantages:
Disadvantages:
transform-origin
0.5
before they can be used.X/Y Coordinate System (XYCS) + range
[-0.5,0.5]
Params
0,0
sits at center-center of the box--pointer-x
and--pointer-y
:[-0.5,0.5]
Demo (using Custom Properties): https://codepen.io/bramus/pen/2711c3083d1c9892ad044dacf9526c26
Advantage(s):
transform-origin
0.5
(or50%
) resembles the actual distance that was travelled from the origin--pointer-x
value of0.5
is also “50%
of the width”cqw
😎Disadvantages:
0.5
is not-handy for scaling purposes: you need to multiply the values by 2.background-position
,top
,left
, …X/Y Coordinate System (XYCS) + range
[-1,1]
Params
0,0
sits at center-center of the box--pointer-x
and--pointer-y
:[-1,1]
Demo (using Custom Properties): https://codepen.io/bramus/pen/250186328bafbb5e63bb1a6f6f2ada04
Advantage(s):
transform-origin
1
is handy for scaling purposesDisadvantages:
1
does not resemble the actual distance that was travelled from the origin--pointer-x
value of1
is not “100%
of the width”, but50%
.cqw
😎background-position
,top
,left
, …Can't we just do this with Houdini?
While the calculations to determine the position can indeed be done via Houdini, there's no way to bounce those calculated values back to the CSS. Custom Properties are Input for Worklets, not Output.
(Feel free to correct me on this, would love to see that 🤩)
Which Pointer?
Performance
Privacy
Starting with
--
would break this promise: https://drafts.csswg.org/css-variables-1/#custom-propertyAlso, sometimes you use
var()
and sometimesenv()
. Environment variables must have the same value everywhere, can't depend on the element.Then, pseudo-classes are just a way to select elements. They shouldn't which CSS features you can use in these elements.
I don't think there is a clear choice for the coordinates, either. You mention center of the box, but that could even change depending on whether we consider the content area, padding area, border area...
Note that saying that -1 is the left, 0 is the center, and 1 is the right doesn't imply that the range of values is [-1,1]. Because an element can be hovered when the pointer is outside of its border area (in front of an overflowing descendant).
Also, I guess that most usecases would need the sizes of the element. Or want the coordinates with respect to the screen, or the containing block.
Overall, it seems to me that this is better fitted for JS.
Yeah, this isn't a
var()
value, but that's a small concern; we can just assume it's a different function.env()
is possible, but only for whole-window pointer position. env() values aren't allowed to change based on context.So this could be done, just as a new pair of functions, for x and y, that each returned a
<length>
. Since the rest of the CSS uses "0,0 is top-left of box", it would work the same way. They'd probably take a<box>
value to determine which box they're measuring relative to. Maybe another value to control whether they return a<length>
or a<percentage>
, since I can see use-cases for both and you can't easily convert between them.@Loirooriol
Ah yes. Started off with Custom Properties in my explorations, and then worked my way back to something proposal-y which uses Env Vars but forgot to remove the
--
prefix in the process.In the part where I compare the various coordinate systems I indeed use
var()
as they were code explorations built with Custom Properties. These snippets are more meant to illustrate the side-effects of the used coordinate system.Oh, that I did not know. This would indeed make env vars not part of a possible solution here.
I started off from hover and built from there. I'm sure WG has better solutions to this that align with all other existing things :)
@tabatkins
Time for a new function? j/k 🙃
In my experimentation I found that "percentage expressed as a float" hit the sweet spot:
qi
/qb
to get a lengthCould of course be that I'm overlooking things here.
Are TiltJS/Atropos the only usecases you see this working for or do you feel there might be something else also?
@mystrdat I've mentioned some use-cases in the OP:
Another thing that came to mind is these two-up image comparison things, but then using hover.
Since we can nowadays define alternative animation timelines with
animation-timeline
, a slightly different approach to this issue could be to create some sort ofHoverTimeline
which authors can use.See https://codepen.io/bramus/full/porJLgR for a demo that could be simplified using this.
/cc @ydaniv who was interested in this type of timeline.
Thanks, @bramus! I think we may need a new
hover-timeline
property for this to scope the range to an element, probably with a corresponding-inset
property as well. Only thing that's a bit different here is that the analogous of ScrollTimeline here ishover()
timeline that matches the viewport and not an element, or perhaps this could simply be done with:There have been a lot of cases where I used JS strictly to get the pointer position & store it in
--x
,--y
variables I then used in the CSS.First that comes to mind is this entry & exit aware button
:hover
effect. Or this highlight effect.I also came across this mouse mask effect, which relies on JS for other things as well, but can be reduced to a version that only needs the relative position of the cursor.
Would this offer a CSS only solution for menu safe triangles too?
I suspect you'd still need JavaScript to run the logic for the safe area?
Fwiw we've been discussing a native safe area mechanism for popovers and interest based triggering (e.g. hover) in OpenUI https://github.com/openui/open-ui/issues/963
I was thinking of using an
::after
that takes the x and y of the mouse and draws a triangle to the target, with a delay on the triangle updating. Though I imaging triggering paint on the after would be heavy, so if there is a native solution in the works that would be way better (and flee like less of a hack).As for syntax it almost feels like this should be wrapped in with anchor somehow.
(Which after writing this I see is already discussed in #8639)
Some more recent demos where authors use the pointer position: