1

I have an h1 tag, and I'm attempting to add a cool animation to it, where the border will 'grow' from the top-left and bottom-right points.

I'm doing this by wrapping my h1 in two divs, and each div has a ::before and ::after pseudo-element.

When the root div is moused over, the pseudo-elements of these divs will 'shrink', revealing the border below.

The issue is, the ::before pseudo-element of my root div is behind the border, and so the border is shown immediately on mouseover. Setting the z-index of the ::befores and ::afters will fix it; however, I do not want to do this -- this is a snippet of code I'm making for copy / pasting. No z-indexs are set, so I'm confused as to why this is happening.

Before, I had no divs besides what was needed for the animation, and it worked fine -- but how would that have to do with it?

This seems to be the issue: ::before pseudo-element stacking order issue.

However, no answers are suggested as to how to fix it. I could add a third div, of course, but I'd like to keep that as a last resort.

Fiddle of issue: https://jsfiddle.net/zppqgn6s/

To see it fixed with z-index; to see what it's supposed to look like, uncomment line 34.

Quelklef
  • 1,999
  • 2
  • 22
  • 37

1 Answers1

7

Reason for pseudo-element's border being below child:

Answer to the original question on why the pseudo-element's (:before) background goes behind the child (h1) can be found in BoltClock's answer (that you had linked within the question). The :before pseudo-element is actually inserted before the content of the root div (which includes the h1).

Here is the general structure of the elements that are used in the demo:

#anim              /* This is the first element inside root and is positioned (relative) */
    #anim:before   /* This is positioned absolutely with respect to the parent */
    div            /* This element is positioned relatively */
        div:before /* This element is positioned absolutely with respect to the div */
        h1         /* This element doesn't have any positioning */
        div:after  /* This element is positioned absolutely with respect to the div */
    #anim:after    /* This is positioned absolutely with respect to the parent */

Now based on the specs for the visual rendering layers, the below is what happens:

#anim              /* Parent element and its background, border gets painted first (Layer 0) */
    #anim:before   /* Positioned descendant, creates stacking context nested within parent (Layer 0.1)*/
    div            /* Positioned descendant of #anim, second child in flow (Layer 0.2) */
        div:before /* Positioned descendant of div, first child in flow (Layer 0.2.2) */
        h1         /* Non positioned, as per Point 3 gets positioned lowest (Layer 0.2.1) */
        div:after  /* Positioned descendant of div, second such child in flow (Layer 0.2.3) */
    #anim:after    /* Positioned descendant of #anim, third child in flow (Layer 0.3) */

As can be seen based on layer numbers (provided in inline comments), the h1 element is positioned above #anim:before (but below all the other three elements that produce the border shrink effect).

Solutions:

The only solution to this is to make the child (h1) get painted after the :before element. This can be achieved by doing either of the below (but both of them require a z-index to be set):

  • Setting the h1 to position: relative with z-index: -1 (so that it goes behind #anim:before)
  • Setting z-index: 1 (or above) to the #anim:before element (so that it goes above the h1)

Alternate Solutions/Approaches:

Actually, you don't need all the extra elements for this particular animation (of borders converging from top-left and bottom-right to meet each other). They can be achieved using the single h1 element itself and I am posting this answer to illustrate two of those methods. Although you weren't asking for other methods, I like the effect and it seemed to be a good place to post this answer.

By using linear-gradient background images:

In this approach, we create one gradient (actually nothing but a solid color as it doesn't change colors) for each side of the border, position them appropriately and then transition the size from 0% to 100%. For top and bottom borders, the size in X-axis should be changed from 0% to 100% on hover while for left and right borders, size in Y-axis should be changed from 0% to 100%.

h1 {
  position: relative;
  display: inline-block;
  padding: 4px;
  background: linear-gradient(to right, #000, #000), linear-gradient(to right, #000, #000), linear-gradient(to bottom, #000, #000), linear-gradient(to bottom, #000, #000);
  background-position: 0% 0%, 100% 100%, 0% 0%, 100% 100%;
  background-size: 0% 2px, 0% 2px, 2px 0%, 2px 0%;  /* 2px is border thickness */
  background-repeat: no-repeat;
  transition: all 1s;
}
h1:hover {
  background-size: 100% 2px, 100% 2px, 2px 100%, 2px 100%;  /* 2px is border thickness */
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/prefixfree/1.0.7/prefixfree.min.js"></script>
<h1>Hover me</h1>
<br>
<h1>How about me?<br>I have dynamic height!</h1>

<div id="wrap">
  <h1>Look at me, I am responsive!!!</h1>
</div>

By using pseudo-elements:

This can also be done using pseudo-elements by transitioning the height and width of them on hover. You were already on the right course here but the extra elements were not required.

h1 {
  position: relative;
  display: inline-block;
  padding: 4px;
}
h1:after,
h1:before {
  position: absolute;
  content: '';
  height: 0%;
  width: 0%;
  transition: width 1s, height 1s, border .01s 1s;  /* border has a delay because it should become invisible only after height and width become 0 */
}
h1:before {
  left: 0;
  top: 0;
  border-top: 2px solid transparent;
  border-left: 2px solid transparent;
}
h1:hover:before {
  border-top: 2px solid black;
  border-left: 2px solid black;
}
h1:after {
  bottom: 0;
  right: 0;
  border-right: 2px solid transparent;
  border-bottom: 2px solid transparent;
}
h1:hover:after {
  border-right: 2px solid black;
  border-bottom: 2px solid black;
}
h1:hover:before,
h1:hover:after {
  height: calc(100% - 2px);
  width: calc(100% - 2px);
  transition: width 1s, height 1s, border .01s;  /* border has a shorter duration because it immediately needs to change colors */
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/prefixfree/1.0.7/prefixfree.min.js"></script>
<h1>Hover me</h1>
<br>
<h1>How about me?<br>I have dynamic height!</h1>

<div id="wrap">
  <h1>Look at me, I am responsive!!!</h1>
</div>

Both these approaches work even when extra div elements are added around (as can be seen from the snippets).

Harry
  • 87,580
  • 25
  • 202
  • 214
  • 1
    Very impressive techniques. – Stickers Oct 21 '15 at 15:32
  • Yeah, this is super clever. I ended up making the animation by wrapping the `h1` in two `divs`, then growing four lines, 2 for each `div`, but this is much more awesome. – Quelklef Oct 21 '15 at 22:18
  • Though, this doesn't seem to work for dynamic-height text. – Quelklef Oct 21 '15 at 22:19
  • @Quelklef: Without the height and line-height settings on the H1 tag (that is after removing them), these should work for dynamic height also. I'll have a look at it tomorrow morning. – Harry Oct 21 '15 at 23:04
  • @Harry You're right. This is some good stuff, thank you :). I'm not gonna accept you as answer, though, just because this solves my problem, but doesn't answer my question. – Quelklef Oct 21 '15 at 23:14
  • @Quelklef: I thought you already knew the answer to that question based on BoltClock's answer in the linked thread. The content of the anim div (which is the h1) is inserted after the `:before` pseudo-element of the parent and hence it gets painted below. The only way to fix that is by adding `z-index` or changing the approach. – Harry Oct 22 '15 at 04:29
  • @Harry Thanks for answering the question. However, wouldn't I want my `h1` to be painted _before_ the `::before` and `::after`s, due to the way I designed my animation? (covering border, then shrinking to reveal it) – Quelklef Oct 22 '15 at 23:58
  • @Quelklef: Yes, absolutely but the `h1` already (as it is) gets painted before all pseudo-elements except the `#anim:before` and hence sending the `h1` alone back (using `z-index: -1`) or bringing the `#anim:before` forward (using `z-index: 1`) should be enough. – Harry Oct 23 '15 at 03:54