Series - Offsetting Targeted Elements Part 2
You can use some well-documented CSS methods to offset an element so that it appears lower down in the browser window when it is targeted. However, these methods can clash with other css, breaking the offset or affecting the element style. In this article, I'll explain what causes these issues and then look for some more robust css methods.
This article extends on part 1, where I defined what a targeted element is, and outlined a common method used to offset the element. I also addressed browser support issues and a css-only way of applying the offset progressively. I closed Part 1 with the following method:
<a href="#the_content">Skip to the content</a> <div id="the_content" class="scroll_target">...</div>
.scroll_target:nth-of-type(1n+0):before { content: " "; display: block; margin-top: -8em; height: 8em; visibility: hidden; }
This solved the inconsistency in browser support for position:fixed (assumed for the header) and the pseudo-element :before. It uses the CSS3 pseudo-class :nth-of-type(1n+0) to apply the offset style. This pseudo-class comes later in browser support than position:fixed and :before, and when support for the pseudo-class drops, the style block won't be applied.
The issues with this method are:
Using top padding or top border on the target element will break the offset.
A background applied to the target element will bleed at the top edge, in other words the background will wrap around the :before pseudo-element (as you can see in the previous example).
If the target element has a relative position, elements above its top edge will be covered by the offset, preventing links and interactions.
First, lets take a close look the top border & padding issue.
Top Padding & Border Break the Offset
When there is a top border or top padding applied to the target element, the CSS offset method looses its effect. To illustrate this, take a look at the example below. The first and third chapter elements don't have a top border or top padding, so their offset works perfectly. The second chapter element has a top border of 1em, which breaks our offset. You can use the links at the top of the page to see where the browser places the page in the window when an element is targeted:
See the Pen Block-style link target offset with border brake by Niall Campbell (@niall-campbell84) on CodePen.
But why does this happen? Well, it is all down to how the CSS offset method works. It relies on the margin of the child element (:before) combining with the top margin of the parent element (the target element). This behaviour is known as margin collapsing. Lets look at how margin collapsing works:
Margin Collapsing
From MDN:
Top and bottom margins of blocks are sometimes combined (collapsed) into a single margin whose size is the largest of the margins combined into it
Margin collapsing occurs when two margins directly contact each other. For instance, the bottom margin of a block element and the top margin of the next block element will normally collapse. When this happens there are three possibilities:
When there are two positive margins (8em & 10em), the result is a single margin of the largest of the two (10em).
When there are two negative margins(-8em & -10em), the result is a single margin of the smallest of the two (-10em).
When there is a positive and a negative margin (-8em & 10em), the result is the sum of the positive margin and the negative margin (10em + -8em = 2em)
The CSS offset method I'm using can have three margins that collapse: the top margin of the target element, the bottom margin of the previous element, and the :before pseudo-element of the target element. So why did all three of those margins collapse? Again from MDN:
If there is no border, padding, inline content, or clearance to separate the margin-top of a block from the margin-top of its first child block, or no border, padding, inline content, height, min-height, or max-height to separate the margin-bottom of a block from the margin-bottom of its last child, then those margins collapse. The collapsed margin ends up outside the parent.
This means that more than two margins can collapse so long as there is no separation between each margin. When there is a top border or top padding to an element, the :before pseudo-elements top margin won't collapse with its parent element's top margin, breaking the offset.
Alternative: Margin Collapsing without :before
One way to get around this is to have a separate offset element before the target element that handles the negative margin, like so:
<a href="#the_content">Skip to the content</a> <div class="scroll_target_collapse">...</div> <div id="the_content" class="scroll_target">...</div>
.scroll_target_collapse:nth-of-type(1n+0) { margin-top: -8em; } .scroll_target:nth-of-type(1n+0):before { content: " "; display: block; height: 8em; visibility: hidden; }
So when there is a border or padding applied to the top of the target element, the offset won’t be broken:
See the Pen Block-style link target offset with separate collapse element by Niall Campbell (@niall-campbell84) on CodePen.
Before I move on I should mention something else about the nature of collapsing margins.
Lets say there is a scenario where three margins collapse and two were negative. For example, the bottom margin of an element was -4em, the bottom margin of the next (empty) element was -8em, and the top margin of the following element was 10em. What happens? Well all of the negative margins are collapsed together and all of the positive margins are collapsed together, then they are added to give the final margin. This would look something like:
(min(-4em,-8em) + max(10em)) = -8em + 10em = 2em
This means that when you use negative margins to move content up the page, its effects will be limited by any negative margins it collapses with. If the -8em negative margin was to compensate for an offset, the result of its effect would only move the content up 4em, rather than 8em, leaving a 4em gap between the first and the last element. This is an issue I can’t resolve, so you will need to add extra css rules if you have this issue. For example:
/* if the previous element has a margin bottom of -4em like: */ .previous_element { margin-bottom: -4em; } /* we will have to add -4em to our target collapse */ .previous_element + .scroll_target_collapse { margin-top: -12em; }
Use the plus (+) selector (called an adjacent sibling selector) to target the next sibling.
Background bleeds into previous element
When a background color or image is declared on the target element, the background appears to bleed into the previous element. I say appears to bleed because it is the target element itself that is over the previous element. This is how the CSS offset method works. It moves the target element up the page 8em using a collapsing margin, then moves the internal content of the element down the page 8em using the height of the :before element:
<!-- /*toggle a fixed header into position when we have a target*/ .target_fixed_header { display: none; } :target + .target_fixed_header { display: block; position: fixed; top: 0; left: 0; right: 0; background: #004A5D; opacity: 0.95; color: #fff; padding: 1em; height: 8em; box-sizing: border-box; font-weight: 400; z-index: 3; } #third_content_link { margin-bottom: 0em; display: block; padding: 1em; position: relative; z-index: 2; } #the_third_box { display: block; background-color: rgba(177, 58, 9, 0.31); padding: 1em 1em 1em; border: 3px dashed #000; } #the_third_box a { display: block; text-align: right; } .scroll_target_collapse { margin-top: -8em; display: block; } #the_third_box.scroll_target:before { content: "8em added between containers top padding and containers content"; display: block; height: 8em; text-align: center; padding: 3em 0; box-sizing: border-box; border: 3px dashed #fff; opacity: 0.5; background-color: #6ff; } --> Skip to the content Back to content linkSome content for the third box. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla ut massa sollicitudin, aliquet dolor eu, elementum arcu. Duis consequat finibus ultricies. In ultrices augue ligula. Phasellus rhoncus ultricies nibh ac congue. Aliquam erat volutpat. Some fixed header. Press the back button in the browser or click this link to hide me.
There are a couple of good methods I know of to get around this issue. The first is from Nicolas Gallagher & involves a transparent top border. The second involves a table caption element. I'll leave the second method for the final article as it requires quite a lengthy explanation. So back to the first method:
Alternative: Transparent top border & background-clip
This method comes from Nicolas Gallagher's site, you can view the original method and a thorough explanation here: http://nicolasgallagher.com/jump-links-and-viewport-positioning/
This solution uses a transparent top border to push the content down instead of a :before pseudo element. There is one caveat. The background doesn't stop at the inner edge of the border (the padding-box), it stops at the outer edge of the border (the border-box). If the top border is transparent, the background will still show through. There is, however, a background-clip property that can change where the background stops:
.scroll_target_collapse:nth-of-type(1n+0) { margin-top: -8em; } .scroll_target:nth-of-type(1n+0) { border-top: 8em solid transparent; -webkit-background-clip: padding-box; -moz-background-clip: padding; background-clip: padding-box; }
Support for background-clip is: IE9, FF3.6, Chrome 4 (partial until 15), Safari 3.1 (partial until 7), Opera 10.1, Android 2.1 which is almost all encapsulated by our nth-of-type(1n+0) pseudo-class (the exception being FF 3.5).
See the Pen Block-style link target offset with background-clip by Niall Campbell (@niall-campbell84) on CodePen.
You might notice some unusual behaviour with the left and right borders at their top ends. These borders are chamfered with the top transparent border, creating an angled spike. This isn’t ideal. To keep the borders, there needs to be a workaround. Something like this:
Make sure the bottom, left & right borders of the container are transparent:
.scroll_target:nth-of-type(1n+0) { position: relative; border-color: transparent !important; border-style: solid; border-top-width: 8em; -webkit-background-clip: padding-box; -moz-background-clip: padding; background-clip: padding-box; }
Then position its :before absolutely. Make sure it has a z-index of -1 and that its positioned the border width beyond the container. Then apply the border.
.scroll_target:nth-of-type(1n+0):before { content: “”; position:absolute; top: -2px; left: -2px; right: -2px; bottom: -2px; border: 2px solid #ccc; z-index: -1; }
The z-index of -1 is relative to the z stacking of the content of our container and not the container itself. It makes sure the pseudo-element doesn’t appear above its content, preventing user interaction with the content. You can also use pointer-events for this purpose if you want to keep the :before element above the container (see https://css-tricks.com/almanac/properties/p/pointer-events/) but its support is more limited.
There is an issue with this. position: relative means that if there is any overlap with previous elements there is a chance that some of the previous elements covered will loose their interactivity. In this case, it might be better to switch to a box-shadow property instead of a border property, like so:
.scroll_target:nth-of-type(1n+0) { border-top: 8em solid transparent; box-shadow: 0 0 0 2px #ccc inset; -webkit-background-clip: padding-box; -moz-background-clip: padding; background-clip: padding-box; }
Wow! So thats a lot of work just for a border, and if you’re familiar with object oriented css I may have just made you cry a little, but it looks ok, and it might be an acceptable style to use as a fallback.
In the next article I'll introduce a different method to offset targeted elements with CSS: offsetting elements with table-caption. The table-caption method will attempt to keep all existing styles in place. Hope you enjoyed reading! until next time.













