CSS only Direction aware hover

Hi,

Here’s something I thought I’d share with you seeing as things have been pretty quiet :slight_smile:

I recently saw a demo using JS to create direction aware CSS hovers and I thought to myself I wonder if that could be done with CSS alone and no script? (I often have these thoughts). I also thought it might be interesting (and of use) to show my thought processes and to see how I ended up with the working version.

So without further ado here’s my CSS only version (modern browsers only).

[edit]
New Improved version with suggested edits from Francky in post 11.[/edit]

If you mouse in from any direction the hover effect appears displays a message animating from that direction. The technique uses the same message for each image so it’s not 4 messages hiding at each edge).

The Method.

Obviously css doesn’t know anything about the directional movement of the mouse (unlike JS) so the first step was to think up a mechanism that would allow the animation to be triggered when entering the image from any side. I already had in mind a way to do this and the idea was to place 4 small elements around the edges of the image so that they would become active when they were moused over.

Here’s a rough drawing of the first step:
Img 1:

I used 4 “b” elements in the html and absolutely placed them into position around the html. I was later hoping to reduce these elements and perhaps use :before and :after instead but when constructing a demo its usually best to use the simplest approach first and then refine it later. After all it may not work anyway so just keep it simple until it works… or doesn’t work.:slight_smile: (If you don’t like “b” elements then use spans instead but as there were 4 of them it looked messy as spans (the “b” is mostly semantically neutral anyway although - I’m not going to argue this point anyway so use what you feel best and appropriate).

With these 4 elements in place it is then a simple matter to activate any of the elements individually when hovered.

e.g.


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Untitled Document</title>
<style type="text/css">
div{
	width:150px;
	height:150px;
	background;red;
	border:1px solid #000;
	position:relative;	
}
b {
	display:block;
	width:150px;
	height:20px;
	position:absolute;
	left:0;
	z-index:2;
}
b:nth-of-type(1) { top:0 }
b:nth-of-type(2) { bottom:0 }
b:nth-of-type(3) {
	top:0;
	width:20px;
	height:150px;
}
b:nth-of-type(4) {
	top:0;
	width:20px;
	height:150px;
	left:auto;
	right:0
}
b:hover{background:red}
</style>
</head>

<body>
<div><b></b><b></b><b></b><b></b></div>
</body>
</html>

Img 2:

If you hover at the edge you will see that a small strip at the edge turns red. This is just basic CSS and nothing clever here at all but it does give an indicator of where the mouse is.

The problem is trying to tie this hover effect in with showing a message and animating the message from that side.

There are now a few things we need to think about.

  1. The strip we added to the image is only 20px deep so once we go past that strip the hover effect is lost.
  2. If we continue across the image with the cursor then the opposite strip is then triggered and would spoil the effect we are aiming for.
  3. How does this help with sliding a message in from that side?

Points one and 2 above can be solved in a single step by ensuring that as soon as we hover the little strip we then increase its size and z-index so that it covers the whole image. This will keep the cursor active all the way across the image and at the same time stop the other strips from being triggered.

All we need to do that is this:


b:hover {
	height:150px;
	width:150px;
	z-index:99;
	background:red;
}

Note that I am using a red background in the explanation but that is only to make it visible for the explanation. This area will be transparent in the full demo.

Ok now for the hard part:

We have established an effect when we hover over the image but how to make that effect animate our message across the image?

So far the html looks like this:


<div><b></b><b></b><b></b><b></b></div>

Lets add some html for our sliding message.


<div><b></b><b></b><b></b><b></b><strong>This is the sliding message</strong></div>

So when we hover a “b” element we want to slide in the message (which will be hidden by default). We could use the adjacent sibling selector so that when the “b” element is hovered we can select an adjacent element.

e.g.


b:hover + strong{styles here..}

However, that soon gets messy as you would need to start doing things like this:


b:hover + b + b + b + strong{styles here..} 
b:hover + b + b + strong{styles here..} 
b:hover + b + strong{styles here..} 
b:hover + strong{styles here..} 

We can simplify that by using the general sibling selector. This allows us to select the strong element while any of the b elements are hovered.


b:hover ~ strong{styles here..} 

Unfortunately the above method doesn’t give us a unique reference to the element being hovered as we need to apply 4 different sets of rules to the strong element depending on which b element was hovered. Therefore we need to identify each b element uniquely which can be done with a class or as I have done in the demo by using :nth-of-type(n).

Finally we have a way of applying 4 different styles to the strong element (the element that holds the message) depending on which side the mouse approaches.

Working Code so far:


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Untitled Document</title>
<style type="text/css">
div{
	width:150px;
	height:150px;
	background;red;
	border:1px solid #000;
	position:relative;	
}
b {
	display:block;
	width:150px;
	height:20px;
	position:absolute;
	left:0;
	z-index:2;
}
b:nth-of-type(1) { top:0 }
b:nth-of-type(2) { bottom:0 }
b:nth-of-type(3) {
	top:0;
	width:20px;
	height:150px;
}
b:nth-of-type(4) {
	top:0;
	width:20px;
	height:150px;
	left:auto;
	right:0
}
b:hover {
	height:150px;
	width:150px;
	z-index:99;
}
strong{
	width:150px;
	height:150px;
	display:none;
	text-align:center;
	color:#fff;	
}
b:hover ~ strong {display:block;z-index:99}
b:nth-of-type(1):hover ~ strong {background:blue}
b:nth-of-type(2):hover ~ strong {background:green}
b:nth-of-type(3):hover ~ strong {background:yellow}
b:nth-of-type(4):hover ~ strong {background:red}
</style>
</head>
<body>
<div><b></b><b></b><b></b><b></b><strong>This is the sliding message</strong></div>
</body>
</html>

As you can see if you hover the element from any side the message turns a different background colour. This is very close to what we want except that we now need to animate the message from that side as well!

When I first approached this I thought it could simply be done with transitions. I’ll just move the element off to the side and then on hover change its position so that it slides into place. I got pretty close but ultimately failed at the final step. I could animate the message ok but it only animated from where it started and although I could set up a different end position with the hover effect I couldn’t specify 4 different starting positions for the message!

Keyframes to the rescue.

With keyframes you have the ability to set a “from” starting point for your animation which doesn’t necessarily have to conform to where the original element is situated. This allows the message to start in the appropriate place for each animation and then finish up where we want.

We will need 4 keyframes for each direction which I will call simply up, down, left and right.

That will leave is with full code that looks like this (Firefox only):


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Untitled Document</title>
<style type="text/css">
div {
	width:150px;
	height:150px;
	border:1px solid #000;
	position:relative;
	margin:20px;
	overflow:hidden;
}
b {
	display:block;
	width:150px;
	height:20px;
	position:absolute;
	left:0;
	z-index:2;
}
b:nth-of-type(1) { top:0 }
b:nth-of-type(2) { bottom:0 }
b:nth-of-type(3) {
	top:0;
	width:20px;
	height:150px;
}
b:nth-of-type(4) {
	top:0;
	width:20px;
	height:150px;
	left:auto;
	right:0
}
b:hover {
	height:150px;
	width:150px;
	z-index:99;
}
strong {
	width:150px;
	height:150px;
	text-align:center;
	color:#fff;
	position:absolute;
	top:-150px;
	background:red;
}
b:nth-of-type(1):hover ~ strong {
 -moz-animation:down .3s ease-in-out forwards;
 -webkit-animation: down .3s ease-in-out forwards;
 animation: down .3s ease-in-out forwards;
}
@keyframes down { from {
 transform: translateY(0px);
 opacity:0;
}
to {
	transform: translateY(150px);
	opacity:1
}
}
b:nth-of-type(2):hover ~ strong {
 -moz-animation:up .3s ease-in-out forwards;
 -webkit-animation: up .3s ease-in-out forwards;
 animation: up .3s ease-in-out forwards;
}
 @keyframes up { from {
 transform: translateY(300px);
 opacity:0;
}
to {
	transform: translateY(150px);
	opacity:1
}
}
b:nth-of-type(3):hover ~ strong {
 -moz-animation:left .3s ease-in-out forwards;
 -webkit-animation: left .3s ease-in-out forwards;
 animation: left .3s ease-in-out forwards;
 top:0;
}
@keyframes left { from {
 transform: translateX(-150px);
 opacity:0;
}
to {
	transform: translateX(0px);
	opacity:1
}
}
 b:nth-of-type(4):hover ~ strong {
 -moz-animation:right .3s ease-in-out forwards;
 -webkit-animation: right .3s ease-in-out forwards;
 animation: right .3s ease-in-out forwards;
 top:0;
 left:auto;
 right:0;
}
@keyframes right { from {
 transform: translateX(150px);
 opacity:0;
}
to {
	transform: translateX(0px);
	opacity:1
}
}
</style>
</head>

<body>
<div><b></b><b></b><b></b><b></b><strong>This is the sliding message</strong></div>
</body>
</html>


As you can see we now have a working demo and the message will fly in from the side that the mouse approaches.

You will of course need to add the vendor prefixes which I have omitted for brevity but refer to the full example which has the pre-fixes in place. The [URL=“http://www.pmob.co.uk/temp/direction-hover4.htm”]full example is just tidied up and has images, content and better alignment but uses the same techniques discussed above.

Pros and Cons

There are some drawbacks that I can see with this effect over a JS version:

1) You need the 4 extra elements in the html.

e.g.


<b></b><b></b><b></b><b></b>

Other than those elements the html is normal html and needs n other special considerations. Of course you could just add those with Javascript but I guess that defeats the purpose of a CSS only effect.

I was hoping to use:before and :after to minimise the use of those “b” elements but I couldn’t find a reliable way of doing this. Maybe you can come up with a better idea?

2) Unlike the JS version the animation doesn’t return the way it cam from when the mouse is moved out of the element. I can make it always hover upwards on muse out but that ruins the effect. The problem is that it is possible to have an animation effect on mouse out but then we lose the position of the message and it goes back to where it was originally placed (which is why I could only make it go upwards). There may well be a way to fix this issue but I kind of prefer it as it is.

3 If you remember we created the “trigger” areas with 4 little strips of 20px depth. That means that if we move the mouse quickly to the centre it will jump over the trigger and not be animated. Again, I like this effect because it acts almost as a “hover-intent” and you don’t get the messages jumping every time you move your mouse to a menu.

4 The 20px section in each corner of the image is overlapped by each subsequent trigger area so we don’t get a perfect trigger area along each side but again that’s just a small issue.

Pros

  1. CSS only and no script.

  2. It’s fun to do.

  3. Looks pretty good.

[B]Refer to the original demo[/B] for the full code as I have not explained all the code in details but leave a message if there’s anything you don’t understand or indeed if you have some improvements as this has mainly been a proof of concept.

If you read this far then well done :slight_smile:

1 Like

Wow… very nice, Paul!
BTW, shouldn’t this effect work with transitions as well? Is there an inherent advantage to an animation?
:slight_smile:

Hi Ray,

Thanks:)

You can only make one slide effect with a transition because you can only slide the element from where it currently is and then move it to another position on hover. The keyframe approach allows you to set the start position of the element before the animation begins and then slide it into the new end position ( and adjust all positions in-between if needed). We actually need 4 different start positions and 4 different end positions and although we can change the end position for each on hover we can’t change the start position before hover.

Without changing the start position of the element the slide effect would always be the same direction.:slight_smile:

Of course now that it is working the slide could be changed to more complicated effects such as a roll-in using transforms and rotate etc.

Nice work, Paul. The funny thing is, the JS version doesn’t work for me in Chrome. (The box only slides from the left, no matter where I attack it from.) CSS FTW!

Thanks Ralph. :slight_smile:

The funny thing is, the JS version doesn’t work for me in Chrome. (The box only slides from the left, no matter where I attack it from.)

Weird, It seems to work for me in Chrome PC and Mac. You haven’t turned JS off have you lol :slight_smile:

LOL, of course n— … OMG, yes I have! :blush: I must have visited that site in the past and turned off JS. The browser still remembers. :smiley:

Nice work Paul. Guru as always :slight_smile:

Thanks Eric :slight_smile:

The [U]original Javascript example[/U] has his con in Firefox (js enabled :wink: ):

  • The script is o.k. for Chrome, Opera, Safari and IE10.
  • In FF23 as well as FF24 the direction-awareness is fine, as long as you move in a row (L/R) or in a column (up/down), but not in combined moves. If you try to circle in a square block of 4 items, the L/R-U/D directions are conflicting. :shifty:

Paul’s CSS approach is working in FF23 and 24 too! Congrats! :slight_smile:

Thanks Francky :slight_smile:

Paul O’B:
There are now a few things we need to think about.

  1. The strip we added to the image is only 20px deep

Yes, this is the culprit that quick moves are jumping over the overlay strip.
But extending the area will cause problems in the stacking order of the 4 overlay strips, and that’s invincible.

With clogging the empty space inside to halfway left and halfway right, there is no space for the top and bottom overlays (vice versa).
Now already the first and last 20px of the top overlay is z-indexed under the left or right overlay; the same for the bottom overlay. If you come up in the left corner, you go right instead of up, and so on: problems in all the corners…
But if you make the strip smaller, then the jump-error will be greater.
We need a kind of [I][U]border slants[/U][/I] for the squares of the corners!

CSS3-rotate
Happily we can rotate the overlays with the transform:rotate(…deg) property.
This is the desired figure for the top overlay area:

  • First we make the width and height of the 4 overlays the same as the width and height of the link area: in this case everything is150px.
  • Then we rotate the overlays with: b { transform: rotate(45deg) }.

At this point, there is created this one for the top overlay strip:

  • Now the need is only a wedge of a quarter of the cake, what can be accomplished by positioning the respective overlay strips. Because of the a { overflow: hidden} the unwanted area doesn’t hurt.
  • According to the a^2 + b^2 = c^2 of [U]Pythagoras[/U], the diagonal of the rotated square is
    SQR(150^2 + 150^2)=SQR(45000)=212,13203435596425732025330863145.
  • We can safely say it is 212px. Now the top of the first overlay <b> has to move the half of the diagonal upwards, so
    b:nth-of-type(1) { left: 0; top: -106px; }.
    The bottom-overlay has to move 106px down b:nth-of-type(2) { left: 0; bottom: -106px;}, the left and right overlay in the same way with the left and the right position.

What rests is resetting everything (positions and rotating) for the hover mode:

b:hover {
	top:0;
	left: 0;
	right: 0;
	bottom: 0;
	z-index:99;
	transform: rotate(0);
}

The complete square is then covered by the (transparent background of the) overlay, and the other 3 cannot have any influence anymore (just like in the original).

The result with transparency for the overlays:

No more gap for the mouse to jump in, quick moves are allowed; and corner problem solved. Unless I did overlook something. :wink:

Great work Francky:)

I was actually going to look at using rotate myself but you saved me a lot of work :slight_smile: I remember from this demo how you can make slanted target area with transforms and was going to try something similar but you beat me to it.:wink:

I think we have something pretty good now for a CSS only direction aware hover effect. We’ll need the webkit prefixes to get it working in chrome of course but that’s an easy fix. IE11 is working fine also and so I guess would IE10 but I haven’t checked.

It would be nice to reduce the number of “b” elements but I don’t think that’s possible.

Thanks, Paul.

I was actually going to look at using rotate myself (…)

Could not imagine you would not do that! :slight_smile:

IE11 is working fine also and so I guess would IE10 but I haven’t checked.

Oops! IE10 does nothing, nor does IE10 on your example. :rolleyes:

  • I don’t know if a -ms- prefix can help, didn’t try.

[Edit]Refinement
The b:hover {z-index: 99;} can be brought back now to b:hover {z-index: 3;}.[/edit]

I’ll have to have a look at that later as I am out for a few hours now. IE11 is working ok and I thought support was similar in IE10 but maybe its not?

I’ve added Franky’s additions here and also added the ms and webkit prefixes. It’s working in IE11 so if anyone with IE10 can check please (it won’t work in ie9 and under).

The next step would be to provide fallbacks for older browsers.

Paul, you rock! Love it!

Paul O’B #15:
… if anyone with IE10 can check please

Hi Paul,

  • Checked … IE10 is still not working. :eek:

But I’m getting on track why not, and a solution is possible. :slight_smile:
In the meantime I’m working on some lteIE9 alternatives.

Further notice and test pages will follow within a few days (today I have some other things to do).

So long,
Francky

That is some amazing work you did there!

I’ve just downloaded IE10 on my laptop and fixed the demo so its working in IE10 now :).

(Fallbacks also added back to IE6.)

Thanks Timo;

Hi Paul and other visitors,
As promised, here I am again. - In order to [U]avoid unwelcome surprises[/U] on the road or in the end, I’ve ‘rebuild’ the original; every now & then I checked the browser support.

  • Under WinXP: tested in FF23,Chrome30, Opera12.16, Safari5.1.7 and IE7.
  • Under Win7: tested in FF24, Chrome30, Opera17, Safari5.1.7 and IE10.
  • [U]Netrenderer[/U] is used for IE8 and IE9 screenshots.
  • For IE11 I assumed a good performance if IE10 is working.

Below my fist step - the explanation is rather extensive, for those who need more then a half word.

Step 1
First, the original [U]direction-hover5.htm[/U] (from #15 above) is using:

ul {margin:0 auto; text-align: center;}
li {display:inline-block;}
ul a {float: left; display:block;}

For IE7 (< IE7 we don’t care, isn’t it?) this doesn’t work:

To get all the images beautiful centered, also at smaller windows, for IE7 we need:

ul {margin: 0 auto;}
li {display: inline;}
ul a {display:block;}

The other browsers can handle this too.

Some annotations

  • To get rid of the “inline gaps” between the <li>'s, I escaped with grabbing the old bended fish-hook trick:

    <li>.....</li ><li>.....</li ><li>.....</li>
  • According to Zappa (“everything more than a mouthful is wasted”) this time I made the rotated square/triangle the smallest size to fit.
  • For the direction hovers I only draw the top triangle as 1st step: the yellow rotated square in the first item (forIE10+ and FF, Chrome, Opera and Safari), not yet with the sliding css; the other items are not working yet. You can see the whole square now: the a {overflow: hidden;} will be added later to plane away the upper part.
  • Instead of the 4 empty <b></b><b></b><b></b><b></b> for the direction triggers I’ll use 4 styled <hr><hr><hr><hr> elements, which make the html some less crowded.
    In this step: only 1 <hr>, in the 1st item.
  • Note (1): this causes the html to be [U]invalid[/U], because a <hr> is a block level element and not allowed inside a link <a> …</a>; but the browsers are forgiving.
    Semantic passionates can turn their head now, if they didn’t that already. :wink:
  • Note (2): I tried to style a <br> instead, but that failed miserably.
    Misusing an <input> element: is also valid, but styling did’nt work either.
  • Note (3): If you want the html to be valid, you can use the <b></b><b></b><b></b><b></b> solution of the original.
  • For IE9 and older the css3-transitions don’t work, so the 4 <hr>'s are not there for them: hr {display: none;} in a conditional comment (CC).
  • Instead, IE9 and older are served with a normal a:hover strong {display: block} to get the message visible, also in the CC. And they are corrected for the positioning of the message (which is tuned for the css-animations of the better browsers).
  • Already busy with CC’s, we can add an IE-only filter for the semi-transparency of the background of the message block (while maintaining the full opacity for the text): for IE8 and before, who don’t support the rgba color property. - Thanks Chris Coyier ([U]css-tricks.com/rgba-browser-support[/U]/).
  • A <meta http-equiv=“X-UA-Compatible” content=“IE=Edge,chrome=1”> is added to prohibit that IE10+ visitors can use the compatibility mode (falling back to older IE, and destroying the direction hover!).

So far: to be continued! :slight_smile:

Edit:

Ai! This time I was to late. :slight_smile:
Busy with writing down my comments, I didn’t see the update. :wink: