div vs span: which one to use, and why

Both are empty wrappers with no meaning. A div makes a block that takes its own line, a span makes an inline box that flows inside a line. Before either, check whether a real element fits.

A <div> and a <span> do the same job at different sizes: each wraps content so CSS and JavaScript can reach it, and neither adds meaning.

The difference is the box. A div is block: it starts on a new line and fills its parent's width. A span is inline: it flows inside a line of text.

Press the button to see the boxes the browser draws for each one.

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>div vs span: layout boxes</title>
<style>
  body { margin: 0; padding: 16px; font: 16px/1.6 system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .stage { position: relative; max-width: 420px; padding: 14px 16px; background: #fff; border-radius: 10px; }
  .stage p { margin: 0 0 10px; }
  .hi { background: #fef3c7; }
  .note { padding: 8px 10px; background: #e0f2fe; border-radius: 6px; }
  button { font: inherit; padding: 8px 14px; border: 0; border-radius: 8px; background: #1d2330; color: #fff; cursor: pointer; margin-bottom: 12px; }
  /* the overlay layer that draws each layout box */
  .layer { position: absolute; inset: 0; pointer-events: none; }
  .box { position: absolute; outline: 2px solid; }
  .box.blk { outline-color: #2563eb; }
  .box.inl { outline-color: #ea580c; outline-style: dashed; }
  .box span { position: absolute; top: -18px; left: -2px; font: 700 11px/16px system-ui; padding: 0 5px; color: #fff; border-radius: 4px 4px 0 0; }
  .blk span { background: #2563eb; } .inl span { background: #ea580c; }
  #out { font-size: 14px; color: #4b5563; margin-top: 12px; }
</style>
</head>
<body>
<button id="toggle" aria-pressed="false">Show boxes</button>
<div class="stage" id="stage">
  <p>We ship <span class="hi" data-box>every order placed before noon on the same working day</span>, and returns are free.</p>
  <div class="note" data-box>A div starts on a new line and fills the width.</div>
  <p style="margin:10px 0 0">Text after the div starts on a new line too.</p>
  <div class="layer" id="layer"></div>
</div>
<div id="out"></div>

<script>
  const stage = document.getElementById('stage');
  const layer = document.getElementById('layer');
  const out = document.getElementById('out');
  const btn = document.getElementById('toggle');
  let on = false;

  function draw() {
    layer.innerHTML = '';
    out.textContent = '';
    if (!on) return;
    const base = stage.getBoundingClientRect();
    const lines = [];
    stage.querySelectorAll('[data-box]').forEach((el) => {
      const display = getComputedStyle(el).display;
      const rects = el.getClientRects();  // one rect per line for inline, one for block
      [...rects].forEach((r, i) => {
        const b = document.createElement('div');
        b.className = 'box ' + (display === 'inline' ? 'inl' : 'blk');
        b.style.cssText = `left:${r.left - base.left}px;top:${r.top - base.top}px;width:${r.width}px;height:${r.height}px`;
        if (i === 0) b.innerHTML = `<span>${el.tagName.toLowerCase()}</span>`;
        layer.append(b);
      });
      lines.push(`${el.tagName.toLowerCase()}: display ${display}, ${rects.length} box(es)`);
    });
    out.textContent = lines.join(' | ');
  }

  btn.addEventListener('click', () => {
    on = !on;
    btn.textContent = on ? 'Hide boxes' : 'Show boxes';
    btn.setAttribute('aria-pressed', on);
    draw();
  });
  addEventListener('resize', draw);
</script>
</body>
</html>
A span and a div in the same paragraph. The dashed outlines are the span's boxes, the solid one is the div's.

The span gets two boxes because its words wrap onto a second line. The div gets one box as wide as its container, even though its text is short.

Block vs inline: what the boxes show

The browser turns every element into one or more layout boxes. What kind of box depends on the element's display value, and the two tags start with different defaults.

A div makes one full-width box. A span makes one box per line it touches.
A div makes one full-width box. A span makes one box per line it touches.
<div> <span>
Default display block inline
Starts a new line Yes No
Width Fills the parent As wide as its words
Boxes when text wraps One One per line
width and height Apply Ignored
Meaning (role) None (generic) None (generic)
May contain Blocks and inline content Text and inline elements only

In JavaScript, el.getClientRects() returns those boxes. For the wrapped span above it returns two rectangles; for the div, one. The demos on this page draw their outlines from that call.

Check for a meaningful element first

A "div or span?" question often has a third answer: neither. HTML has elements that say what the content is, and they come with behaviour you would otherwise rebuild by hand.

Three questions in order. Only reach for div or span when no element with meaning fits.
Three questions in order. Only reach for div or span when no element with meaning fits.
If it is... Use Not
A block of text <p> <div>
A heading <h2>, <h3> a bold <div>
Something to click that does an action <button> a <div> with a click listener
A menu of links <nav> <div class="menu">
A self-contained card or post <article> <div class="card">
Important or stressed words <strong>, <em> a styled <span>
A date or time <time> <span class="date">

The button row matters most. A div with a click listener cannot be reached with the Tab key, and Enter and Space do not press it. A real <button> does all of that without extra code. Semantic HTML walks through the full list of substitutions.

What is left after this check is pure grouping: a wrapper for a grid, a box to center, a price to color, a number a script updates. That is where div and span belong.

When a span is right, and when a div is

Use a span for a few words inside a line of text:

  • a price or a unit you want to color
  • a small badge next to a title
  • a number a script rewrites, such as a cart count
  • a word wrapped so a script can animate it

Use a div for a wrapper around blocks:

  • the container that gets display: grid or display: flex
  • a box you need to center
  • a group that shares a class or a background
  • the mount point a script fills in

The HTML span tag article covers the span side in more depth, including why width does nothing on it: the HTML span tag.

Changing display does not change the tag

CSS can give any element any box. A span with display: block takes its own line and fills the width. A div with display: inline flows in the text and ignores width. Try every combination:

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>div vs span: change display</title>
<style>
  body { margin: 0; padding: 16px; font: 16px/1.6 system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .controls { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; }
  label { font-size: 14px; }
  select { font: inherit; padding: 4px 6px; margin-left: 4px; }
  .stage { position: relative; max-width: 420px; padding: 14px 16px; background: #fff; border-radius: 10px; }
  /* the same styles on whichever element is chosen */
  #target { width: 150px; height: 60px; padding: 6px 10px; background: #fef3c7; border: 1px solid #f59e0b; }
  .layer { position: absolute; inset: 0; pointer-events: none; }
  .box { position: absolute; outline: 2px dashed #ea580c; }
  table { margin-top: 12px; border-collapse: collapse; font-size: 14px; background: #fff; }
  td { padding: 4px 10px; border: 1px solid #e1e4ea; }
  td:first-child { color: #6b7280; }
</style>
</head>
<body>
<div class="controls">
  <label>Element <select id="tag"><option>span</option><option>div</option></select></label>
  <label>display <select id="disp">
    <option value="">(default)</option><option>inline</option><option>block</option><option>inline-block</option>
  </select></label>
</div>
<div class="stage" id="stage">
  <div id="line">Text before <span id="target">the chosen element</span> and text after it in the same line.</div>
  <div class="layer" id="layer"></div>
</div>
<table>
  <tr><td>Tag</td><td id="rTag"></td></tr>
  <tr><td>Computed display</td><td id="rDisp"></td></tr>
  <tr><td>Layout boxes</td><td id="rBoxes"></td></tr>
  <tr><td>Measured width</td><td id="rW"></td></tr>
</table>

<script>
  const $ = (id) => document.getElementById(id);
  const stage = $('stage'), layer = $('layer');

  function render() {
    // rebuild the element with the chosen tag, keep its id, text and styles
    const old = $('target');
    const el = document.createElement($('tag').value);
    el.id = 'target';
    el.textContent = old.textContent;
    el.style.display = $('disp').value;  // empty string = the tag's default
    old.replaceWith(el);

    // outline every box the element produced
    layer.innerHTML = '';
    const base = stage.getBoundingClientRect();
    const rects = [...el.getClientRects()];
    rects.forEach((r) => {
      const b = document.createElement('div');
      b.className = 'box';
      b.style.cssText = `left:${r.left - base.left}px;top:${r.top - base.top}px;width:${r.width}px;height:${r.height}px`;
      layer.append(b);
    });

    $('rTag').textContent = '<' + el.tagName.toLowerCase() + '>';
    $('rDisp').textContent = getComputedStyle(el).display;
    $('rBoxes').textContent = rects.length;
    const w = Math.round(el.getBoundingClientRect().width);
    // 150px width + 20px padding + 2px border = 172px when width applies
    $('rW').textContent = w + 'px' + (w === 172 ? ' (width: 150px applied)' : ' (width ignored, sized by the text)');
  }

  $('tag').addEventListener('change', render);
  $('disp').addEventListener('change', render);
  addEventListener('resize', render);
  render();
</script>
</body>
</html>
Pick the tag and the display value. The measured width shows whether width: 150px was applied.

Two things stay with the tag, whatever the display value:

  1. What it may contain. A span with display: block should still hold only text and inline elements. The content rules come from the tag, not the CSS.
  2. Where it may go. A div with display: inline is still a div, so it still may not sit inside a paragraph.

So pick the tag for what the content is and where it sits in the markup, then use CSS for the box.

inline-block is the common middle ground: it stays in the line and accepts width, height and vertical padding. CSS display covers every value, and the CSS box model explains how padding and borders add to the width.

Nesting rules that bite

A div inside a paragraph is the classic mistake, and the browser fixes it in a way that surprises people. A paragraph cannot contain a block like a div, so the HTML parser closes the paragraph as soon as it meets the <div>:

The parser closes the p before the div. The text after it lands outside any paragraph.
The parser closes the p before the div. The text after it lands outside any paragraph.

Written:

<p>Intro <div>Box</div> more</p>

What the page actually contains:

<p>Intro </p><div>Box</div> more<p></p>

The stray </p> at the end becomes a new, empty paragraph. Styles meant for the paragraph now miss " more", and a script looking for the div inside the p finds nothing.

Other rules worth knowing:

  • div inside span: not valid. The parser keeps it where you wrote it, but the span's inline box is split around the block, and a validator flags it. Make the outer element a div.
  • div inside a button or a heading: not valid either. Both take inline content only. Use a span inside them.
  • div inside a link: allowed. A link takes the content model of its parent, so a whole card can be one link.
  • span inside anything that holds text: fine.

A finished example: a card built the right way

This card uses each element for what it is. The grid wrapper is a div, because it is pure layout.

Each card is an <article>, the title an <h3>, the text a <p>, the features a list, and the action a <button>. Spans appear only for the price, the badge and the count.

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>div vs span: a finished card</title>
<style>
  body { margin: 0; padding: 16px; font: 15px/1.5 system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  button { font: inherit; cursor: pointer; }
  #toggle { padding: 8px 14px; border: 0; border-radius: 8px; background: #1d2330; color: #fff; margin-bottom: 14px; }
  #page { position: relative; }
  /* div: pure layout, no meaning needed */
  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }
  .card { background: #fff; border-radius: 12px; padding: 14px 16px; }
  .card h3 { margin: 0 0 6px; font-size: 17px; }
  .card p { margin: 0 0 8px; }
  .card ul { margin: 0 0 10px; padding-left: 18px; }
  /* span: a styling hook inside a line */
  .price { font-weight: 700; color: #0f766e; }
  .badge { display: inline-block; padding: 0 8px; border-radius: 99px; background: #dbeafe; font-size: 13px; }
  .buy { padding: 7px 12px; border: 1px solid #1d2330; border-radius: 8px; background: #fff; }
  .count { display: inline-block; min-width: 20px; margin-left: 6px; border-radius: 99px; background: #1d2330; color: #fff; font-size: 12px; }
  .layer { position: absolute; inset: 0; pointer-events: none; }
  .box { position: absolute; outline: 1.5px solid #2563eb; }
  .box.inl { outline: 1.5px dashed #ea580c; }
  .box i { position: absolute; top: -1px; right: -1px; font: 700 10px/14px system-ui; font-style: normal; padding: 0 4px; color: #fff; background: #2563eb; }
  .box.inl i { background: #ea580c; top: -15px; left: -1px; right: auto; }
  .show .card p { line-height: 2.4; }  /* room for the labels while they are shown */
</style>
</head>
<body>
<button id="toggle" aria-pressed="false">Show tags</button>
<div id="page">
  <div class="grid">
    <article class="card">
      <h3>Desk lamp</h3>
      <p>Now <span class="price">$39</span> <span class="badge">new</span>, ships in <strong>2 days</strong>.</p>
      <ul><li>Warm and cool light</li><li>USB-C power</li></ul>
      <button class="buy">Add to cart<span class="count">0</span></button>
    </article>
    <article class="card">
      <h3>Wall clock</h3>
      <p>Now <span class="price">$24</span>, <em>silent</em> movement.</p>
      <ul><li>30 cm face</li><li>One AA battery</li></ul>
      <button class="buy">Add to cart<span class="count">0</span></button>
    </article>
  </div>
  <div class="layer" id="layer"></div>
</div>

<script>
  const page = document.getElementById('page');
  const layer = document.getElementById('layer');
  const btn = document.getElementById('toggle');
  let on = false;

  // label every element with its tag: solid = block-level box, dashed = inline
  function draw() {
    layer.innerHTML = '';
    if (!on) return;
    const base = page.getBoundingClientRect();
    page.querySelectorAll('.grid, .grid *').forEach((el) => {
      const inline = getComputedStyle(el).display === 'inline';
      [...el.getClientRects()].forEach((r, i) => {
        const b = document.createElement('div');
        b.className = 'box' + (inline ? ' inl' : '');
        b.style.cssText = `left:${r.left - base.left}px;top:${r.top - base.top}px;width:${r.width}px;height:${r.height}px`;
        if (i === 0) b.innerHTML = `<i>${el.tagName.toLowerCase()}</i>`;
        layer.append(b);
      });
    });
  }

  btn.addEventListener('click', () => {
    on = !on;
    btn.textContent = on ? 'Hide tags' : 'Show tags';
    btn.setAttribute('aria-pressed', on);
    page.classList.toggle('show', on);
    draw();
  });

  // the buttons work too: the span inside each one holds the count
  page.querySelectorAll('.buy').forEach((b) => {
    b.addEventListener('click', () => {
      const c = b.querySelector('.count');
      c.textContent = Number(c.textContent) + 1;
      draw();
    });
  });
  addEventListener('resize', draw);
</script>
</body>
</html>
Press Show tags to label every element. Solid outlines are block-level boxes, dashed ones are inline. The Add to cart buttons work.
<div class="grid">
  <article class="card">
    <h3>Desk lamp</h3>
    <p>Now <span class="price">$39</span>, ships in <strong>2 days</strong>.</p>
    <button>Add to cart<span class="count">0</span></button>
  </article>
</div>

Inside the grid, the only div is the grid itself, and the spans are three small hooks per card. Everything else says what it is, so no ARIA attributes are needed to describe it.

When it does not work

What you see Cause Fix
width or height on a span does nothing Inline boxes ignore them display: inline-block or block
A background on a span has ragged, broken edges The span wrapped into several line boxes Expected; use inline-block to keep one box
A div inside a p leaves text unstyled The parser closed the p before the div Use a span inside, or make the outer element a div
An empty paragraph appears after your text A stray </p> after a block inside a p Move the block out of the paragraph
Two spans sit side by side, but two divs stack Default display: inline vs block Set display on the parent (flex or grid)
A clickable div cannot be reached with Tab A div is not focusable and has no key handling Use a <button>

Layout boxes are easier to show than to explain. A screenshot of the demo cannot be toggled, and an .html attachment may open as plain code on a phone.

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 press the buttons and see the boxes themselves. If you change the code later, the same link shows the new version.

Questions people ask

What is the main difference between div and span?

Their default display. A div is block: it starts on a new line and fills the width of its parent. A span is inline: it sits inside a line of text, is as wide as its words, and splits into one box per line when the text wraps.

Can I make a span act like a div with display: block?

Yes. display: block gives the span a full-width block box, and width and height start to apply. It is still a span, so the content rules do not change: it should hold text and inline elements only.

Can I put a div inside a span, or inside a p?

Neither is valid HTML. Inside a span the parser keeps the div where you wrote it, but the markup fails validation. Inside a p it is worse: the parser closes the paragraph before the div, so the text after it ends up outside any paragraph.

Are div and span bad for accessibility?

They are neutral: both have the generic role and tell assistive technology nothing. The problem starts when a div stands in for something with meaning, such as a button or a navigation menu. Use the real element there.

Which one is better for SEO?

Neither. Search engines read the text either way. Meaningful elements such as headings, paragraphs, lists and links describe the page structure; div and span only group things for CSS and JavaScript.

Keep reading