42

I have a list of figures containing background images. Something like the following:

<ul>
  <li>
    <figure style="background-image: url(...);"></figure>
  </li>
  <li>
    <figure style="background-image: url(...);"></figure>
  </li>
  <li>
    <figure style="background-image: url(...);"></figure>
  </li>
</ul>

Each of these images has their background-size set to cover and background-attachment set to fixed.

figure {
  width: 100%;
  height: 100%;
  background-size: cover;
  background-attachment: fixed;
}

When each of the figures takes up the entire viewport, this works fine, but if there is an offset of any kind the background-image gets clipped.

As far as I can tell this is by design (https://developer.mozilla.org/en-US/docs/Web/CSS/background-size#Values).

I would like the images to either clip vertically or horizontally but not both, and be centred by the size of the figure itself.

I know there are javascript solutions but is there a way to do this using CSS?

Here is a working example: http://codepen.io/Godwin/pen/KepiJ

Godwin
  • 9,739
  • 6
  • 40
  • 58
  • It is being clipped horizontally only in the demo. What else do you mean? – Zach Saucier Feb 14 '14 at 23:10
  • @ZachSaucier, it's being clipped horizontally and vertically in Chrome and Firefox for me. Take a look at the maps, if you know what the US should look like, there's no New England, Florida, or western states. – Godwin Feb 14 '14 at 23:15
  • 1
    (If you're viewing it at a relatively small height) – Godwin Feb 14 '14 at 23:16

9 Answers9

68

Unfortunately this is simply an artifact of how fixed positioning works in CSS and there is no way around it in pure CSS - you have to use Javascript.

The reason this happens is due to the combination of background-attachment: fixed and background-size: cover. When you specify background-attachment: fixed it essentially causes the background-image to behave as if it were a position: fixed image, meaning that it's taken out of the page flow and positioning context and becomes relative to the viewport rather than the element it's the background image of.

So whenever you use these properties together, the cover value is being calculated relative to the size of the viewport irrespective of the size of the element itself, which is why it works as expected when the element is the same size as the viewport but is cropped in unexpected ways when the element is smaller than the viewport.

To get around this you basically need to use background-attachment: scroll and bind an event listener to the scroll event in JS that manually updates the background-position relative to how far the window has been scrolled in order to simulate fixed positioning but still calculate background-size: cover relative to the container element rather than the viewport.

Ennui
  • 10,102
  • 3
  • 35
  • 42
  • 3
    Is there a specific reason that in the last paragraph you suggest using CSS sizing and JS positioning vs CSS positioning and JS sizing? – Will Oct 10 '14 at 13:15
  • 2
    Not particularly, either way should be doable although I haven't tried it the way you mentioned. Mostly just because I think JS positioning is a bit faster/easier to write than JS sizing in this context. – Ennui Oct 15 '14 at 04:48
  • 5
    There's a much easier way to do this. Instead of using background-position: fixed, make a background ELEMENT with position: fixed, and then use background-size: cover with that. That will cover the image according to the size of the bg element vs. the viewport, and still give you a fixed background. – Matthew Dean Nov 25 '18 at 19:23
23

There's a jQuery fix for this: http://jsfiddle.net/QN9cH/1/ I know it's not optimal but at least it works :)

$(window).scroll(function() {
  var scrolledY = $(window).scrollTop();
  $('#container').css('background-position', 'left ' + ((scrolledY)) + 'px');
});
Nick
  • 425
  • 3
  • 9
4

Nick Noordijk's answer put me on the right track, but I like to avoid scripts that perform a calculation every time the scroll event happens. Here's my version that only performs the calculation when page loads or screen size changes:

html:

<div class="fake-img"></div>

css:

.fake-img {
    display: block;
    height: 280px;  /* set the height here */
    width: 100%;
    background-image: url('http://example.com/path/to/image.jpg');
    background-repeat: no-repeat;
    background-position: center 68px;
    background-size: auto 50%; /* make a "best guess" here as a default */
    background-attachment: fixed;
    position: relative;
}

jQuery:

$(window).on('resize load orientationchange', function(){
    responsive_calc();
});

var responsive_calc = function(){

    // get the viewport height
    var h = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;

    // get the element height
    var bannerHeight = $('.fake-img').height();

    // get the integer percentage value difference between them
    var bgHeightPercent = Math.ceil(bannerHeight/h*100);

    // set background-size height to match the element instead of the viewport
    $('.fake-img').css('background-size', 'auto ' + bgHeightPercent + '%');
}

Note that this really only works with landscape "banner" images - using background-size: auto nn% doesn't have the same advantage of background-size: cover in working no matter if your image has excess in either direction.

squarecandy
  • 4,894
  • 3
  • 34
  • 45
3
background: url(<image>) center top no-repeat fixed;
background-size: auto <width size in %>;

There is no real solution. However, you could make size height of the image auto instead of cover. And adjust the width size in % in your element until it bites the border.

You see if you resize your window, it won't be clipped. Instead, it will always keep the original image size format. With this above you basically 'zoom' it but doesn't 'cover' it up.

Thielicious
  • 4,122
  • 2
  • 25
  • 35
2

The background-size: cover; property is indeed clipping the image in order for it to fill the area and not have any empty space.

The background-size: contain; property is determining which dimension is larger and scales according to that. So if you have a 100px x 100px block and a background image of 200x150px, setting the background-size to contain will scale the image to 100x75px. In this scenario however, you will have empty space if the element's aspect ratio is different than the image's.

You can also manually control which proportion has priority, assuming you know the image's aspect ratio.

So if you know that your image is always 100x200px, this means that the width is always the small dimension and the height the large one.

Now setting the background-size: 100% auto; will ensure that you will not get empty space but you will end up with clipping. If you set it to background-size: auto 100%; it will ensure that no clipping takes place and the height will never have empty space ( but the width will).

If you do want clipping and just center the image, use background-position: 50%;.

kakashigr
  • 124
  • 6
  • Thanks but this doesn't address the problem, this is simply an explanation of how `contain` works but the problem I'm trying to address is with `cover`. The problem here is that `cover` is clipping both the horizontally and vertically when the image is taller than the viewport, I would like it to clip one or the other but never both and never none as `contain` does. – Godwin Feb 18 '14 at 00:02
  • You can use background-size: 100% auto; then. This will ensure what the width will be inside the element, not clipped and 100% in size. If the height is smaller than the element's height, it will have empty space there. If not, it will be clipped. – kakashigr Feb 19 '14 at 13:04
  • I would like both the width and the height to be covered at all times. – Godwin Feb 19 '14 at 13:35
  • This can happen in two ways: 1. If the height and width of the element are the same as the picture, setting background-size to background-size: 100% 100%; However if they are different your image will have the wrong aspect ratio. 2. If the element has different width and height or one of them is variable (changing) then you can't fill the area without clipping. – kakashigr Feb 21 '14 at 10:48
  • 2
    Thanks for trying to help but I think you may have misunderstood the question. I am intimately aware of how background-size is supposed to work, the problem is when background-attachment is also used, it seems to change the behavior unexpectedly. – Godwin Feb 21 '14 at 14:01
1

Another very simple solution is to use of the vw and vh units (which, in case you didn't know, has totally passable browser support for most cases). For example, instead of doing background-size: cover, do background-size: auto 100vh.

If you're not familiar with vw and vh, they're viewport units, and they correspond to viewport width and viewport height. The values correspond to a percentage of the viewport height or width. For example, 50vw means 50% of the viewport width.

For our purposes, we can simply tell the background to be 100% of the viewport height.

Here's a fiddle.

If you need to accommodate for different aspect ratios you can take advantage of the aspect-ratio @media rules.

Pete
  • 7,289
  • 10
  • 39
  • 63
0

An example of parallax effect for separately arranged elements (not for fullscreen elements):

html:

<div style="background-image: url('/path/to/image.jpg')">Content</div>

css:

#element_to_scroll {
    background-repeat: no-repeat;
    background-size: cover; 
    background-position-x: center; 
    background-position-y: 0;   
}

js:

$(window).scroll(function() {
    var realImageWidth = 1280;
    var realImageHeight = 1024;
    var viewportBottom = $(window).scrollTop() + $(window).height();
    $('#element_to_scroll').each(function(){
        var scrollAmountPx = realImageHeight/(realImageWidth/$(this).outerWidth())-$(this).outerHeight();
        var elementOffsetFromBottom = viewportBottom-$(this).offset().top;
        var scrollAreaHeight = $(this).outerHeight() + $(window).height();
        if(elementOffsetFromBottom>0 && elementOffsetFromBottom<scrollAreaHeight) {
            var backgroundPositionOffset = Math.ceil(scrollAmountPx/scrollAreaHeight*elementOffsetFromBottom);
            //$(this).css('background-position-y',"-"+backgroundPositionOffset+"px");
            $(this).clearQueue().animate({'background-position-y':"-"+backgroundPositionOffset+"px"},50);
        }
    });
});
baduga
  • 233
  • 1
  • 8
-1

Update in July 2018: There is now a 1-line fix for this problem

I had this same problem until I read this article, which taught me that I could add this line to my CSS to make it work:

will-change: transform;

It worked!

Now my CSS looks like this:

.myClass{
    position: relative;
    background: hsla(300, 100%, 90%, 0.1);
    overflow: hidden;
    &::before {
        background-image: url(/img/bg.jpg);
        background-size: cover;
        background-size: cover !important;
        -webkit-background-size: cover !important;
        background-repeat: repeat repeat !important;
        background-attachment: scroll !important;//"fixed" is the desired effect, but mobile browsers find it too "expensive" and disabled it, so use "scroll"
        background-position: 30% 50%;
        @media(min-width: $screen-sm-min){
            background-attachment: fixed !important;
            background-position: 30% 20%;
            will-change: transform;
        }//768
        content: "";
        display: block;
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        opacity: 0.1;
    }
}

It's working for me on Windows Chrome, Edge, and Safari. But not Firefox.

Ryan
  • 22,332
  • 31
  • 176
  • 357
-8

I did a version with Responsive background image.

.flexslider .slides > li {
  background-position: center center;
  background-repeat: no-repeat;
  -webkit-background-size: cover;
  -moz-background-size: cover;
  -o-background-size: cover;
  background-size: cover;
}
#slider,
.alink {
  min-height: 300px
}
.alink {
  display: block
}

http://jsfiddle.net/onigetoc/grpku1gk/

Gino
  • 1,834
  • 2
  • 19
  • 20