CSS background image: url(), size, position and repeat

One property puts a picture behind an element. Three more decide how big it is, which part shows, and whether it tiles.

To add a CSS background image, give the element a size and set background-image: url("photo.jpg"). The picture is painted behind the element's content. Three more properties control it: background-size for how big, background-position for which part shows, background-repeat for tiling.

.hero {
  min-height: 300px;
  background-image: url("images/photo.jpg");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

Try the three properties on a box. Change a value and the CSS below the box updates.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>background-image builder</title>
<style>
  :root {
    /* a landscape drawn in SVG; in your own page use url("images/photo.jpg") */
    --scene: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1200' height='600' viewBox='0 0 1200 600' preserveAspectRatio='none'%3E%3Cdefs%3E%3ClinearGradient id='s' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23ffe3a3'/%3E%3Cstop offset='1' stop-color='%23f59e62'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='1200' height='600' fill='url(%23s)'/%3E%3Ccircle cx='900' cy='230' r='70' fill='%23fffbea'/%3E%3Cpath d='M0 420L220 250L420 400L640 220L900 420L1200 280V600H0Z' fill='%23b07aa1'/%3E%3Cpath d='M0 500L300 380L600 480L900 360L1200 470V600H0Z' fill='%235b4a7a'/%3E%3Crect y='540' width='1200' height='60' fill='%232f2a4a'/%3E%3C/svg%3E");
  }
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .box {
    height: 220px;                 /* no height = no background to see */
    border-radius: 12px;
    border: 2px dashed #9aa3b2;
    background-color: #e8ebf0;     /* shows wherever the image does not cover */
    background-image: var(--scene);
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
  }
  .controls { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 14px 0 10px; }
  label { font-size: 13px; font-weight: 600; display: grid; gap: 4px; }
  select { font: inherit; font-weight: 400; padding: 6px; border-radius: 8px; border: 1px solid #c9ced6; background: #fff; }
  pre { margin: 0; padding: 12px 14px; border-radius: 10px; background: #1d2330; color: #e5e9f0; font-size: 13px; line-height: 1.5; overflow-x: auto; }
  @media (max-width: 480px) { .controls { grid-template-columns: 1fr 1fr; } .box { height: 170px; } }
</style>
</head>
<body>
<div class="box" id="box"></div>

<div class="controls">
  <label>background-size
    <select id="size">
      <option>cover</option><option>contain</option><option>auto</option>
      <option>50%</option><option>200px</option><option>100% 100%</option>
    </select>
  </label>
  <label>background-position
    <select id="position">
      <option>center</option><option>left top</option><option>right bottom</option>
      <option>75% 38%</option><option>20px 40px</option>
    </select>
  </label>
  <label>background-repeat
    <select id="repeat">
      <option>no-repeat</option><option>repeat</option><option>repeat-x</option>
      <option>space</option><option>round</option>
    </select>
  </label>
</div>

<pre id="css"></pre>

<script>
  const box = document.getElementById('box');
  const out = document.getElementById('css');
  const pick = (id) => document.getElementById(id).value;

  function update() {
    box.style.backgroundSize = pick('size');
    box.style.backgroundPosition = pick('position');
    box.style.backgroundRepeat = pick('repeat');
    out.textContent =
      '.box {\n' +
      '  height: 220px;\n' +
      '  background-image: url("landscape.svg");\n' +
      '  background-size: ' + pick('size') + ';\n' +
      '  background-position: ' + pick('position') + ';\n' +
      '  background-repeat: ' + pick('repeat') + ';\n' +
      '}';
  }

  document.querySelectorAll('select').forEach((s) => s.addEventListener('change', update));
  update();
</script>
</body>
</html>
A box with a background image. Pick a size, position and repeat value and read the CSS it produces.

The picture here is an SVG drawn inside the page, so the example needs no image file. In your own page, point url() at a JPG, PNG, WebP or SVG file.

background-size: cover, contain or a number

By default a background image is drawn at its own pixel size. A large photo in a small box shows only its top-left corner. background-size changes that.

cover fills and crops, contain fits and leaves gaps, 100% 100% stretches.
cover fills and crops, contain fits and leaves gaps, 100% 100% stretches.
Value What it does Use it for
auto The image's own size (the default) Small icons and tiles
cover Fills the box, crops what sticks out Hero sections, cards, full-screen photos
contain Fits the whole image, leaves gaps Logos and diagrams that must not be cut
200px Sets the width, height follows the proportions Tiles and patterns
50% A share of the box's width Images that scale with the box
100% 100% Width and height set separately Rarely: it distorts photos

With contain, the gaps show the element's background-color. Set one that matches the picture's edges. CSS background-color covers the colour side.

background-position and the focus point

When cover crops the picture, background-position decides which part survives. Keywords such as center, left top and right bottom work. Percentages give finer control.

X% lines up the point X% across the picture with the point X% across the box.
X% lines up the point X% across the picture with the point X% across the box.

A percentage works like a pin. 75% 38% puts the point 75% across and 38% down the picture exactly 75% across and 38% down the box. Whatever the box's shape, that spot stays visible.

So find the important part of the picture, such as a face or a product, and write its position as percentages. That is the focus point. On a narrow phone screen the edges are cropped away and the focus point stays in view.

Pixel values behave differently. 20px 40px moves the image's top-left corner 20px right and 40px down. It does not follow the picture's content when the box changes size.

background-repeat and patterns

A background image repeats in both directions by default. For a photo, that makes copies appear beside it when the box is larger than the image, so set no-repeat.

For a pattern, repetition is the point. Draw one small tile and let the browser copy it:

.pattern {
  background-image: url("tile.svg");
  background-size: 40px 40px;
  background-repeat: repeat;
}

repeat-x and repeat-y repeat along one direction only. space spreads whole copies out with gaps between them, and round resizes the tile slightly so a whole number of copies fits.

Several layers and a gradient overlay

background-image takes a comma-separated list. The first image is painted on top, and background-color sits below every layer.

White text on a bright photo is often hard to read. The common fix is a see-through gradient laid over the photo:

.hero {
  color: #fff;
  background-image:
    linear-gradient(rgba(20, 16, 40, 0.1), rgba(20, 16, 40, 0.6)),
    url("images/photo.jpg");
  background-size: cover;
}

Turn the overlay on and off, and change how dark it gets:

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Gradient overlay for readable text</title>
<style>
  :root {
    /* a landscape drawn in SVG; in your own page use url("images/photo.jpg") */
    --scene: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1200' height='600' viewBox='0 0 1200 600' preserveAspectRatio='none'%3E%3Cdefs%3E%3ClinearGradient id='s' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23ffe3a3'/%3E%3Cstop offset='1' stop-color='%23f59e62'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='1200' height='600' fill='url(%23s)'/%3E%3Ccircle cx='900' cy='230' r='70' fill='%23fffbea'/%3E%3Cpath d='M0 420L220 250L420 400L640 220L900 420L1200 280V600H0Z' fill='%23b07aa1'/%3E%3Cpath d='M0 500L300 380L600 480L900 360L1200 470V600H0Z' fill='%235b4a7a'/%3E%3Crect y='540' width='1200' height='60' fill='%232f2a4a'/%3E%3C/svg%3E");
    --shade: 0.6;  /* how dark the overlay gets at the bottom */
  }
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .hero {
    min-height: 240px; padding: 28px; border-radius: 12px;
    display: flex; flex-direction: column; justify-content: flex-end;
    color: #fff;
    background-image: var(--scene);
    background-size: cover;
    background-position: center;
  }
  /* the first layer is painted on top: a see-through gradient over the picture */
  .hero.overlay {
    background-image:
      linear-gradient(rgba(20, 16, 40, 0.1), rgba(20, 16, 40, var(--shade))),
      var(--scene);
  }
  .hero h2 { margin: 0 0 6px; font-size: 30px; }
  .hero p { margin: 0; font-size: 16px; max-width: 30em; }
  .controls { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; margin: 14px 0 10px; font-size: 14px; }
  button { font: inherit; font-weight: 600; padding: 8px 14px; border-radius: 8px; border: 0; background: #1d2330; color: #fff; cursor: pointer; }
  input[type=range] { width: 150px; }
  pre { margin: 0; padding: 12px 14px; border-radius: 10px; background: #1d2330; color: #e5e9f0; font-size: 13px; line-height: 1.5; overflow-x: auto; }
</style>
</head>
<body>
<div class="hero" id="hero">
  <h2>Evening walks</h2>
  <p>White text straight on a bright photo is hard to read. A dark gradient layer fixes it.</p>
</div>

<div class="controls">
  <button id="toggle" type="button">Overlay: off</button>
  <label>Darkness <input id="shade" type="range" min="0.2" max="0.9" step="0.05" value="0.6"></label>
</div>

<pre id="css"></pre>

<script>
  const hero = document.getElementById('hero');
  const toggle = document.getElementById('toggle');
  const shade = document.getElementById('shade');
  const out = document.getElementById('css');

  function update() {
    const on = hero.classList.contains('overlay');
    toggle.textContent = 'Overlay: ' + (on ? 'on' : 'off');
    document.documentElement.style.setProperty('--shade', shade.value);
    out.textContent = on
      ? 'background-image:\n  linear-gradient(rgba(20,16,40,.1), rgba(20,16,40,' + shade.value + ')),\n  url("photo.jpg");'
      : 'background-image: url("photo.jpg");';
  }

  toggle.addEventListener('click', () => { hero.classList.toggle('overlay'); update(); });
  shade.addEventListener('input', () => {
    hero.classList.add('overlay');  // moving the slider turns the overlay on
    update();
  });
  update();
</script>
</body>
</html>
The same heading with and without a dark gradient layer over the picture.

A gradient counts as an image, so it can be a layer like any file. CSS gradient background covers the gradient syntax in detail. The other background properties also take lists, one value per layer, in the same order.

A full-screen background image

For a hero that fills the screen, give the section min-height: 100vh and combine everything above: cover, a focus point, no-repeat and an overlay. Below it, a second section uses a repeating SVG tile.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Full-screen background image</title>
<style>
  :root {
    /* a landscape drawn in SVG; in your own page use url("images/photo.jpg") */
    --scene: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1200' height='600' viewBox='0 0 1200 600' preserveAspectRatio='none'%3E%3Cdefs%3E%3ClinearGradient id='s' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23ffe3a3'/%3E%3Cstop offset='1' stop-color='%23f59e62'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='1200' height='600' fill='url(%23s)'/%3E%3Ccircle cx='900' cy='230' r='70' fill='%23fffbea'/%3E%3Cpath d='M0 420L220 250L420 400L640 220L900 420L1200 280V600H0Z' fill='%23b07aa1'/%3E%3Cpath d='M0 500L300 380L600 480L900 360L1200 470V600H0Z' fill='%235b4a7a'/%3E%3Crect y='540' width='1200' height='60' fill='%232f2a4a'/%3E%3C/svg%3E");
    /* a 40px tile: one dot and a thin grid line, repeated */
    --tile: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Ccircle cx='20' cy='20' r='4' fill='%23c7d2fe'/%3E%3Cpath d='M0 0h40v40' fill='none' stroke='%23e0e7ff'/%3E%3C/svg%3E");
    --fx: 75%;  /* focus point: where the sun is in the picture */
    --fy: 38%;
  }
  body { margin: 0; font-family: system-ui, sans-serif; color: #1d2330; }

  .hero {
    position: relative;
    min-height: 100vh;             /* the full screen, whatever its size */
    box-sizing: border-box; padding: 24px;
    display: flex; flex-direction: column; justify-content: flex-end;
    color: #fff;
    background-color: #5b4a7a;     /* shows while the image loads */
    background-image:
      linear-gradient(rgba(20, 16, 40, 0.05) 40%, rgba(20, 16, 40, 0.7)),
      var(--scene);
    background-size: cover;
    background-position: var(--fx) var(--fy);
    background-repeat: no-repeat;
  }
  .hero h1 { margin: 0 0 6px; font-size: clamp(28px, 7vw, 44px); }
  .hero p { margin: 0 0 8px; max-width: 32em; }

  .focus {
    position: absolute; top: 12px; right: 12px;
    display: grid; gap: 4px; padding: 10px 12px; border-radius: 10px;
    background: rgba(255, 255, 255, 0.92); color: #1d2330; font-size: 13px;
  }
  .focus b { font-size: 12px; }
  .focus input { width: 130px; }

  .pattern {
    padding: 40px 24px;
    background-color: #f5f7ff;
    background-image: var(--tile);
    background-size: 40px 40px;    /* one tile */
    background-repeat: repeat;     /* the default, written out for clarity */
  }
  .pattern .card { max-width: 420px; margin: 0 auto; padding: 18px 20px; border-radius: 12px; background: #fff; box-shadow: 0 6px 20px rgba(0, 0, 0, .08); }
  .pattern h2 { margin: 0 0 6px; font-size: 20px; }
  .pattern p { margin: 0; line-height: 1.5; }
</style>
</head>
<body>
<section class="hero">
  <div class="focus">
    <b id="readout">background-position: 75% 38%</b>
    <label>X <input id="fx" type="range" min="0" max="100" value="75"></label>
    <label>Y <input id="fy" type="range" min="0" max="100" value="38"></label>
  </div>
  <h1>Evening walks</h1>
  <p>The picture always covers the screen. Narrow the window: the focus point keeps the sun in view while the edges are cropped.</p>
  <p>Scroll down for a patterned section.</p>
</section>

<section class="pattern">
  <div class="card">
    <h2>A repeating pattern</h2>
    <p>This background is one 40px SVG tile. background-repeat copies it across and down the whole section, however tall it grows.</p>
  </div>
</section>

<script>
  const root = document.documentElement.style;
  const fx = document.getElementById('fx');
  const fy = document.getElementById('fy');
  const readout = document.getElementById('readout');

  function update() {
    root.setProperty('--fx', fx.value + '%');
    root.setProperty('--fy', fy.value + '%');
    readout.textContent = 'background-position: ' + fx.value + '% ' + fy.value + '%';
  }
  fx.addEventListener('input', update);
  fy.addEventListener('input', update);
</script>
</body>
</html>
A full-screen hero with a focus point you can move, then a section with a repeating pattern.

Use min-height, not height. If the text grows longer than the screen on a small phone, the section grows with it instead of letting the text spill out.

background-attachment: fixed pins the image to the window so the content scrolls over it. Mobile browsers may ignore it and scroll the image with the page, so the section must still look right without the effect.

Relative paths in url()

A relative path inside a CSS file starts from the folder of the CSS file, not the HTML page. This is the most common reason a background image works in one place and not another.

url() in css/style.css starts from css/, so it needs ../ to reach the images folder.
url() in css/style.css starts from css/, so it needs ../ to reach the images folder.

Inside a <style> block or a style attribute, the path starts from the HTML page instead. Moving CSS from the page into a separate file can therefore break every image path. Relative vs absolute path explains the rules.

Quote the path. Without quotes, spaces, brackets and quote marks in a file name must be escaped, and a stray space breaks the whole declaration.

If you send the page as a single file, a relative path points to a file the recipient does not have. Base64 images put the picture inside the page, and images not showing in HTML covers the other causes.

Background image or img?

Screen readers do not announce background images, and CSS has no alt text for them. That is correct for decoration and wrong for content.

  • Content (a product photo, a team portrait, a chart): use <img> with alt text.
  • Decoration (a texture, a mood photo behind a heading): a background image is fine.
  • Text inside the picture: put the text in HTML on top of the background instead, so it can be read, searched and translated.

When it does not work

What you see Cause Fix
Nothing at all, no error The element is empty, so its height is 0 Add content, padding or min-height
Works inline, breaks in a CSS file The relative path starts from the CSS file's folder Adjust the path, often with ../
Nothing, file name has spaces or brackets Unquoted url() Wrap the path in quotes
Size or position suddenly back to default A later background shorthand reset them Put the shorthand first, longhands after it
Copies of the photo next to each other Repeat is on by default background-repeat: no-repeat
Important part cut off on phones cover crops, and background-position picks what stays Set background-position to the focus point
Fixed background scrolls on a phone Mobile browsers may ignore fixed Design the section to work without it

The shorthand trap looks like this. background sets every sub-property it does not mention back to its initial value, so here the image ends up at auto size:

.hero {
  background-size: cover;
  background: url("photo.jpg") no-repeat;  /* resets size to auto */
}

Either write the size inside the shorthand, after the position and a slash (center / cover), or put the shorthand first.

A background image depends on where the file sits, which is why a page that looks right on your computer can arrive blank. Pictures drawn as SVG or gradients inside the page travel with it.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can try the controls themselves. If you change the code later, the same link shows the new version.

Questions people ask

Why is my CSS background image not showing?

Check three things in order. The element may have no height, so there is nothing to paint on. The path in url() may be wrong, because a relative path in a stylesheet starts from the CSS file's folder. Or a later background shorthand may have reset the image.

What is the difference between background-size cover and contain?

cover scales the image until it fills the whole box, cropping whatever sticks out. contain scales it until the whole image fits inside the box, leaving empty space where the shapes do not match. Both keep the image's proportions.

How do I make a background image fill the whole screen?

Give a section min-height: 100vh, then set background-size: cover and background-position to the part of the picture that matters. The section is as tall as the screen and the image always covers it.

Should I use a background image or an img tag?

Use img when the picture is content, such as a product photo or a chart, because img takes alt text and screen readers announce it. Use a background image for decoration that could disappear without losing any information.

Why does background-attachment: fixed not work on my phone?

Mobile browsers may ignore fixed backgrounds and scroll the image with the page instead. Treat the fixed effect as an extra for large screens, and make sure the section still looks right when the image scrolls.

Keep reading