Reputation: 1775
I have this code for my website:
function clickMe() {
var element = document.getElementById('about');
element.scrollIntoView({
block: 'start',
behavior: 'smooth',
});
}
This works pretty nice but I have a fixed header so when the code scrolls to the element the header is in the way.
Is there a way to have an offset and make it scroll smoothly?
Upvotes: 164
Views: 177534
Reputation: 11
can keep using scrollIntoView, and add css style scroll-margin in your element . such as
.tableClass {
scroll-margin-top: 52px;
}
tableDom.scrollIntoView({ behavior: 'smooth', inline: 'start' });
Upvotes: 1
Reputation: 1313
to prevent any element from intersecting with fixed top. there are actually many ways to do that. recently I use scroll-padding-top in CSS file.
* {
scroll-behavior: smooth;
scroll-padding-top: 100px; /* this pixel should match fixed header height */
}
what do you mean scroll smoothly?
just add scroll-behavior: smooth;
in CSS.
if what you want is to open a new page and then scroll smoothly, then that's a different approach. you can check my answer for this here
if what you looking for is to check if the element is in the viewport or not, then that's another story. I'm not sure which one you are looking for. if it's this one, please confirm and I will spend more time summarizing the answer for you. I had this issue and I finally solved it.
Upvotes: 8
Reputation: 17354
Søren D. Ptæus's answser is almost right, but it only works when the user is on top. This is because getBoundingClientRect will always get us the relative height and using window.scrollTo with a relative height doesn't work.
ekfuhrmann improved the answer by getting the total height from the body element and calculating the real height. However, I think it can be easier than that, we can simply use the relative position and use window.scrollBy.
Note: Key difference is window.scrollBy
const HEADER_HEIGHT = 45;
function scrollToTargetAdjusted(){
const element = document.getElementById('targetElement');
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition - HEADER_HEIGHT;
window.scrollBy({
top: offsetPosition,
behavior: "smooth"
});
}
Upvotes: 15
Reputation: 135
Come across this question and seems scrollBy provides the best flexibility. This is just a minimalistic version based on @yangli-io answer to save you some time and cleaner code.
function scrollIntoViewAdjusted(elem, offset=0){
window.scrollBy({
top: elem.getBoundingClientRect().top - offset,
behavior: "smooth"
});
}
Upvotes: 0
Reputation: 1905
Simple but elegant solution if the element has a small height (shorter than the viewport):
element.scrollIntoView({ behavior: 'auto' /*or smooth*/, block: 'center' });
The block: center
will scroll the element so the center of the element is at the vertical center of the viewport, so the top header will not cover it.
EDIT 8.5.22: behavior: instant
was used in the past, but removed from browsers.
Upvotes: 70
Reputation: 1574
elementRef.current!.scrollIntoView({
behavior: 'smooth',
block: 'center'
})
Upvotes: 1
Reputation: 20527
There is also scroll-margin and scroll-padding.
For me, scroll-padding is most useful for this kind of stuff.
https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-padding-top
https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-margin-top
/* Keyword values */
scroll-padding-top: auto;
/* <length> values */
scroll-padding-top: 10px;
scroll-padding-top: 1em;
scroll-padding-top: 10%;
/* Global values */
scroll-padding-top: inherit;
scroll-padding-top: initial;
scroll-padding-top: unset;
Additionally, you can use smooth-scroll by setting scroll behaviour to smooth.
/* Keyword values */
scroll-behavior: auto;
scroll-behavior: smooth;
/* Global values */
scroll-behavior: inherit;
scroll-behavior: initial;
scroll-behavior: revert;
scroll-behavior: unset;
It's likely not Internet Explorer compatible, though.
Upvotes: 9
Reputation: 4542
Is there a way to have an offset and make it scroll smoothly?
#Yes, but not with scrollIntoView()
The scrollIntoViewOptions of Element.scrollIntoView() do not allow you to use an offset. It is solely useful when you want to scroll to the exact position of the element.
You can however use Window.scrollTo() with options to both scroll to an offset position and to do so smoothly.
If you have a header with a height of 30px
for example you might do the following:
function scrollToTargetAdjusted(){
var element = document.getElementById('targetElement');
var headerOffset = 45;
var elementPosition = element.getBoundingClientRect().top;
var offsetPosition = elementPosition + window.pageYOffset - headerOffset;
window.scrollTo({
top: offsetPosition,
behavior: "smooth"
});
}
This will smoothly scroll to your element just so that it is not blocked from view by your header.
Note: You substract the offset because you want to stop before you scroll your header over your element.
#See it in action
You can compare both options in the snippet below.
<script type="text/javascript">
function scrollToTarget() {
var element = document.getElementById('targetElement');
element.scrollIntoView({
block: "start",
behavior: "smooth",
});
}
function scrollToTargetAdjusted() {
var element = document.getElementById('targetElement');
var headerOffset = 45;
var elementPosition = element.getBoundingClientRect().top;
var offsetPosition = elementPosition + window.pageYOffset - headerOffset;
window.scrollTo({
top: offsetPosition,
behavior: "smooth"
});
}
function backToTop() {
window.scrollTo(0, 0);
}
</script>
<div id="header" style="height:30px; width:100%; position:fixed; background-color:lightblue; text-align:center;"> <b>Fixed Header</b></div>
<div id="mainContent" style="padding:30px 0px;">
<button type="button" onclick="scrollToTarget();">element.scrollIntoView() smooth, header blocks view</button>
<button type="button" onclick="scrollToTargetAdjusted();">window.scrollTo() smooth, with offset</button>
<div style="height:1000px;"></div>
<div id="targetElement" style="background-color:red;">Target</div>
<br/>
<button type="button" onclick="backToTop();">Back to top</button>
<div style="height:1000px;"></div>
</div>
window.pageYOffset
have being added, to fix the problem related to @coreyward comments
Upvotes: 238
Reputation: 60043
You can use scrollIntoView()
like in your example
function clickMe() {
var element = document.getElementById('about');
element.scrollIntoView({
block: 'start',
behavior: 'smooth',
});
}
if you add scroll-margin
with the height of the header to the target element (about
):
.about {
scroll-margin: 100px;
}
Nothing else is needed. scroll-margin
is supported by all modern browsers.
Upvotes: 83
Reputation: 1
With a very small hack you can make it work with scrollIntoView()
<section id="about">
<p>About title</p>
<p>About description</p>
</section>
<section id="profile">
<p>About title</p>
<p>About description</p>
</section>
<section>
<span className="section-offset" id="about"></span>
<!-- or <span className="section-offset" id="about" /> for React -->
<p>About title</p>
<p>About description</p>
</section>
<section>
<span className="section-offset" id="profile"></span>
<p>Profile title</p>
<p>Profile description</p>
</section>
.section-offset {
position: relative;
bottom: 60px; // <<< your offset here >>>
}
Move the element selector to a span inside the section, then you can use position: relative
on the span (top/bottom placement does not affect other elements on the page) to set the needed offset. If you need bottom offset, place the span
element at the end of your section (ex: before the </section>
).
Upvotes: 0
Reputation: 1057
Here is the function that I wrote based on the @ekfuhrmann's answer.
It takes the element that needs to be scrolled to as the first parameter and other options in the form of the object as the second parameter, similar to how the window.scrollTo()
function works.
function scrollToTarget(element, options) {
if (options.headerHeight === undefined) {
options.headerHeight = 0;
}
var elementRect = element.getBoundingClientRect();
// If an element has 0 height, then it is hidden, do not scroll
if (elementRect.height == 0) {
return;
}
var offset = elementRect.top - options.headerHeight;
if (options.block == 'center') {
// If an element's height is smaller, than the available screen height (without the height of the header), then add the half of the available space
// to scroll to the center of the screen
var availableSpace = window.innerHeight - options.headerHeight;
if (elementRect.height < availableSpace) {
offset -= (availableSpace - elementRect.height) / 2;
}
}
var optionsToPass = {
top: offset
};
if (options.behavior !== undefined) {
optionsToPass.behavior = options.behavior
}
window.scrollBy(optionsToPass);
}
The main difference is that it uses window.scrollBy()
function instead of window.scrollTo()
, so that we don't need to call .getBoundingClientRect()
on body
.
The options
parameter can contain a headerHeight
field - it can contain the height of the fixed element on the screen, that needs to be ignored when scrolling to the element.
This function can also have a block
option, that for now can only accept a single "center"
value. When set, the element which is scrolled to will appear in the center of the screen excluding the fixed element height. By default, the scroll will be applied to the element's top.
Here we have two overlapping elements with fixed position. Let's imagine the largest of them is not visible on some viewport widths, so we need to dynamically get the available viewport height minus the height of fixed element.
The following example demonstrates, that the element will appear in the center of the available viewport height if the block
option is set to "center"
, similar to how the Element.scrollIntoView()
function works.
function scrollToTarget(element, options) {
if (options.headerHeight === undefined) {
options.headerHeight = 0;
}
var elementRect = element.getBoundingClientRect();
if (elementRect.height == 0) {
return;
}
var offset = elementRect.top - options.headerHeight;
if (options.block == 'center') {
var availableSpace = window.innerHeight - options.headerHeight;
if (elementRect.height < availableSpace) {
offset -= (availableSpace - elementRect.height) / 2;
}
}
var optionsToPass = {
top: offset
};
if (options.behavior !== undefined) {
optionsToPass.behavior = options.behavior
}
window.scrollBy(optionsToPass);
}
var headerElements = [
document.querySelector('.header__wrap'),
document.getElementById('wpadminbar')
];
var maxHeaderHeight = headerElements.reduce(function (max, item) {
return item ? Math.max(max, item.offsetHeight) : max;
}, 0);
document.getElementById('click-me').addEventListener('click', function() {
scrollToTarget(document.querySelector('.scroll-element'), {
headerHeight: maxHeaderHeight,
block: 'center',
behavior: 'smooth'
});
});
body {
margin: 0;
height: 1000px;
}
#wpadminbar, .header__wrap {
position: fixed;
top: 0;
left: 0;
right: 0;
}
#wpadminbar {
height: 32px;
background-color: #1d2327;
z-index: 2;
opacity: 0.8;
}
.header__wrap {
margin: 0 15px;
height: 74px;
background-color: #436c50;
z-index: 1;
}
.scroll-element {
margin-top: 500px;
padding: 1em;
text-align: center;
background-color: #d7d7d7;
}
#click-me {
margin: 100px auto 0;
padding: 0.5em 1em;
display: block;
}
<div id="wpadminbar"></div>
<div class="header__wrap"></div>
<button id="click-me">Click me!</button>
<!-- Some deeply nested HTML element -->
<div class="scroll-element">
You scrolled to me and now I am in the visual center of the screen. Nice!
</div>
Upvotes: 1
Reputation: 6769
I know this is a hack and definitely is something that you should use with caution, but you can actually add a padding
and a negative margin
to the element. I cannot guarantee that it would work for you as I don't have your markup and code, but I had a similar issue and used this workaround to solve it.
Say your header is 30px
and you want an offset of 15px
, then:
#about {
padding-top: 45px; // this will allow you to scroll 15px below your 30px header
margin-top: -45px; // and this will make sure that you don't change your layout because of it
}
Upvotes: 9
Reputation: 181
I tried the other solutions, but I was getting some strange behavior. However, this worked for me.
function scrollTo(id) {
var element = document.getElementById(id);
var headerOffset = 60;
var elementPosition = element.offsetTop;
var offsetPosition = elementPosition - headerOffset;
document.documentElement.scrollTop = offsetPosition;
document.body.scrollTop = offsetPosition; // For Safari
}
and the style:
html {
scroll-behavior: smooth;
}
Upvotes: 12
Reputation: 1729
Søren D. Ptæus's answer got me on the right track but I had issues with getBoundingClientRect()
when not at the top of the window
.
My solution adds a bit more to his to get getBoundingClientRect()
working a bit more consistently with more versatility. I used the approach outlined here and implemented it to get this working as intended.
const element = document.getElementById('targetElement');
const offset = 45;
const bodyRect = document.body.getBoundingClientRect().top;
const elementRect = element.getBoundingClientRect().top;
const elementPosition = elementRect - bodyRect;
const offsetPosition = elementPosition - offset;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
Remember to include the polyfill when implementing this!
Upvotes: 103