CSS QR Code Pattern

A decorative QR-like pattern drawn with CSS box-shadow on a single element, with no images, no SVG and no JavaScript.

Published August 21, 2026 Advanced 7 min read

A CSS QR code is a pixel grid painted entirely from box-shadow. The property takes a comma separated list with no upper limit, and with blur and spread both at zero every entry is an exact copy of the element's own box in a new position. One ten pixel square plus a list of offsets is enough to draw the whole pattern from a single empty <div>.

The catch is that shadows are invisible to layout, so the element still measures ten pixels by ten while painting eighty by eighty. Two further versions follow: a pixel sprite whose whole size is driven by one custom property, and the same kind of grid drawn from tiled background-image layers, which behave completely differently in the box model.

One element, forty seven shadows

purecss.com

One <div>, and every pixel is a CSS box-shadow value

HTML

<div class="qr-stack">
  <div class="qr-wrap">
    <!-- the entire QR grid is this one element -->
    <div class="qr-pixel"></div>
    <span class="qr-label">purecss.com</span>
  </div>
  <p class="qr-caption">One <code>&lt;div&gt;</code>, and every pixel is a CSS <code>box-shadow</code> value</p>
</div>

CSS

.qr-stack {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.5rem;
}

.qr-wrap {
  background: #ffffff;
  border-radius: 12px;
  padding: 1.25rem;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}

.qr-pixel {
  /* Each box-shadow is one 10x10 cell; blur and spread stay 0 for sharp pixels */
  width: 10px;
  height: 10px;
  /* Shadows draw outside the box and add no layout, so reserve the 9x9 grid */
  margin: 0 80px 80px 0;
  background: #1a1a1a;
  border-radius: 0;
  box-shadow:
    /* Finder: top-left 3x3 */
    10px 0   0 0 #1a1a1a, 20px 0   0 0 #1a1a1a,
    0   10px 0 0 #1a1a1a, 10px 10px 0 0 #1a1a1a, 20px 10px 0 0 #1a1a1a,
    0   20px 0 0 #1a1a1a, 10px 20px 0 0 #1a1a1a, 20px 20px 0 0 #1a1a1a,

    /* Finder: top-right 3x3 */
    60px 0   0 0 #1a1a1a, 70px 0   0 0 #1a1a1a, 80px 0   0 0 #1a1a1a,
    60px 10px 0 0 #1a1a1a, 70px 10px 0 0 #1a1a1a, 80px 10px 0 0 #1a1a1a,
    60px 20px 0 0 #1a1a1a, 70px 20px 0 0 #1a1a1a, 80px 20px 0 0 #1a1a1a,

    /* Finder: bottom-left 3x3 */
    0   60px 0 0 #1a1a1a, 10px 60px 0 0 #1a1a1a, 20px 60px 0 0 #1a1a1a,
    0   70px 0 0 #1a1a1a, 10px 70px 0 0 #1a1a1a, 20px 70px 0 0 #1a1a1a,
    0   80px 0 0 #1a1a1a, 10px 80px 0 0 #1a1a1a, 20px 80px 0 0 #1a1a1a,

    /* Data cells */
    40px 10px 0 0 #1a1a1a,
    40px 20px 0 0 #1a1a1a, 50px 20px 0 0 #1a1a1a,
    20px 30px 0 0 #1a1a1a, 40px 30px 0 0 #1a1a1a, 60px 30px 0 0 #1a1a1a, 80px 30px 0 0 #1a1a1a,
    10px 40px 0 0 #1a1a1a, 30px 40px 0 0 #1a1a1a, 50px 40px 0 0 #1a1a1a, 70px 40px 0 0 #1a1a1a,
    0   50px 0 0 #1a1a1a, 20px 50px 0 0 #1a1a1a, 40px 50px 0 0 #1a1a1a, 60px 50px 0 0 #1a1a1a, 80px 50px 0 0 #1a1a1a,
    40px 60px 0 0 #1a1a1a, 60px 60px 0 0 #1a1a1a, 80px 60px 0 0 #1a1a1a,
    60px 70px 0 0 #1a1a1a,
    40px 80px 0 0 #1a1a1a, 60px 80px 0 0 #1a1a1a, 80px 80px 0 0 #1a1a1a;
}

.qr-label {
  font-family: "DM Mono", "Fira Code", Consolas, monospace;
  font-size: .65rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: #666;
}

.qr-caption {
  font-family: "DM Mono", "Fira Code", Consolas, monospace;
  font-size: .7rem;
  color: #88888f;
  letter-spacing: .04em;
  text-align: center;
  margin: 0;
}

Other ways to build it

A sprite that scales from one value

Writing offsets as multiples of a custom property makes the whole drawing resizable by changing a single declaration. All three of these share one shadow list. Only --px differs, and it drives the cell size, every offset, and the margin that reserves the area. The one on the right also sets border-radius: 50%, which every shadow picks up at once, so the same forty six cells become forty six dots.

HTML

<div class="qr-sprite" style="--px: 5px"></div>
<div class="qr-sprite" style="--px: 8px"></div>
<div class="qr-sprite qr-sprite-round" style="--px: 8px"></div>

CSS

.qr-sprite {
  --px: 8px;
  --pxc: #b8ff57;
  /* one column step per name, so an offset is a name and nothing else */
  --p1:  calc(var(--px) * 1);
  --p2:  calc(var(--px) * 2);
  --p3:  calc(var(--px) * 3);
  --p4:  calc(var(--px) * 4);
  --p5:  calc(var(--px) * 5);
  --p6:  calc(var(--px) * 6);
  --p7:  calc(var(--px) * 7);
  --p8:  calc(var(--px) * 8);
  --p9:  calc(var(--px) * 9);
  --p10: calc(var(--px) * 10);

  width: var(--px);
  height: var(--px);
  /* the sprite is 11 cells wide and 8 tall, and none of that is layout,
     so the margin has to hold the area open by hand */
  margin: 0 var(--p10) var(--p7) 0;
  /* cell 0,0 is empty in this drawing, so the source box paints nothing */
  background: transparent;
  border-radius: 0;
  box-shadow:
    var(--p2) 0 0 0 var(--pxc), var(--p8) 0 0 0 var(--pxc),
    var(--p3) var(--p1) 0 0 var(--pxc), var(--p7) var(--p1) 0 0 var(--pxc),
    var(--p2) var(--p2) 0 0 var(--pxc), var(--p3) var(--p2) 0 0 var(--pxc),
    var(--p4) var(--p2) 0 0 var(--pxc), var(--p5) var(--p2) 0 0 var(--pxc),
    var(--p6) var(--p2) 0 0 var(--pxc), var(--p7) var(--p2) 0 0 var(--pxc),
    var(--p8) var(--p2) 0 0 var(--pxc),
    var(--p1) var(--p3) 0 0 var(--pxc), var(--p2) var(--p3) 0 0 var(--pxc),
    var(--p4) var(--p3) 0 0 var(--pxc), var(--p5) var(--p3) 0 0 var(--pxc),
    var(--p6) var(--p3) 0 0 var(--pxc), var(--p8) var(--p3) 0 0 var(--pxc),
    var(--p9) var(--p3) 0 0 var(--pxc),
    0 var(--p4) 0 0 var(--pxc), var(--p1) var(--p4) 0 0 var(--pxc),
    var(--p2) var(--p4) 0 0 var(--pxc), var(--p3) var(--p4) 0 0 var(--pxc),
    var(--p4) var(--p4) 0 0 var(--pxc), var(--p5) var(--p4) 0 0 var(--pxc),
    var(--p6) var(--p4) 0 0 var(--pxc), var(--p7) var(--p4) 0 0 var(--pxc),
    var(--p8) var(--p4) 0 0 var(--pxc), var(--p9) var(--p4) 0 0 var(--pxc),
    var(--p10) var(--p4) 0 0 var(--pxc),
    0 var(--p5) 0 0 var(--pxc), var(--p2) var(--p5) 0 0 var(--pxc),
    var(--p3) var(--p5) 0 0 var(--pxc), var(--p4) var(--p5) 0 0 var(--pxc),
    var(--p5) var(--p5) 0 0 var(--pxc), var(--p6) var(--p5) 0 0 var(--pxc),
    var(--p7) var(--p5) 0 0 var(--pxc), var(--p8) var(--p5) 0 0 var(--pxc),
    var(--p10) var(--p5) 0 0 var(--pxc),
    0 var(--p6) 0 0 var(--pxc), var(--p2) var(--p6) 0 0 var(--pxc),
    var(--p8) var(--p6) 0 0 var(--pxc), var(--p10) var(--p6) 0 0 var(--pxc),
    var(--p3) var(--p7) 0 0 var(--pxc), var(--p4) var(--p7) 0 0 var(--pxc),
    var(--p6) var(--p7) 0 0 var(--pxc), var(--p7) var(--p7) 0 0 var(--pxc);
}

/* one property, forty six round cells */
.qr-sprite-round {
  border-radius: 50%;
}

The same idea with background layers

Backgrounds draw inside the element's own box, so this version needs no margin trick and cannot overflow. Three finder marks are stacked from pairs of solid color gradients, and the data field is a single tiled dot that covers whatever area the element happens to have. Earlier layers paint on top of later ones, which is why the finder marks sit over the tile rather than under it. The trade off is that a tile repeats by definition, so an arbitrary non repeating pattern still wants shadows.

drawn from 10 layers

HTML

<div class="qr-stack">
  <div class="qr-wrap">
    <div class="qr-grad"></div>
    <span class="qr-label">drawn from 10 layers</span>
  </div>
</div>

CSS

.qr-grad {
  width: 168px;
  height: 168px;
  background-color: #ffffff;
  /* first listed paints on top: cores, then rings, then the data tile */
  background-image:
    linear-gradient(#1a1a1a, #1a1a1a),
    linear-gradient(#1a1a1a, #1a1a1a),
    linear-gradient(#1a1a1a, #1a1a1a),
    linear-gradient(#ffffff, #ffffff),
    linear-gradient(#ffffff, #ffffff),
    linear-gradient(#ffffff, #ffffff),
    linear-gradient(#1a1a1a, #1a1a1a),
    linear-gradient(#1a1a1a, #1a1a1a),
    linear-gradient(#1a1a1a, #1a1a1a),
    radial-gradient(#1a1a1a 38%, transparent 40%);
  background-repeat:
    no-repeat, no-repeat, no-repeat,
    no-repeat, no-repeat, no-repeat,
    no-repeat, no-repeat, no-repeat, repeat;
  background-size:
    18px 18px, 18px 18px, 18px 18px,
    30px 30px, 30px 30px, 30px 30px,
    42px 42px, 42px 42px, 42px 42px, 14px 14px;
  background-position:
    12px 12px, calc(100% - 12px) 12px, 12px calc(100% - 12px),
    6px 6px,   calc(100% - 6px) 6px,   6px calc(100% - 6px),
    0 0,       100% 0,                 0 100%,
    0 0;
}

How it works

box-shadow accepts an unlimited list of shadows, each an independent colored rectangle offset from the element. With blur: 0 and spread: 0, each shadow is exactly the same size as the element, which makes it a sharp square cell. A 10px element at the top left plus shadows at multiples of 10px paints a contiguous grid with one element and no images.

Each shadow in the list is drawn as the element's border box shape, offset by the first two lengths, resized by the spread, and softened by the blur. Set blur and spread to zero and the copy is pixel identical to the source, so the offsets alone decide where cells land. Because the offsets here are multiples of the element's own 10px size, the cells touch edge to edge and the pattern reads as one solid image rather than a scatter of squares.

The part that catches people out is that box-shadow contributes nothing to layout. It is painted after the box model has already decided how much room the element needs, and the box model only ever counted the original ten by ten square. The pattern therefore spills out of its container, overlaps whatever follows it, and gets clipped by the white card behind it. The fix in this stylesheet is margin: 0 80px 80px 0, which reserves exactly the area the shadows will occupy. Without those two values the grid overflows and the card cuts it in half.

Shadows inherit the element's border-radius, all of them, which is worth knowing before reusing the technique. Set border-radius: 50% on the source element and every shadow becomes a circle, which is how the star field in CSS geometric art is drawn from two dots and a dozen offsets. Here the radius is pinned to 0 so the cells stay square and meet cleanly.

One element is not the same as one paint operation. Forty seven shadows means forty seven rasterised rectangles every time the element repaints, and the cost grows linearly with the list. For a fixed decorative motif that never animates this is irrelevant. For anything that moves, or for a pattern with several hundred cells, an SVG or a single image file is the right answer, because it is one draw instead of hundreds.

The last point is the important one: this is decoration that looks like a QR code, not a QR code. Real codes carry error correction, alignment patterns and a specific data encoding, none of which are here. Nothing will scan it. When a page needs a code that actually resolves, generate a real one and ship it as an image or inline SVG with a text alternative.

CSS properties used

box-shadow
Takes an unlimited comma separated list. Each entry is offset-x offset-y blur spread color, and every one paints another copy of the element's box shape.
margin
The only way to reserve space for shadows, since they add nothing to the element's own size. Here it holds open the area the grid will paint into.
border-radius
Applies to every shadow as well as the element. 0 keeps the cells square, 50% turns the whole grid into a field of dots.
background
Fills the source cell itself. Without it the top left square of the pattern is missing while all the shadows are present.
background-image
The alternative drawing surface. Unlike shadows, tiled gradient layers stay inside the element's own box and need no margin reservation.

Browser support

FeatureChromeFirefoxSafariEdge
box-shadow43.5512
border-radius433.112
multiple backgrounds43.63.112
gradients103615.412
custom properties49311016

Figures come from Can I Use for CSS Box-shadow, Border-radius, Multiple backgrounds, CSS Gradients and CSS Variables. Nothing on this page needs a recent browser. The gradient row reads later than the others because Can I Use only counts a browser as fully supporting gradients once the whole modern syntax is in place, and the simple two stop forms used here have worked for far longer than that figure suggests.

Accessibility notes

The pattern is decoration, so it should be hidden from assistive technology with aria-hidden="true" on the element that carries it. An empty <div> with no role announces nothing useful in the first place, and leaving it exposed only adds noise to a screen reader's pass through the page.

Never label a drawn pattern as a scannable code. A reader who is told there is a QR code will try to scan it, and nothing will happen. If the page needs a working code, render a real one to an image or SVG and give it alternative text that carries the destination in words, such as "QR code linking to the ticket page", plus the same link as plain text nearby.

Because the grid is painted rather than drawn from content, it survives page zoom without pixelating but it does not respond to a user stylesheet or a forced color mode the way an image with alternative text would. Any information the pattern is carrying visually has to exist somewhere in the text as well, not only in the pixels.

What you can build with it

  • Ticket and badge mockups. Design comps and print previews where a code shaped block is needed for layout but the real code comes later.
  • Loading placeholders. A stand in for a code that is generated on the server, in the same style as a CSS skeleton loader.
  • Decorative pixel motifs. Logos, icons and sprites drawn from a single element, which keeps them in the stylesheet rather than in the asset pipeline.
  • Email safe patterns. Some clients strip images but keep inline styles, so a shadow drawn motif renders where an image would show a broken frame.
  • Print styling. A pattern that is part of the stylesheet rather than a remote asset, so it does not depend on image loading rules in a print context.

Mistakes worth avoiding

  • Forgetting that shadows add nothing to layout. The grid paints outside the element's box, overflows its container, and is clipped or overlapped, and the element still reports ten pixels square in the inspector.
  • Leaving blur or spread at a non zero value. The cells get soft edges and bleed into each other, so the pattern turns into a gray smear instead of a grid.
  • Rounding the source element. border-radius applies to every shadow at once, so one property turns the entire grid into circles.
  • Choosing offsets that are not multiples of the element size. Cells either overlap or leave hairlines between them, and the hairlines only appear at certain zoom levels.
  • Presenting the result as a working QR code. It carries no data and no error correction, so no scanner will read it.

Frequently asked questions

Can a CSS QR code actually be scanned?
No. The pattern on this page is a decorative arrangement of squares with no data encoding, no error correction and no alignment patterns. Scanners will find nothing to decode. Generate a real code server side and serve it as an image or inline SVG when it needs to work.
Why does my box-shadow pattern overflow its container?
Because shadows are painted outside the border box and contribute nothing to layout. The element is still only as large as its width and height, so the browser reserves no room for the shadows. Add margin on the right and bottom equal to the largest offset in the list.
How many box-shadows can one element have?
There is no limit in the specification. The practical limit is paint cost, since every entry is another rectangle rasterised on each repaint. A few dozen is unnoticeable. Several hundred on an element that animates will show up as dropped frames.
Is box-shadow or background-image better for drawing patterns?
Shadows suit an arbitrary, non repeating arrangement, because every cell can go anywhere. Tiled gradients suit a motif that repeats, because one declaration covers the whole surface no matter how large it gets. Gradients also stay inside the element's box, so they need no margin reservation.
How do I resize a box-shadow pixel grid?
Write the offsets as calc() expressions over a custom property, then change the one value. The sprite variant on this page sets --px on the element and every offset is a multiple of it, so the whole drawing scales from a single declaration.