jquery.isinview
v1.0.6
Published
Detects if elements are in view - inside of the viewport, an iframe, or other containers. Extremely fast.
Downloads
235
Maintainers
Readme
jQuery.isInView
Setup – Example: Lazy loading – API – Browser support – Limitations – Performance tips – Build and test
This jQuery plugin tells you if elements are in view inside a scrollable container, or inside a container hiding its overflow. It works with respect to the viewport, iframes, or other scrollable elements.
What sets jQuery.isInView apart?
A whole lot of other components aim to do the same job as jQuery.isInView. Most are a bit more lightweight, in terms of file size. They are good choices if you need to check just a few elements, and just occasionally, when timing is not critical.
The main advantage of jQuery.isInView over them is speed. The methods of the plugin are optimized for performance and do their job faster, by orders of magnitude, than any other component I have been able to find (see this live performance test). To give you an idea, filtering 1000 elements takes no more than 3ms on an elderly desktop running Chrome, and about 9ms on an underpowered mobile device.
As a result, jQuery.isInView is suitable for event handlers which are called frequently – scroll and resize handlers, for instance –, and it can deal with large numbers of elements.
Core methods
You can choose between different ways to figure out which elements are in view. The plugin gives you filters like $elems.inView()
. You get an :inViewport
selector, too. Or you can use boolean queries, such as $elem.isInView()
.
Useful helpers
jQuery.isInView exposes a number of useful helper functions which are valuable in their own right. One is $elem.hasScrollbar()
. You also get $elem.scrollbarWidth()
, telling you about the space taken up by scroll bars inside a given element. Finally, there is $elem.ownerWindow()
, which is helpful for operations in iframes and child windows.
(If you think the API is a bit too prolific for your taste, feel free to overwrite any part of it with your own jQuery extensions. Each plugin method is independent of the others, at least with regard to the public API.)
Tests
Browsers are a moving target for development, and they are full of quirks, too. In this environment, the plugin needs to prove that it works. It comes along with a massive test suite, auto-generated from diverse scenarios and a carefully crafted set of base tests. Performance tests are also part of the package.
Example and Demo
There is an interactive demo at JS Bin or Codepen which you can play with. Or explore the lazy-loading example, below.
Other
If you are a happy user of this project already, you can support its development by donating to it. You absolutely don't have to, of course, but perhaps it is something you might actually want to do.
Dependencies and setup
jQuery.isInView depends on jQuery and jQuery.documentSize. They must be ready for use when jquery.isinview.js is loaded.
The stable version of jQuery.isInView is available in the dist
directory (dev, prod), including an AMD build (dev, prod). If you use Bower, fetch the files with bower install jquery.isinview
. With npm, it is npm install jquery.isinview
.
Usage by example: Lazy loading
Suppose you want to lazy-load images as soon as they are scrolled into view. You already have <img>
tags for those images, but their src
attribute hasn't been set, keeping the assets from loading. The URL for each image is stored in a data-url
attribute of the <img>
tag.
Now we implement a scroll handler, which loads each image as soon as it appears on screen. (Of course, we must throttle the calls. To keep things simple, we just use _.throttle from the Underscore library, which does the job just fine.)
A rough draft
Our first iteration results in the following code:
var $scrollable = $( window ),
$images = $( ".imageContainer img" )
.width( 200 ).height( 100 ), // see (*)
loader = function () {
var loaded = [];
$images
.inViewport() // jQuery.isInView filter
.each( function () {
this.src = $( this ).data( "url" );
loaded.push( this );
} );
$images = $images.not( loaded );
};
$scrollable.scroll( _.throttle( loader, 100 ) );
loader(); // see (**)
Here, we use $images.inViewport()
to filter the images and grab those which are already on screen. Only then do we load the actual image assets. But that is too late.
Timely loading
Let's improve our code and load images when they are within 400px of the viewport. For that, we modify the .inViewport()
filter with the tolerance
option: $images.inViewport( { tolerance: 400 } )
. Done!
Next, we should ask ourselves what "in view" means, exactly. When we say that "an element" is "in view", do we talk about the element as a whole, or is part of it enough? And does it include padding and border?
For jQuery.isInView, an element is considered to be "in view" when it is completely visible: its content box, padding, and border, every tiny bit of it. But we can change that. Perhaps we decide that images should load as soon as they begin to move into the tolerance zone, even if only partially. For that, there is the partially
option: $images.inViewport( { partially: true, tolerance: 400 } )
.
In addition, we could be more focused and declare that we only care about the content area, and not about the padding or border of an element. The option box: "content-box"
would flip that particular switch. For lazy loading, however, being that specific does not make sense, so we leave the option at its default value, "border-box"
.
Flexible container
Now, imagine that we don't want to scroll the viewport for some reason, and scroll our container div, .imageContainer
, instead. So we redefine $scrollable = $( ".imageContainer" )
in the snippet above. We also set it to overflow: auto
in the CSS.
But what else do we need to change? We can no longer use .inViewport()
, as its name suggests. So we replace it with the more flexible .inView()
method. It accepts two arguments: the container ($scrollable
in our case), and the options we have chosen above. Here is our final filter, then: $images.inView( $scrollable, { partially: true, tolerance: 400 } )
. It also works for our initial scenario, where $scrollable
is a window.
That's it. This short example should leave you with a pretty good understanding of what jQuery.isInView can do. And as a bonus, the final version is not just some contrived code you'd never use in real life, but rather an efficient, production-ready way to do lazy loading.
Coming up next: a more formal description, more methods, and the fine print. The API section.
(*) We must assign a width and height to the image tags. Without explicit dimensions, they would not occupy any space in the document and fit into the viewport all at once. As a result, the images would all be loaded immediately, defeating the purpose.
(**) We want the first batch of images to appear without scrolling, so we need to call the loader directly once.
API
Core
The primary purpose of jQuery.isInView is to tell you whether or not an element is "in view" – inside the viewport, or otherwise.
Filters
These methods operate on a jQuery collection and reduce it to those elements which are in view.
.inView( [container] [, options] )
Returns: jQuery
Acts as a filter and returns those elements in the collection which are in view inside the viewport, or inside another container.
The container can be a window, iframe, scrollable element (overflow: scroll
or overflow: auto
), an element with overflow: hidden
, or a selector for any of these. A jQuery object or a plain DOM object are equally acceptable. The container defaults to the window containing the elements.
The size of an element is defined by its border-box, which includes its padding and border. Alternatively, the content-box of the element can be used, excluding padding and borders.
Accepts the options for core queries.
.inViewport( [options] )
Returns: jQuery
Acts as a filter and returns those elements in the collection which are in view inside the window. Shorthand for $elem.inView( $elem.ownerWindow(), options )
.
Accepts the options for core queries.
:inViewport selector
Selects all elements which are in view inside the window. Equivalent to calling .inViewport()
on a jQuery collection.
Does not accept options.
Boolean queries
These methods operate on a single element and return whether or not a given element is in view.
.isInView( [container] [, options] )
Returns: boolean
Returns true if the element is in view inside the viewport, or inside another container. Examines the first element in a jQuery collection.
The container can be a window, iframe, scrollable element (overflow: scroll
or overflow: auto
), an element with overflow: hidden
, or a selector for any of these. A jQuery object or a plain DOM object are equally acceptable. The container defaults to the window containing the element.
The size of an element is defined by its border-box, which includes its padding and border. Alternatively, the content-box of the element can be used, excluding padding and borders.
Accepts the options for core queries.
.isInViewport( [options] )
Returns: boolean
Returns true if the element is in view inside the window. Examines the first element in a jQuery collection. Shorthand for $elem.isInView( $elem.ownerWindow(), options )
.
Accepts the options for core queries.
Options
The following options are shared by all core methods of jQuery.isInView.
options.partially
Type: boolean, default: false
If set to true, elements which are just partially inside the container are deemed to be "in view", too. By default, an element must be in full view in order to be included.
options.box
Type: string, default: "border-box", alternative: "content-box"
By default, an element is defined by its border-box and considered to be "in view" if its border, padding, and content area are inside the container. With the box
option set to "content-box"
, borders and padding are ignored – only the content area determines if an element is in or out of view.
Please be aware that the "content-box"
option requires additional DOM queries for every element in a jQuery set, and therefore makes filtering a lot slower. See the performance tips below.
options.excludeHidden
Type: boolean, default: false
If set to true, only elements which take up visible space in the document can be "in view". Elements hidden with display: none
, or have a size of zero, are "not in view" regardless of their position.
By default, elements without dimensions on screen can be "in view" if their location is inside the container. (Somewhat counter-intuitively, this is also a little faster to check.)
options.tolerance
Type: number or string, default: 0
Extends the area in which an element is considered to be "in view". That zone is enlarged by the specified amount on each side of the container. Conversely, a negative value "shrinks" the zone, making it smaller than the container.
The value can either be passed as a number, which is taken to mean "px", or with a unit ("px" or "%" only).
options.direction
Type: string, default: "both", other values: "horizontal", "vertical"
Restricts a query or filter to a single dimension: "horizontal"
or "vertical"
. When evaluating whether or not an element is in view, only that dimension is taken into account.
Helpers
By necessity, jQuery.isInView has to deal with scroll bars a lot. It also operates in windows other than the global one. A few utilities have come out of this.
Scroll bar
.hasScrollbar( [axis] )
Returns: number or object (or undefined)
Checks if an element has a scroll bar. The axis can be specified as "horizontal"
, "vertical"
, or "both"
. Both axes are checked by default.
The return type depends on whether one or both axes are queried. For a single axis, the method returns a boolean. For both axes, it returns an object with the state of each individual axis, e.g. { vertical: true, horizontal: false }
.
Only acts on the first element of a jQuery collection. Returns undefined if the collection is empty.
The .hasScrollbar
method can be called on any sort of item a jQuery collection can hold. It attempts to convert the item intelligently into a sensible target for a scroll bar query if possible. Specifically,
- when called on a window, document, or document element (
<html>
tag),.hasScrollbar()
looks for window scroll bars - when called on an iframe element,
.hasScrollbar()
looks for scroll bars on the content window of the iframe - when called on the body,
.hasScrollbar()
looks for scroll bars on the body tag itself (!). That's because in exotic scenarios, the body can have scroll bars of its own. Usually, there aren't any – if you want to find out about window scroll bars, don't call.hasScrollbar()
on the body.
Please be aware that the method checks for the presence of a scroll bar and nothing else. It doesn't mean that the scroll bar actually scrolls, or takes up any space in the document:
- It always returns true for elements set to
overflow: scroll
. That is because scroll bars appear even if an element doesn't contain content which needs to be scrolled. - It returns true if there is a scroll bar of width 0, which is the standard behaviour of Safari on the Mac and on iOS.
.scrollbarWidth( [axis] )
Returns: number or object (or undefined)
Returns the effective size (width) of a scrollbar on the element, in pixels, as a number without unit. The axis can be specified as "horizontal"
, "vertical"
, or "both"
. Both axes are queried by default.
The return type depends on whether one or both axes are queried. For a single axis, the method returns a number. For both axes, it returns an object with the size of each individual scroll bar, e.g. { vertical: 17, horizontal: 0 }
.
For a given axis, the method returns the value of $.scrollbarWidth()
, a browser constant, if there is a scroll bar, and 0 if there isn't. It does not handle custom scroll bars, and expects the default scroll bars of the browser to appear.
Only acts on the first element of a jQuery collection. Returns undefined if the collection is empty.
Please be aware that the method does not allow you to infer the presence of a scroll bar, or whether it actually scrolls:
- It always returns the default scroll bar width for elements set to
overflow: scroll
. That is because scroll bars appear even if an element doesn't contain content which needs to be scrolled. - It returns 0 if there is no scroll bar, but also if there is a scroll bar of width 0, which is the standard behaviour of Safari on the Mac and on iOS.
For the type of elements the method can be called on, and how they are handled, see .hasScrollbar()
, above.
Other
.ownerWindow()
Returns: Window (or undefined)
Returns the window containing the element. Examines the first element in a jQuery collection.
If the "element" is a window, .ownerWindow()
returns the window itself. If there aren't any elements in the jQuery collection, .ownerWindow()
returns undefined.
If the element is inside an iframe, .ownerWindow()
returns the window representing the iframe. However, if the element is the iframe, .ownerWindow()
returns the window containing the iframe (the global window
, usually).
(Please keep in mind that selecting elements inside an iframe, from code running in the context of the global window, is subject to cross-domain security restrictions and does not always work.)
Browser support
jQuery.isInView has been tested with
- 2015, 2016, 2017 versions of Chrome, Firefox, Safari, and Opera on the desktop
- IE9+, Edge
- Safari on iOS 8-10; Chrome and Firefox on Android 5
- PhantomJS, SlimerJS
The plugin is not formally tested in IE8 (due to a limitation of the test suite), and for that reason, IE8 is not supported. That said, it appears to work there, too. Your mileage may vary – if you still have to support IE8, go ahead and use it, but keep your eyes open.
Feel free to run the test suite on other devices. Feedback is welcome, about successful tests as well as failing ones.
Limitations
jQuery.isInView examines whether elements are in or out of view with regard to an area, defined by a container. From that perspective, elements only ever drop out of view because they overflow their container. But there are other ways elements can be obscured – and they are not captured by jQuery.isInView.
No consideration of stacking:
Positioning can lead to an item being placed on top of another. Floating can obscure the background of a non-floated element. Actual visibility is further complicated by opacity. Obscuring elements by stacking others on top of them is conceptually different from moving elements out of view, which is what the plugin deals with, and not taken into account.
No consideration of clipping and masking:
Likewise, jQuery.isInView does not account for the effects of clipping and masking, using the deprecated
clip()
, its successorclip-path()
, or any new masking techniques which may or may not land in the next crop of browsers.
There are also a few limitations with regard to HTML elements and their styles:
The plugin doesn't support elements with
position: fixed
, or elements inside containers withposition: fixed
.Containers must not be rotated with a CSS transform. Scaling is ok. (You can do anything with content, including rotation, that's ok).
The
<object>
tag is not supported as a container.
In terms of weird edge cases, there is one for .hasScrollbar()
. I only mention it for the sake of completeness – you are extremely unlikely to run into it in an actual application. It can only occur in browsers which display scroll bars of width 0, like those on iOS and Android. The overflow setting of the documentElement has to be different from the default, "visible"
. The overflow of the body has to be set to "auto"
. Then, if you take the unusual step of querying the scroll bars of the body itself (as opposed to the window), .hasScrollbar()
fails to detect them. That is not a bug, and can't be fixed. It is not possible to detect the presence of scroll bars in these circumstances.
Finally, the plugin doesn't deal with multiple, nested scrolls yet. But it merely isn't aggregating the results for you. You can call it on each individual container and simply chain the results (with &&
for boolean tests, or filter chaining).
Performance tips
A few best practices speed up jQuery.isInView calls, and do so by a huge amount. In hot code paths like scroll handlers, they have the potential to make or break your application as a whole. So here goes.
If you act on multiple elements, use a filter, not a boolean query in a loop. In other words, don't do this:
$content.each( function () { if ( $( this ).isInView( $container ) ) { /* ... do stuff */ } // WRONG! } );
Instead, use the corresponding filter:
$content.inView( $container ).each( function () { // ... do stuff } );
The speed difference can be huge. Perhaps you can still get away with using a boolean query in a loop if your container is the
window
. It is five times slower than a filter then, but even that is still blazing fast. If your container is an ordinary HTML element, however, the boolean query slows down the process by a factor of up to 50 (!). In a nutshell: Prefer filters, and you won't go wrong.The option
box: "content-box"
is an expensive choice, much slower than working with the"border-box"
(which is the default). When performance matters, use it only if you really have to.The option
excludeHidden: true
also has a performance cost, though to a much lesser degree. It is off (false) by default. Leave it off if you can, but don't lose your sleep over it.
Build process and tests
If you'd like to test, fix, customize or otherwise improve jQuery.isInView: here are your tools.
Setup
npm sets up the environment for you.
- The only thing you've got to have on your machine (besides Git) is Node.js. Download the installer here.
- Clone the project and open a command prompt in the project directory.
- Run the setup with
npm run setup
. - Make sure the Grunt CLI is installed as a global Node module. If not, or if you are not sure, run
npm install -g grunt-cli
from the command prompt.
Your test and build environment is ready now. If you want to test against specific versions of jQuery, edit bower.json
first.
Running tests, creating a new build
Considerations for testing
Some tests are executed in a child window (aka pop-up window). Please disable the pop-up blocker of the browser for the domain the tests are run under (usually localhost), or they will fail.
To run the tests on remote clients (e.g. mobile devices), start a web server with grunt interactive
and visit http://[your-host-ip]:9400/web-mocha/
with the client browser. Running the tests in a browser like this takes a long time, so it makes sense to disable the power-save/sleep/auto-lock timeout on the mobile device.
Further, on iOS, you need to guide the tests along. Even with the pop-up blocker disabled, iOS displays a notification each time a child window is opened by a test, and you need to dismiss each notification manually. You have about a minute to hit OK before the related part of the test suite times out. These notifications show up multiple times, so keep an eye on your device until all tests are done.
Performance tests
You can examine the performance of jQuery.isInView, and compare it to some other popular plugins which have served as a benchmark during development. Spin up a server with grunt demo
and navigate to the performance test page, http://[your-host-ip]:9400/demo/perftest/
.
A live version of the test, showing data of a stable, published build, is available on JS Bin.
Tool chain and commands
The test tool chain: Grunt (task runner), Karma (test runner), Mocha (test framework), Chai (assertion library), Sinon (mocking framework). The good news: you don't need to worry about any of this.
A handful of commands manage everything for you:
- Run the tests in a terminal with
grunt test
. - Run the tests in a browser interactively, live-reloading the page when the source or the tests change:
grunt interactive
. - If the live reload bothers you, you can also run the tests in a browser without it:
grunt webtest
. - Run the linter only with
grunt lint
orgrunt hint
. (The linter is part ofgrunt test
as well.) - Build the dist files (also running tests and linter) with
grunt build
, or justgrunt
. - Build continuously on every save with
grunt ci
. - Change the version number throughout the project with
grunt setver --to=1.2.3
. Or just increment the revision withgrunt setver --inc
. (Remember to rebuild the project withgrunt
afterwards.) grunt getver
will quickly tell you which version you are at.
Finally, if need be, you can set up a quick demo page to play with the code. First, edit the files in the demo
directory. Then display demo/index.html
, live-reloading your changes to the code or the page, with grunt demo
. Libraries needed for the demo/playground should go into the Bower dev dependencies – in the project-wide bower.json
– or else be managed by the dedicated bower.json
in the demo directory.
The grunt interactive
and grunt demo
commands spin up a web server, opening up the whole project to access via http. So please be aware of the security implications. You can restrict that access to localhost in Gruntfile.js
if you just use browsers on your machine.
Changing the tool chain configuration
In case anything about the test and build process needs to be changed, have a look at the following config files:
karma.conf.js
(changes to dependencies, additional test frameworks)Gruntfile.js
(changes to the whole process)web-mocha/_index.html
(changes to dependencies, additional test frameworks)
New test files in the spec
directory are picked up automatically, no need to edit the configuration for that.
Facilitating development
To my own surprise, a kind soul wanted to donate to one of my projects, but there hadn't been a link. Now there is.
Please don't feel obliged in the slightest. The license here is MIT, and so it's free. That said, if you do want to support the maintenance and development of this component, or any of my other open-source projects, I am thankful for your contribution.
Naturally, these things don't pay for themselves – not even remotely. The components I write aim to be well tested, performant, and reliable. These qualities may not seem particularly fascinating, but I put a lot of emphasis on them because they make all the difference in production. They are also rather costly to maintain, time-wise.
That's why donations are welcome, and be it as nod of appreciation to keep spirits up. Thank you!
Release Notes
v1.0.6
- Fixed
:inViewport
selector when negated (issue #3)
v1.0.5
- Updated the jQuery dependency to jQuery 3.1
v1.0.4
- Updated the jQuery dependency to jQuery 3
v1.0.2
- Removed $.scrollbarWidth() (now provided by jQuery.documentSize)
- Updated the jQuery.documentSize dependency
v1.0.1
- Added friendly error message for invalid container param
- Removed redundant project boilerplate
- Improved demo, documentation
v1.0.0
- Improved caching, especially of container properties
- Improved handling of mixed overflows in $.fn.hasScrollbar()
- Fixed tests for PhantomJS, SlimerJS, IE10
- Added missing copyright notice to Leche test helper
v0.5.1
- Fixed wrong treatment of container border
- Fixed readme typos
v0.5.0
- Removed code for document size detection, using jQuery.documentSize instead
- Fixed swapped axes for body query in $.fn.hasScrollbar()
- Refactored $.fn.hasScrollbar
- Fixed $ parameter in IIFE
v0.4.1
- Made the browser scrollbar width detection run immediately on DOM ready
- Simplified browser scrollbar width detection
- Fixed $(window).hasScrollbar() for IE9
- Fixed tests for IE9
v0.4.0
- Fixed plugin code for IE8
- Fixed $(window).hasScrollbar() with body overflow: auto, hidden for FF and IE
- Excluded complex hasScrollbar() edge case, when called on body, from handling in some browsers (documented in readme)
- Handled iOS anomaly which ignores overflow-y: hidden for the viewport
- Improved handling of overflow edge cases for viewport and body
- Improved test code, fixed for iOS
- Added an AMD demo
- Fleshed out the readme
v0.3.0
- Fixed plugin and tests for IE
- Fixed test code for FF
- Added IE, Safari, Opera launchers to test environment
- Fixed various minor bugs
v0.2.0
- Added box option
- Added $.fn.hasScrollbar, $.fn.scrollbarWidth
- Reorganized plugin, made all tests pass
- Completed core tests, increased test coverage
v0.1.1
- Fixed custom inViewport selector, switched to current Sizzle API
v0.1.0
- Initial development, tests, documentation
License
MIT.
Copyright (c) 2014-2017 Michael Heim.
Code in the data provider test helper: (c) 2014 Box, Inc., Apache 2.0 license. See file.