1441

I am trying to clean up the way my anchors work. I have a header that is fixed to the top of the page, so when you link to an anchor elsewhere in the page, the page jumps so the anchor is at the top of the page, leaving the content behind the fixed header (I hope that makes sense). I need a way to offset the anchor by the 25px from the height of the header. I would prefer HTML or CSS, but Javascript would be acceptable as well.

Skippy le Grand Gourou
  • 6,976
  • 4
  • 60
  • 76
Matt Dryden
  • 14,465
  • 3
  • 16
  • 13
  • 2
    The wrapper div shown in http://stackoverflow.com/questions/1431639/general-offset-for-all-anchors-in-html is fine I think, not too aggressive. – ron Apr 20 '12 at 14:23
  • 50
    There is a nice article on this subject: http://css-tricks.com/hash-tag-links-padding/ – J. Bruni Oct 06 '13 at 01:44
  • 2
    Related: http://stackoverflow.com/questions/4086107/html-positionfixed-page-header-and-in-page-anchors – 0fnt Apr 07 '15 at 09:14
  • 1
    @0fnt, This is actually a duplicate of that post. Why is it not closed? – Pacerier Jul 12 '15 at 16:31
  • @Pacerier I think that requires moderator action. I've flagged though – 0fnt Jul 12 '15 at 17:49
  • Re: deleting this post, from SO: This question cannot be deleted because other questions are linked as duplicates of this one. – Roy Truelove Feb 22 '16 at 21:14
  • 5
    The question which marks this one as duplicate does not accept javascript solutions. This question has script solutions. It should be reopened. – Huseyin Yagli Apr 18 '18 at 12:30
  • This solution is the way to go: https://stackoverflow.com/a/28824157/1066234 – Avatar May 16 '18 at 14:57
  • i'm seeking a solution which * works for anchors coming in from same page or other page, * and which adjusts page-down key-press so that content doesn't get clipped by header, * and which allows sib-div footer to scroll up with content-div. Haven't found a solution yet! https://stackoverflow.com/questions/51070758/css-to-make-vertical-scrolling-region-under-fixed-header – johny why Jun 29 '18 at 23:44
  • 23
    @J.Bruni There is a much newer CSS-tricks.com article about `scroll-padding-top` here: https://css-tricks.com/fixed-headers-on-page-links-and-overlapping-content-oh-my/ – Flimm Dec 03 '19 at 11:39
  • 29
    I wish these posts could be updated. I use `:target { scroll-margin-top: 24px; }` – valtism Jul 12 '21 at 01:51
  • Add the code in your css file will fix the problem, replace your header height and gap ``html { --headerHeight:79px; --gap:40px; scroll-padding-top: calc(var(--headerHeight) + var(--gap)); }`` – Aftab Ahmed Jan 10 '23 at 09:58
  • `.anchor-margin { margin-top: -110px; padding-top: 110px; }` – kodmanyagha Apr 26 '23 at 11:37
  • scroll-margin-top: 100px; is a more elegant solution. – Tony Tai Nguyen Jun 20 '23 at 22:11

28 Answers28

1469

You could just use CSS without any javascript.

Give your anchor a class:

<a class="anchor" id="top"></a>

You can then position the anchor an offset higher or lower than where it actually appears on the page, by making it a block element and relatively positioning it. -250px will position the anchor up 250px

a.anchor {
    display: block;
    position: relative;
    top: -250px;
    visibility: hidden;
}
johannchopin
  • 13,720
  • 10
  • 55
  • 101
Jan
  • 14,894
  • 1
  • 12
  • 6
449

I found this solution:

<a name="myanchor">
    <h1 style="padding-top: 40px; margin-top: -40px;">My anchor</h1>
</a>

This doesn't create any gap in the content and anchor links works really nice.

johannchopin
  • 13,720
  • 10
  • 55
  • 101
Alexander Savin
  • 6,202
  • 3
  • 29
  • 41
  • 8
    Worked great. I created a special CSS anchor class and just attached it to my anchors: . Thanks. –  Sep 12 '12 at 20:00
  • 3
    there is still jankiness with the indicator in the nav. so if you scroll down the page, the active nav item doesn't switch until you scroll past the anchor target. – Randy L Feb 13 '13 at 18:04
  • 45
    Element just above

    will not be clickable, because of the hidden padding/margin. I ended up using Ian Clack's jQuery solution, which works great.

    – Matt Aug 14 '13 at 19:46
  • 2
    How would you make it work with anchors that use element IDs, i.e.

    Text

    ...Link?
    – Alek Davis Nov 07 '13 at 07:06
  • 7
    I think I figured this out: h2[id], h3[id], h4[id], a[name] { padding-top: XXpx; padding-bottom: XXpx; } It applies to all h2, h3, h4 tags with IDs, as well as named anchors. – Alek Davis Nov 07 '13 at 08:41
  • I don't think having a header tag inside of a link is standards compliant, but it works! +1 – Nate Mar 02 '14 at 20:35
  • @Mathijs : I had the same problem. I simply replaced it with `My anchor` and it works great, without jQuery (invisible and 0px anchor target) – xav Jun 29 '14 at 07:07
  • FYI: merged from http://stackoverflow.com/questions/9047703/fixed-position-navbar-obscures-anchors – Shog9 Jul 24 '14 at 19:16
  • @Nate it's compliant. Try the W3C HTML Validator or see http://stackoverflow.com/q/7023512/1709587. – Mark Amery Mar 07 '15 at 19:34
  • Down vote as anchors with the name attribute are no longer supported from HTML 5, so should be linking to id of object – Stephen Keable Aug 16 '17 at 09:09
  • 2
    To prevent the issue with elements directly above not being clickable, you can set a negative z-index on the h* element. `h3[id] { z-index: -1; }` – DVK Oct 10 '17 at 12:21
  • @Matt have you tried to apply negative z-index to anchor element? – Andrea_86 Dec 17 '20 at 15:52
  • Problem not solved. Anchor is still under the fixed header. – Black May 10 '21 at 09:02
242

FWIW this worked for me:

[id]::before {
  content: '';
  display: block;
  height:      75px;
  margin-top: -75px;
  visibility: hidden;
}
Mark Nottingham
  • 5,546
  • 1
  • 25
  • 21
  • FYI: merged from http://stackoverflow.com/questions/9047703/fixed-position-navbar-obscures-anchors – Shog9 Jul 24 '14 at 19:17
  • 8
    I don't fully understand why this works, but +1 from me. None of the others were as easy to implement or worked as well as this. :-) – JackWilson Nov 20 '15 at 06:44
  • 2
    I wrapped this in a media query so it is only applied to medium and large screens where I have a fixed nav. – Ron Feb 15 '16 at 18:41
  • 4
    This may work, but it will overlap the content before the headline. – dude May 04 '16 at 13:23
  • You could add `position: relative` and `z-index: -1` to prevent it overlapping previous content. At least that worked for me – kylesimmonds Oct 03 '16 at 22:28
  • Do not work if you have background image for your element for exeample... (because the size of the element change with this hack) – fred727 Feb 27 '17 at 13:44
  • And it preserves collapse of margins, absolutely beautiful! – Paul Apr 11 '17 at 12:59
  • 2
    Can someone explain why this works? – Brady Holt Nov 29 '17 at 14:02
  • 1
    This is my favorite solution of all the answers here, but it's not a good approach when the links element is itself a link—the height value creates a big strike pad that extends past the area a user would expect to be linked. To keep the strike pad smaller, I did this: `content: '\00a0'; width: '1px'; margin-left: '-1px';padding-top: 75px;` -- this creates a valid element that pushes down the scroll bar without leaving any unexpected clickable areas. – jamesfacts Jun 27 '19 at 17:28
  • 3
    This is great! However, it doesn't work if the `[id]` element is a child of `display: flex` container: the height created by the `::before` element doesn't affect the height of the `[id]` element because flex ‍♀️ – Henry Blyth Jan 17 '20 at 12:16
  • Unfortunately increases the element size which causes its `border` and `outline` to be drawn around the `before` element. – fabb Mar 07 '20 at 07:05
  • Definitely the easiest & best solution in 2020! As @kylesimmonds pointed out, an additional tweak is needed to prevent the pseudo-element from overlapping any (clickable) content above: `[id]::before { position: relative; z-index: -1; }`. If the host element has relative/absolute/fixed position, those styles will need to be added there: `[id] { position: relative; z-index: -1; }`. – Rene Hamburger May 15 '20 at 14:55
  • 1
    I have a ton of elements with IDs that aren't anchors. This wreaks havoc with them. – clayRay Mar 22 '21 at 09:09
  • @jamesfacts, no you can just do it like this then `

    one

    `
    – Black May 10 '21 at 09:21
  • 1
    The solution looks good, but it destroys the whole layout of our website if I add the css. – Black May 10 '21 at 09:27
  • @BradyHolt this may probably be irrelevant now for you, but anyways: what it says is this, if you encounter an element with an id apply this in between this element and element before it. "this" would be position the pseudo element x pixel lower. – muzzletov Jul 01 '21 at 19:55
  • I have no idea how it works, but it works! The invisible block corrected the landing of the scroll. Thumbs up! – Lionel Yeo Oct 02 '21 at 20:36
  • addition `position: absolute` also works to keep the before element from reflowing the rest of your page. – Jon Cohen Dec 23 '21 at 00:11
  • wish I could party with you all but its not working for me :( (using with bootstrap 4.x.x) – Andy Jan 27 '22 at 13:28
  • This solution is magic! – dubmojo May 16 '23 at 02:36
226

I was looking for a solution to this as well. In my case, it was pretty easy.

I have a list menu with all the links:

<ul>
<li><a href="#one">one</a></li>
<li><a href="#two">two</a></li>
<li><a href="#three">three</a></li>
<li><a href="#four">four</a></li>
</ul>

And below that the headings where it should go to.

<h3>one</h3>
<p>text here</p>

<h3>two</h3>
<p>text here</p>

<h3>three</h3>
<p>text here</p>

<h3>four</h3>
<p>text here</p>

Now because I have a fixed menu at the top of my page I can't just make it go to my tag because that would be behind the menu.

Instead, I put a span tag inside my tag with the proper id.

<h3><span id="one"></span>one</h3>

Now use 2 lines of CSS to position them properly.

h3{ position:relative; }
h3 span{ position:absolute; top:-200px;}

Change the top value to match the height of your fixed header (or more). Now I assume this would work with other elements as well.

johannchopin
  • 13,720
  • 10
  • 55
  • 101
Hrvoje Miljak
  • 2,423
  • 1
  • 13
  • 6
  • 5
    `span` could be replaced with an `a` – David Cook Jul 23 '15 at 02:34
  • 2
    This works really nice and avoids some of the problems I hit with other techniques, such as when using an h2 tag that sets a padding-top. – Jonathan Feb 03 '16 at 08:20
  • I use this, as well as a JS event listening for click events on the anchors to smooth scroll if JS is available. – Daniel Dewhurst Apr 20 '17 at 11:02
  • In Wordpress, empty get stripped out. This solution, the editor does not strip out the empty spans. This is great! – Jarad Mar 28 '19 at 06:42
  • I added a class to the `` element so the children span do not inherit the same styling. – Parmandeep Chaddha Jun 06 '21 at 23:16
  • This is a superb solution, in my situation (I have React/Next) I gave the h5 tag the class of anchor and the inner span my id name (so Link was happy). Then just set the anchor class to position relative and the inner span to position absolute and top -3rem. Using Sass made it easier too, so thanks for this direction. – Nosnibor Jul 02 '21 at 10:33
188

As this is a concern of presentation, a pure CSS solution would be ideal. However, this question was posed in 2012, and although relative positioning / negative margin solutions have been suggested, these approaches seem rather hacky, create potential flow issues, and cannot respond dynamically to changes in the DOM / viewport.

With that in mind I believe that using JavaScript is still (February 2017) the best approach. Below is a vanilla-JS solution which will respond both to anchor clicks and resolve the page hash on load (See JSFiddle). Modify the .getFixedOffset() method if dynamic calculations are required. If you're using jQuery, here's a modified solution with better event delegation and smooth scrolling.

(function(document, history, location) {
  var HISTORY_SUPPORT = !!(history && history.pushState);

  var anchorScrolls = {
    ANCHOR_REGEX: /^#[^ ]+$/,
    OFFSET_HEIGHT_PX: 50,

    /**
     * Establish events, and fix initial scroll position if a hash is provided.
     */
    init: function() {
      this.scrollToCurrent();
      window.addEventListener('hashchange', this.scrollToCurrent.bind(this));
      document.body.addEventListener('click', this.delegateAnchors.bind(this));
    },

    /**
     * Return the offset amount to deduct from the normal scroll position.
     * Modify as appropriate to allow for dynamic calculations
     */
    getFixedOffset: function() {
      return this.OFFSET_HEIGHT_PX;
    },

    /**
     * If the provided href is an anchor which resolves to an element on the
     * page, scroll to it.
     * @param  {String} href
     * @return {Boolean} - Was the href an anchor.
     */
    scrollIfAnchor: function(href, pushToHistory) {
      var match, rect, anchorOffset;

      if(!this.ANCHOR_REGEX.test(href)) {
        return false;
      }

      match = document.getElementById(href.slice(1));

      if(match) {
        rect = match.getBoundingClientRect();
        anchorOffset = window.pageYOffset + rect.top - this.getFixedOffset();
        window.scrollTo(window.pageXOffset, anchorOffset);

        // Add the state to history as-per normal anchor links
        if(HISTORY_SUPPORT && pushToHistory) {
          history.pushState({}, document.title, location.pathname + href);
        }
      }

      return !!match;
    },

    /**
     * Attempt to scroll to the current location's hash.
     */
    scrollToCurrent: function() {
      this.scrollIfAnchor(window.location.hash);
    },

    /**
     * If the click event's target was an anchor, fix the scroll position.
     */
    delegateAnchors: function(e) {
      var elem = e.target;

      if(
        elem.nodeName === 'A' &&
        this.scrollIfAnchor(elem.getAttribute('href'), true)
      ) {
        e.preventDefault();
      }
    }
  };

  window.addEventListener(
    'DOMContentLoaded', anchorScrolls.init.bind(anchorScrolls)
  );
})(window.document, window.history, window.location);
Ian Clark
  • 9,237
  • 4
  • 32
  • 49
  • 4
    works great, though for jquery 1.7+, use $("a").on("click",... instead of $("a").live("click",... – JasonS Aug 03 '13 at 09:01
  • 5
    Nice comment, I'll update :) - BTW it should also be `$("body").on("click", "a"...` as it may need to work for anchors which are added into the document by scripts (hence why I was using `.live`) – Ian Clark Aug 03 '13 at 10:15
  • +1: in pure CSS soln, elements above aren't clickable (eg, when a side navbar moves on top for small screens) – tom10 Dec 10 '13 at 02:19
  • 3
    Also, though, it's worth noting that this will mess with other href/id pairs, as in collapse, carousel, etc... is there an easy way around this? – tom10 Dec 10 '13 at 03:35
  • 3
    @tom10 I suppose you would just have to make the selector more specific, either by blacklisting anchors using `a:not(.not-anchor)` (or something less confusingly named) and then make sure that your collapse/carousel libraries add those classes to their anchors. If you're using jQuery UI or Bootstrap I imagine they add some other classes which you could reference. – Ian Clark Dec 10 '13 at 13:54
  • @IanClark: Thanks for the tips. (In retrospect, I should have made that an independent SO question.) – tom10 Dec 10 '13 at 17:06
  • 2
    @tom10 I've added a few conditions to the test for `#` in the `href` in order to avoid breaking collapse and carousel: `if(href.indexOf("#") == 0 && (!$(this).data() || this.self == window))` – raddevon Feb 12 '14 at 15:53
  • @raddevon But any element could happen to have some jQuery data attached to it, I think that could prove quite a frustrating gotcha in the future. – Ian Clark Feb 12 '14 at 16:48
  • That's true. I can't think of a more accurate way to special case all of Bootstrap's JS components without doing each one individually. This solution also doesn't address JS components configured outside data attributes. – raddevon Feb 12 '14 at 17:12
  • Looks like bootstrap elements such as Carousel and Tabs have a `data-toggle` attribute, you could check for that? – Ian Clark Feb 12 '14 at 17:49
  • @raddevon. That if statement is blowing me away. I get the lead character == '#' portion, but don't get the rest of the AND stuff. In current form, my internal anchor links all fail your test, and I can't figure out why. The 'this' thing is confusing.. isn't 'this' = to the function scroll_if_anchor(). What your intent? thnx. – zipzit Mar 22 '14 at 17:41
  • @zipzit - I guess he added that before I changed the approach slightly. At that point the `this` would always have been the anchor element which fired the event. He's just suggesting that you add another condition to check that it's a "standard" anchor, and not something used be a JS function (like a carousel). – Ian Clark Mar 22 '14 at 18:21
  • @zipzit It's a pretty naïve approach that worked in my case. Ian is right that it was written when `this` would have referred to the target element. I'm not running the code on anything that has a data attribute unless it's the window object. – raddevon Mar 22 '14 at 21:21
  • I had a lot of difficulty getting this routine to work. Some weird timing issues. I ended up with `jQuery('a').click(function(){ setTimeout(function(){scroll_if_anchor(window.location.hash)},400); })` for the last 'intercept all anchor clicks' call. – zipzit Apr 01 '14 at 18:54
  • FYI: merged from http://stackoverflow.com/questions/9047703/fixed-position-navbar-obscures-anchors – Shog9 Jul 24 '14 at 19:16
  • @Shog9 OK, but maybe the word "navbar" made the other question useful. – Ian Clark Jul 24 '14 at 19:27
  • The other question still exists as a signpost - anyone finding it in search results for "navbar" will be redirected to this question. – Shog9 Jul 24 '14 at 19:28
  • This is incredible - thank you! However, I have a page with dynamic content that's populating and changing the position of everything below it. So if you get anchored to something below that dynamic content, you end up on the wrong spot. Any way to "re-calculate" where to land after all dynamic content has populated? Note that we're scrolling to the id="foo" anchor, not an a name="foo" kind of anchor. Thanks! – Berto Nov 14 '14 at 17:46
  • Sure. Why not just call .height() on the things you're trying to calculate? You'll need to do it in the ready block when the DOM is loaded, and if you style the height to a variable in the outer scope, it'll be available to the scroll_if_anchor method – Ian Clark Nov 14 '14 at 18:22
  • If `history` is indeed unset, as you check in `if (history && ...)`, then it will cause a ReferenceError for a variable not defined. Instead, check for a property on window -- `if (window.history && ...)` – Matthias Dec 02 '15 at 20:49
  • FYI, someone can sort of hack your function by writing a jQuery selector as the href value: `` – Matthias Dec 02 '15 at 20:57
  • @MatthiasDailey updated the solution to vanilla JS and fixed your problems above :) – Ian Clark Dec 14 '15 at 13:57
  • 1
    Excellent thanks! I had links to other pages with anchors as well, e.g. `/page/#anchor`. This regex covers those: `/^[\/a-z0-9-]*#[^\s]+$/` – tobiv Jan 06 '16 at 17:11
  • Though the solution adds entries to the history, it doesn't listen to hashchange and then no scrolling happens at all when trying to go back. – letmaik Jun 19 '16 at 11:34
  • @neo good spot, added - though smooth scrolling doesn't seem to apply with the jQuery variant despite the `preventDefault()` call. – Ian Clark Jun 19 '16 at 12:32
  • Strange problem which I don't have a chance of resolving myself; in Google Chrome v54. It doesn't work when you first load the page (or if you refresh) but it works if you change the # on the page, eg page.html#1 doesn't work first time (no error), page.html#2 works perfectly if called from the page, page.html#1 then works. Any ideas? – Chris Pink Dec 02 '16 at 11:02
  • @ChrisPink this works for me on Chrome 54: http://output.jsbin.com/cepigagowa#target-3 – Ian Clark Dec 02 '16 at 12:27
  • Trying to make this responsive. I use to adjust my header for the slider. Am trying to set OFFSET_HEIGHT_PX to the same calculation as the above code, but no matter how I write it it isn't working. Any help on setting this would be appreciated. – BitBug Feb 10 '17 at 21:15
  • I ended up with this replacement for the getFixedOffset function to have a responsive header height: getFixedOffset: function() { var responsiveHeight = jQuery('#header').position().top + jQuery('#header').height(); return responsiveHeight; } – BitBug Feb 11 '17 at 00:09
  • 5
    If you are clicking 2 times on the same anchor consecutively (from menu with anchor links), the second click is not working well. –  Jun 28 '17 at 08:22
  • 1
    Still working in 2018. Great JQuery solution, thanks. Would love to know if someone has a better way by now or not though, this was the best I could find – rickjerrity Jan 06 '18 at 05:38
  • Changing the scroll position in the `DOMContentLoaded` event works in Firefox, but not in Chrome. – Quolonel Questions Jan 24 '18 at 12:04
  • I fixed the issue where clicking on the same anchor link twice by swapping the line inside `delegateAnchors` that says `var elem = e.target;` to `var elem = e.currentTarget;`. I don't think this change breaks anything... – Connel Mar 10 '18 at 15:34
  • @IanClark IE Edge doesn't support scrollTop and in fact won't scroll at all. I've had to check for edge and use `$(match).scrollIntoView()` when using edge. `/Edge/.test(navigator.userAgent)` will test for Edge. Maybe you could update your excellent fiddle? – rory Nov 23 '18 at 16:20
  • 1
    @Ian Clark Very elegant & thorough solution. Worked perfectly, once I changed the 'name' attribute to 'id' attribute in the accompanying '' tag. Thanks. Upvoted... – Charles Robertson Feb 28 '19 at 15:32
  • 1
    this does not work on page load right? Only when I click an anchor on the page? – Adam Mar 14 '19 at 17:09
  • 1
    I figured out the cause of the issue where clicking a second time on the anchor does not work. The regex only works with links that begin with `#`, not links like `/some-url/#anchor`. It was always returning false in the scrollIfAnchor function, whereas the scrollToCurrent function was working. – jwinn May 08 '19 at 17:00
  • This is the best solution I've found after quite some searching, though it does have one issue: if you go to an anchor it works fine, but if you then go to the browsers URL bar and hit enter, you'll end up on the wrong spot, since the hashchange event is not triggered. – Jeroen De Dauw Oct 24 '19 at 06:42
  • To adapt for responsive web design, replace the this.OFFSET_HEIGHT_PX with a reference to the header element's offsetHeight (or similar). Even with a fixed header, this would be better since the height value is only specified once (in the CSS) - the JS doesn't need to be updated at all if the layout is changed. – greenbutterfly Mar 09 '20 at 02:15
  • Thanks for sharing! Didn't validate it too much, but I fixed the bug when you click an anchor link twice and added a dynamic offset for bootstrap navbars: https://pastebin.com/gdHdAhSb – scotty86 Aug 27 '20 at 09:06
  • href reference from top to bottom working, but bottom to top not working. – JVJplus Sep 05 '20 at 16:36
  • @MikeW that seems really odd... the bottom of the file uses a closure to assign `location` to `window.location`. In any event it _should_ be a singleton. Are you able to confirm? – Ian Clark Mar 14 '22 at 14:42
  • Of course you are right - my bad. I just realised for my use case I had removed it from the closure - I'll delete my comment. Nice code btw. – user115014 Mar 14 '22 at 15:25
94

Pure css solution inspired by Alexander Savin:

a[name] {
  padding-top: 40px;
  margin-top: -40px;
  display: inline-block; /* required for webkit browsers */
}

Optionally you may want to add the following if the target is still off the screen:

  vertical-align: top;
Allen
  • 305
  • 3
  • 13
Ziav
  • 1,587
  • 1
  • 11
  • 11
86

My solution combines the target and before selectors for our CMS. Other techniques don't account for text in the anchor. Adjust the height and the negative margin to the offset you need...

:target::before {
    content: '';
    display: block;
    height:      180px;
    margin-top: -180px;
}
johannchopin
  • 13,720
  • 10
  • 55
  • 101
Lezz
  • 1,242
  • 1
  • 9
  • 12
  • 3
    These two CSS solutions didn't work for me on the first sight, but finally I found out it **might not be compatible with other CSS properties**. Added a wrapper and that fixed the problem. Combination of :target:before with display:block works best for me. – John Dec 23 '14 at 13:49
  • 2
    [Solved] This solution works and i used this solution with `display: block;`. Without a special class, empty tag or javascript. – Amir Astaneh Aug 05 '17 at 21:51
  • 5
    If you decide to use this method, it is probably a good idea to add `pointer-events: none;` as well. Because otherwise links intersecting with the invisible overlay (above the anchor) will not be clickable. – Steven Aug 13 '19 at 12:58
  • 2
    This solution messes up collapsed margins by disconnecting them. Edit: I just put the id on the `

    ` and not the `
    ` and it works fine

    – Volper Feb 14 '20 at 15:18
  • Great otherwise but if the parent of the target element has `grid-area: named-area` the required offset cannot escape the grid area and the resulting actual offset may be too short. I haven't found a good workaround that special case so this should be considered as the correct answer in general. – Mikko Rantalainen May 25 '20 at 11:18
  • 13
    If supporting just modern browsers is okay, I'd recommend just `:target { scroll-margin-top: 180px; }` (or any other measurement or `calc()` you wish). – Mikko Rantalainen Sep 23 '21 at 11:08
49

This takes many elements from previous answers and combines into a tiny (194 bytes minified) anonymous jQuery function. Adjust fixedElementHeight for the height of your menu or blocking element.

    (function($, window) {
        var adjustAnchor = function() {

            var $anchor = $(':target'),
                    fixedElementHeight = 100;

            if ($anchor.length > 0) {

                $('html, body')
                    .stop()
                    .animate({
                        scrollTop: $anchor.offset().top - fixedElementHeight
                    }, 200);

            }

        };

        $(window).on('hashchange load', function() {
            adjustAnchor();
        });

    })(jQuery, window);

If you don't like the animation, replace

$('html, body')
     .stop()
     .animate({
         scrollTop: $anchor.offset().top - fixedElementHeight
     }, 200);

with:

window.scrollTo(0, $anchor.offset().top - fixedElementHeight);

Uglified version:

 !function(o,n){var t=function(){var n=o(":target"),t=100;n.length>0&&o("html, body").stop().animate({scrollTop:n.offset().top-t},200)};o(n).on("hashchange load",function(){t()})}(jQuery,window);
Lance
  • 861
  • 7
  • 12
  • 1
    This solution really helped me out, but it is somehow not working consistently in IE9-11. Sometimes it works, some other clicks it doesn't (scroll position stays at the anchor position). I am totally out of ideas what could cause the issue. – ericstumper May 20 '15 at 15:20
  • @Crono1985 Is your doc HTML 4 or 5? In 4, IDs had a stricter list of characters so they may be failing to register as valid targets. Next, are you using ID or name? In HTML5, ID is a valid anchor for all tags but name can only be used on link tags. – Lance Jun 14 '15 at 21:39
  • Very nice! Thanks! The only problem, it doesn't reliably work, if one follows the link with fragment/hash (I mean some-page#anchor). At least on Chromium 45.0.2454.101 and Firefox. I'm not sure it could fixed though. – MajesticRa Dec 10 '15 at 15:46
  • 2
    @MajesticRa One tricky issue is the order of operations in the on load or scroll events. If your page adjusts the layout after the page is loaded or scrolled (shrinking masthead for example), the calculation of the :target offset can be wrong. – Lance Dec 10 '15 at 17:54
  • 1
    That solved my issue. Maybe it worth putting this remark in the answer. – MajesticRa Dec 11 '15 at 16:53
  • I really don't understand why it doesn't work in IE11. Is pure HTML5 and name attribute on link tags. – David Prieto Dec 23 '15 at 20:11
  • Is the $(':target'), IE11 returns "length": 0, therefore the animate code doesn't work. – David Prieto Dec 28 '15 at 14:33
  • @DavidPrieto careful to check you have a valid anchor. It works with well formed IDs, not names. For instance, in both IE11 and Edge I get length=1 on this URL: http://stackoverflow.com/questions/10732690/offsetting-an-html-anchor-to-adjust-for-fixed-header/29853395?noredirect=1#comment-56627664 – Lance Feb 04 '16 at 21:12
  • href reference from top to bottom working, but bottom to top not working. – JVJplus Sep 05 '20 at 16:47
  • `window.scrollTo({ top: $anchor.offset().top - fixedElementHeight, behavior: 'smooth' });` this works for me – agriboz Jul 27 '21 at 10:15
43

For modern browsers, just add the CSS3 :target selector to the page. This will apply to all the anchors automatically.

:target {
    display: block;    
    position: relative;     
    top: -100px;
    visibility: hidden;
}
Alessandro Alinone
  • 4,934
  • 2
  • 18
  • 22
  • 15
    You could even do `:target:before` to create a hidden pseudo-element rather than hide the whole target. – Tavian Barnes Mar 24 '14 at 23:08
  • The :target selector is supposed to be supported since IE9, but the offset only works with FF and Chrome and Safari on my site, not with IE 11. – cdonner May 27 '14 at 14:00
  • I guess you could argue that IE is not a modern browser. Either way, by using a CSS class instead of the :target selector it works fine in IE as well. – cdonner May 27 '14 at 14:19
  • 1
    When you do things the HTML5 way (and I think 4 too) where you target an id attribute within a node such as section or header, this way caused the element to display overlapping elements above it. – Alice Wonder Nov 21 '14 at 00:40
  • 5
    Hiding `:target` seems like a bad idea in most contexts. You're granting anybody who sends someone a link to your page the power to pick any element with an ID and delete it from the page that the viewer will see. On the tame side, tech-savvy pranksters may well notice this and decide to have some fun with it, much as they once had fun with the ability to make links to some news sites show up with silly titles on Facebook by manipulating their URLs. More severely, it may be a security hole if your page has something like a "don't share this secret information with anyone" message with an ID. – Mark Amery Mar 07 '15 at 21:22
  • 1
    adding this code to the style sheet does nothing for me using Chrome 60.0.3112.78 in the website I'm currently working on - though that may well be due to interaction effects... Could you post a pen? – Julix Aug 01 '17 at 20:07
  • I couldn't use this cause it'd hide the place I was going, but I just placed a `````` where I was going and now everything is fine (I know it's not that orthodox but whatever). – Rusca8 May 19 '20 at 10:34
41

You can do it without js and without altering html. It´s css-only.

a[id]::before {
    content: '';
    display: block;
    height: 50px;
    margin: -30px 0 0;
}

That will append a pseudo-element before every a-tag with an id. Adjust values to match the height of your header.

Gust van de Wal
  • 5,211
  • 1
  • 24
  • 48
Zsolt Szilagyi
  • 4,741
  • 4
  • 28
  • 44
34

I had been facing a similar issue, unfortunately after implementing all the solutions above, I came to the following conclusion.

  1. My inner elements had a fragile CSS structure and implementing a position relative / absolute play, was completely breaking the page design.
  2. CSS is not my strong suit.

I wrote this simple scrolling js, that accounts for the offset caused due to the header and relocated the div about 125 pixels below. Please use it as you see fit.

The HTML

<div id="#anchor"></div> <!-- #anchor here is the anchor tag which is on your URL -->

The JavaScript

 $(function() {
  $('a[href*=#]:not([href=#])').click(function() {
    if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') 
&& location.hostname == this.hostname) {

      var target = $(this.hash);
      target = target.length ? target : $('[name=' + this.hash.slice(1) +']');
      if (target.length) {
        $('html,body').animate({
          scrollTop: target.offset().top - 125 //offsets for fixed header
        }, 1000);
        return false;
      }
    }
  });
  //Executed on page load with URL containing an anchor tag.
  if($(location.href.split("#")[1])) {
      var target = $('#'+location.href.split("#")[1]);
      if (target.length) {
        $('html,body').animate({
          scrollTop: target.offset().top - 125 //offset height of header here too.
        }, 1000);
        return false;
      }
    }
});

See a live implementation here.

Shouvik
  • 11,350
  • 16
  • 58
  • 89
  • ok i got the anchor to put the anchor name into the url, but i cant seem to get the second part to work. When i open my page with an anchor in the url, it moves to where the anchor is but it won't offset it. Do i need something in addition to jquery to make that work? – Robbiegod Sep 10 '14 at 19:06
  • @Robbiegod to offset just tweak the pixel count I have mentioned, `scrollTop: target.offset().top - 125` the 125 here is something that worked for me. Refine it as per your needs. It would be great if you could post an example with your problem for more clarifications. – Shouvik Sep 11 '14 at 07:06
  • @Shouvik I did change 125 to 165 to match my site already, but it still doesnt offset. I have the js code in a file called site.js at that file loads in the footer, could that be the problem? Does this need to load in the head section? I also copied your code straight into my site.js file. The total change i made was changing the $ to jQuery. – Robbiegod Sep 11 '14 at 14:46
  • hmmm, it shouldn't be an issue. The idea is that we want to call this function after the window is loaded as opposed to the page being ready. This is done to take the images loaded into perspective to scroll the page. You should probably check your jquery reference, i.e if the jquery file is loaded. Use the `$` instead of `JQuery`. If you are getting an error it's probably because the jquery is not getting loaded. – Shouvik Sep 11 '14 at 14:53
  • jQuery is loading for sure (lightboxes, flexslider etc all work). I load jQuery in the footer too. Then i load my site.js file. using jquery 1.11 and jquery ui as well. i changed all of the jQuery back to $ (though i don't think this is an issue either way because $ is just an alias for jQuery) - it seemed to not make any difference. I don't see any errors, but also when i open a new browser and paste the url with the anchor it doesnt offset the page. I'll keep trying different stuff – Robbiegod Sep 11 '14 at 15:19
  • @Shouvik i did some digging. Found out that var target = jQuery('#'+location.href.split("#")[1]); returns an array, but when i output the console.log(target.length); it returns a zero. For this to work, what value does var target need to be? – Robbiegod Sep 11 '14 at 16:43
  • @Robbiegod your location.href is the URL which should have the anchor tag. We split it by # to get the tag id. The `('#'+location.href.split("#")[1])` basically get's the element, and should return 1. Do you have the anchor tag as a div id in your page? If not then this will not work. See my live implementation link and debug the code to learn more. I will not be of much help unless you can give me a link to your site. HTH :) – Shouvik Sep 12 '14 at 08:19
  • This will not serve you well if your website uses dynamic sizing units such as `em` or `rem`s for the header size since the offsets will differ maybe even per user. Also if you'd change the header to be less high at a smaller viewport this would require manual tweaking or taking into account all sizes you want to use. Not to mention that there will be a moment where the header will jump due to JS loading ever so slightly later. All that and the fact that it uses JS to perform a CSS task. Kudos for solving it but I would refrain from using it for those reasons. – SidOfc Sep 18 '19 at 17:32
13

As @moeffju suggests, this can be achieved with CSS. The issue I ran into (which I'm surprised I haven't seen discussed) is the trick of overlapping previous elements with padding or a transparent border prevents hover and click actions at the bottom of those sections because the following one comes higher in the z-order.

The best fix I found was to place section content in a div that is at z-index: 1:

// Apply to elements that serve as anchors
.offset-anchor {
  border-top: 75px solid transparent;
  margin: -75px 0 0;
  -webkit-background-clip: padding-box;
  -moz-background-clip: padding;
  background-clip: padding-box;
}

// Because offset-anchor causes sections to overlap the bottom of previous ones,
// we need to put content higher so links aren't blocked by the transparent border.
.container {
  position: relative;
  z-index: 1;
}
Kris Braun
  • 1,330
  • 13
  • 21
13

For the same issue, I used an easy solution : put a padding-top of 40px on each anchor.

odupont
  • 1,946
  • 13
  • 16
  • 2
    Thanks, this was basically what I ended up doing, but I was wondering whether there's a solution for situations where adding extra padding might be awkward. – Ben Feb 29 '12 at 22:57
  • FYI: merged from http://stackoverflow.com/questions/9047703/fixed-position-navbar-obscures-anchors – Shog9 Jul 24 '14 at 19:17
12

Solutions with changing position property are not always possible (it can destroy layout) therefore I suggest this:

HTML:

<a id="top">Anchor</a>

CSS:

#top {
    margin-top: -250px;
    padding-top: 250px;
}

Use this:

<a id="top">&nbsp;</a>

to minimize overlapping, and set font-size to 1px. Empty anchor will not work in some browsers.

user2475125
  • 129
  • 1
  • 3
11

Borrowing some of the code from an answer given at this link (no author is specified), you can include a nice smooth-scroll effect to the anchor, while making it stop at -60px above the anchor, fitting nicely underneath the fixed bootstrap navigation bar (requires jQuery):

$(".dropdown-menu a[href^='#']").on('click', function(e) {
   // prevent default anchor click behavior
   e.preventDefault();

   // animate
   $('html, body').animate({
       scrollTop: $(this.hash).offset().top - 60
     }, 300, function(){
     });
});
Community
  • 1
  • 1
  • FYI: merged from http://stackoverflow.com/questions/9047703/fixed-position-navbar-obscures-anchors – Shog9 Jul 24 '14 at 19:17
  • A full solution should also include the scenario in which a user loads a new page with the anchor already in the address bar. I tried to adapt this code to fire upon the $(document).ready event but it is still scrolling to the wrong place in the document. Any ideas? – Adam Friedman Aug 21 '14 at 22:46
  • 1
    thanks, that is the solution for twitter bootstrap users – Sebastian Viereck Sep 17 '15 at 09:49
  • @AdamFriedman did you even found a solution to that specifik scenario. I use a smoothanchor library which I can append an offset but that also does not work on pageloads with urls. url#target – Todilo Feb 25 '17 at 12:25
4

The above methods don't work very well if your anchor is a table element or within a table (row or cell).

I had to use javascript and bind to the window hashchange event to work around this (demo):

function moveUnderNav() {
    var $el, h = window.location.hash;
    if (h) {
        $el = $(h);
        if ($el.length && $el.closest('table').length) {
            $('body').scrollTop( $el.closest('table, tr').position().top - 26 );
        }
    }
}

$(window)
    .load(function () {
        moveUnderNav();
    })
    .on('hashchange', function () {
        moveUnderNav();
    });

* Note: The hashchange event is not available in all browsers.

Mottie
  • 84,355
  • 30
  • 126
  • 241
4

Instead of having a fixed-position navbar which is underlapped by the rest of the content of the page (with the whole page body being scrollable), consider instead having a non-scrollable body with a static navbar and then having the page content in an absolutely-positioned scrollable div below.

That is, have HTML like this...

<div class="static-navbar">NAVBAR</div>
<div class="scrollable-content">
  <p>Bla bla bla</p>
  <p>Yadda yadda yadda</p>
  <p>Mary had a little lamb</p>
  <h2 id="stuff-i-want-to-link-to">Stuff</h2>
  <p>More nonsense</p>
</div>

... and CSS like this:

.static-navbar {
  height: 100px;
}
.scrollable-content {
  position: absolute;
  top: 100px;
  bottom: 0;
  overflow-y: scroll;
  width: 100%;
}

There is one significant downside to this approach, however, which is that while an element from the page header is focused, the user will not be able to scroll the page using the keyboard (e.g. via the up and down arrows or the Page Up and Page Down keys).

Here's a JSFiddle demonstrating this in action.

Mark Amery
  • 143,130
  • 81
  • 406
  • 459
  • 3
    Non-hacky, but: (1) entirely useless outside this example, (2) cumbersome to adopt and maintain, (3) anti-semantic and-or css-abusive, (4) unintuitive. I'll take the _clean_ `a:before {...}` solution over this any day! – Ярослав Рахматуллин Jan 25 '17 at 12:40
  • Best answer for me. I am using it now. All the answers here are hacky. Adding a "before" pseudo element to everything is not acceptable for me and can potentially interfere with many other CSS elements already using the "before" pseudo element. This is the right and clean way to proceed. – David Apr 18 '18 at 12:55
  • 1
    This is ABSOLUTELY the best solution. It requires no hacks or pseudo-classes. I also find it 100% semantic. Essentially there is a part of the page that you want to have scroll, and you set that explicitly. Also, it completely solves the problem of how to make headings disappear behind navs with no background. – abalter May 29 '18 at 00:45
3

You can achieve this without an ID using the a[name]:not([href]) css selector. This simply looks for links with a name and no href e.g. <a name="anc1"></a>

An example rule might be:

a[name]:not([href]){
    display: block;    
    position: relative;     
    top: -100px;
    visibility: hidden;
}
Chris GW Green
  • 1,145
  • 9
  • 17
2

This was inspired by the answer by Shouvik - same concept as his, only the size of the fixed header isn't hard coded. As long as your fixed header is in the first header node, this should "just work"

/*jslint browser: true, plusplus: true, regexp: true */

function anchorScroll(fragment) {
    "use strict";
    var amount, ttarget;
    amount = $('header').height();
    ttarget = $('#' + fragment);
    $('html,body').animate({ scrollTop: ttarget.offset().top - amount }, 250);
    return false;
}

function outsideToHash() {
    "use strict";
    var fragment;
    if (window.location.hash) {
        fragment = window.location.hash.substring(1);
        anchorScroll(fragment);
    }
}

function insideToHash(nnode) {
    "use strict";
    var fragment;
    fragment = $(nnode).attr('href').substring(1);
    anchorScroll(fragment);
}

$(document).ready(function () {
    "use strict";
    $("a[href^='#']").bind('click',  function () {insideToHash(this); });
    outsideToHash();
});
Alice Wonder
  • 896
  • 2
  • 9
  • 17
1

I'm facing this problem in a TYPO3 website, where all "Content Elements" are wrapped with something like:

<div id="c1234" class="contentElement">...</div>

and i changed the rendering so it renders like this:

<div id="c1234" class="anchor"></div>
<div class="contentElement">...</div>

And this CSS:

.anchor{
    position: relative;
    top: -50px;
}

The fixed topbar being 40px high, now the anchors work again and start 10px under the topbar.

Only drawback of this technique is you can no longer use :target.

lipsumar
  • 944
  • 8
  • 22
0

Adding to Ziav's answer (with thanks to Alexander Savin), I need to be using the old-school <a name="...">...</a> as we're using <div id="...">...</div> for another purpose in our code. I had some display issues using display: inline-block -- the first line of every <p> element was turning out to be slightly right-indented (on both Webkit and Firefox browsers). I ended up trying other display values and display: table-caption works perfectly for me.

.anchor {
  padding-top: 60px;
  margin-top: -60px;
  display: table-caption;
}
  • FYI: merged from http://stackoverflow.com/questions/9047703/fixed-position-navbar-obscures-anchors – Shog9 Jul 24 '14 at 19:17
0

I added 40px-height .vspace element holding the anchor before each of my h1 elements.

<div class="vspace" id="gherkin"></div>
<div class="page-header">
  <h1>Gherkin</h1>
</div>

In the CSS:

.vspace { height: 40px;}

It's working great and the space is not chocking.

Quentin
  • 2,529
  • 2
  • 26
  • 32
  • FYI: merged from http://stackoverflow.com/questions/9047703/fixed-position-navbar-obscures-anchors – Shog9 Jul 24 '14 at 19:17
0

how about hidden span tags with linkable IDs that provide the height of the navbar:

#head1 {
  padding-top: 60px;
  height: 0px;
  visibility: hidden;
}


<span class="head1">somecontent</span>
<h5 id="headline1">This Headline is not obscured</h5>

heres the fiddle: http://jsfiddle.net/N6f2f/7

Pete
  • 17
  • 5
  • FYI: merged from http://stackoverflow.com/questions/9047703/fixed-position-navbar-obscures-anchors – Shog9 Jul 24 '14 at 19:17
0

You can also add an anchor with follow attr:

(text-indent:-99999px;)
visibility: hidden;
position:absolute;
top:-80px;    

and give the parent container a position relative.

Works perfect for me.

  • FYI: merged from http://stackoverflow.com/questions/9047703/fixed-position-navbar-obscures-anchors – Shog9 Jul 24 '14 at 19:17
0

A further twist to the excellent answer from @Jan is to incorporate this into the #uberbar fixed header, which uses jQuery (or MooTools). (http://davidwalsh.name/persistent-header-opacity)

I've tweaked the code so the the top of the content is always below not under the fixed header and also added the anchors from @Jan again making sure that the anchors are always positioned below the fixed header.

The CSS:

#uberbar { 
    border-bottom:1px solid #0000cc; 
    position:fixed; 
    top:0; 
    left:0; 
    z-index:2000; 
    width:100%;
}

a.anchor {
    display: block;
    position: relative;
    visibility: hidden;
}

The jQuery (including tweaks to both the #uberbar and the anchor approaches:

<script type="text/javascript">
$(document).ready(function() {
    (function() {
        //settings
        var fadeSpeed = 200, fadeTo = 0.85, topDistance = 30;
        var topbarME = function() { $('#uberbar').fadeTo(fadeSpeed,1); }, topbarML = function() { $('#uberbar').fadeTo(fadeSpeed,fadeTo); };
        var inside = false;
        //do
        $(window).scroll(function() {
            position = $(window).scrollTop();
            if(position > topDistance && !inside) {
                //add events
                topbarML();
                $('#uberbar').bind('mouseenter',topbarME);
                $('#uberbar').bind('mouseleave',topbarML);
                inside = true;
            }
            else if (position < topDistance){
                topbarME();
                $('#uberbar').unbind('mouseenter',topbarME);
                $('#uberbar').unbind('mouseleave',topbarML);
                inside = false;
            }
        });
        $('#content').css({'margin-top': $('#uberbar').outerHeight(true)});
        $('a.anchor').css({'top': - $('#uberbar').outerHeight(true)});
    })();
});
</script>

And finally the HTML:

<div id="uberbar">
    <!--CONTENT OF FIXED HEADER-->
</div>
....
<div id="content">
    <!--MAIN CONTENT-->
    ....
    <a class="anchor" id="anchor1"></a>
    ....
    <a class="anchor" id="anchor2"></a>
    ....
</div>

Maybe this is useful to somebody who likes the #uberbar fading dixed header!

Dzseti
  • 447
  • 1
  • 7
  • 18
-1

@AlexanderSavin's solution works great in WebKit browsers for me.

I additionally had to use :target pseudo-class which applies style to the selected anchor to adjust padding in FF, Opera & IE9:

a:target {
  padding-top: 40px
}

Note that this style is not for Chrome / Safari so you'll probably have to use css-hacks, conditional comments etc.

Also I'd like to notice that Alexander's solution works due to the fact that targeted element is inline. If you don't want link you could simply change display property:

<div id="myanchor" style="display: inline">
   <h1 style="padding-top: 40px; margin-top: -40px;">My anchor</h1>
</div>
jibiel
  • 8,175
  • 7
  • 51
  • 74
  • my nav items link to h2 elements, which are all display: block. i am using chrome, and i did not need to set the h2's to inline or inline-block. – Randy L Feb 13 '13 at 17:50
  • @the0ther I was referring to wrapper element not headers. Also experiencing difficulties to imagine your markup based on your words. – jibiel Feb 13 '13 at 19:31
  • FYI: merged from http://stackoverflow.com/questions/9047703/fixed-position-navbar-obscures-anchors – Shog9 Jul 24 '14 at 19:17
-1

Here's the solution that we use on our site. Adjust the headerHeight variable to whatever your header height is. Add the js-scroll class to the anchor that should scroll on click.

// SCROLL ON CLICK
// ---------------------------------------------------------------------
$('.js-scroll').click(function(){
    var headerHeight = 60;

    $('html, body').animate({
        scrollTop: $( $.attr(this, 'href') ).offset().top - headerHeight;
    }, 500);
    return false;
});
iorgv
  • 787
  • 2
  • 11
  • 26
stacigh
  • 676
  • 1
  • 7
  • 13
-2

I ran into this same issue and ended up handling the click events manually, like:

$('#mynav a').click(() ->
  $('html, body').animate({
      scrollTop: $($(this).attr('href')).offset().top - 40
  }, 200
  return false
)

Scroll animation optional, of course.

jean
  • 1,027
  • 1
  • 8
  • 15
  • FYI: merged from http://stackoverflow.com/questions/9047703/fixed-position-navbar-obscures-anchors – Shog9 Jul 24 '14 at 19:18
  • 3
    This javascript isn't even valid, I understand the message you're trying to convey. But it should AT LEAST have valid braces and syntax to be a legitimate answer. – gzimmers Apr 03 '19 at 21:52