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