12 years and noone has posted a simple, solid answer faithful to the original question. Granted, some other answers may work in your case if you don't really need to test whether the element is visible to the user; this gets into XY problem territory. Here's a summary of everything issue/quirk with the other answers:
getComputedStyle only works in the most trivial of cases. Neither display nor visibility inherit by default (assuming they weren't overridden by css)visibility: hidden and content-visibility: hidden to still be shown.checkVisibility() doesn't check whether the element is on the screen and visible, rather it checks if its on the page and could become visible at some point. (With the notable exception of not checking opacity, visibility, or element overlaps.) THe only difference, as far as I've been able to find, between checkVisibility() and !!ele.offsetParent||!!ele.offsetHeight||!!ele.offsetWidth is that checkVisibility() returns false for the children of content-visibility: hidden elements.Overall, I'd rate @FanaticPythoner's answer the winner at failing in more places than any other answer. His answer fails in iFrames, considers any element clipping a single pixel outside the viewport as hidden, badly misuses getComputedStyle without checking it upon ancestors, conflates parentNode with the nearest scroll ancestor scroll pane, seems not to understand z-index is relative to other elements (and would require extensive logic to reconcile absolute/fixed/sticky positioning of ancestors), checks all ancestors displays and opacities without considering visibility, and finishes with what seems to be half-baked attempted to only scan adjacent siblings for overlap (wtf?).
Here's a comprehensive solution that answers, simply, is the element visible to the user?
/** isVisibleToUser: tell whether the user can see a HTML element
 *    ele (required):      The DOM element to test.
 *    clip (default true): Consider any portion not scrolled into view.
 *                          as hidden. Use false to test if the element
 *                          can become visible if users scrolls to it.
 *    thres (default 0.5): What percentage must be visible for `true`.
 *    samp (default 2.14): Equispaced x/y samples. Decimal offsets it.
 *                         E.g. 2.14 calls elementFromPoint four times.
 */
function isVisibleToUser(ele, clip=true, thres=0.5, samp=2.14) {
    if ( ! checkVisibilityPolyfill(ele) ) return false;
    var t=+thres, s=+samp, B=ele.getBoundingClientRect();
    var O=ele.offsetParent, d=ele.ownerDocument, G=d.defaultView;
    var eT=B.top|0,eL=B.left|0,eB=B.bottom|0,eR=B.right|0,iW,iH;
        if (O) {B = O.getBoundingClientRect();
        var oT=B.top|0,oL=B.left|0,oB=B.bottom|0,oR=B.right|0;
        if (clip) iW=G.innerWidth|0,iH=G.innerHeight|0,
                      oT=oT<0?0:oT|0,oL=oL<0?0:oL|0,
                  oB=oB>iH?iH:oB|0,oR=oR>iW?iW:oR|0;
        var oW=oR-oL|0, oH=oB-oT|0;
    }else oT=0,oL=0,oR=oW=G.innerWidth|0,oB=oH=G.innerHeight|0;
    var eW=eR-eL|0, bX=(eL<oL?oL-eL|0:0) + (eR>oR?eR-oR|0:0)|0;
    var eH=eB-eT|0, bY=(eT<oT?oT-eT|0:0) + (eB>oB?eB-oB|0:0)|0;
    if(bX>(t*eW|0)||bY>(t*eH|0)||bX*bY>eW*eH*t)return false;
    var sW=eW/(s+1),sH=eH/(s+1),I=s|0,l=I*I*-t|0;
    for (var i=1; i<=I; i=i+1|0)
        for (var j=1; j<=I; j=j+1|0)
            if(d.elementFromPoint(.5+eL+sW*i|0,.5+eT+sH*j|0)
               !== ele) if ((l=l+1|0) >= 0) return false;
    return true; // all checks passed!
}
/** checkVisibilityPolyfill: fallback if checkVisibility unsupported
 */
function checkVisibilityPolyfill(ele) {
    if ("checkVisibility" in ele) return ele.checkVisibility();
    return !!ele.offsetParent||!!ele.offsetHeight||!!ele.offsetWidth;
}