The HTML label: connect the words to the control

A <label> ties a piece of text to one form control. Clicking the text then works the control, and screen readers read the text as the control's name.

The HTML <label> tag connects a piece of text to one form control. Set for to the control's id, or put the control inside the label.

After that, clicking the text works the control, and screen readers read the text as the control's name.

Try it. Click the words on each side, not the boxes.

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>Label click test</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }
  .box { background: #fff; border-radius: 12px; padding: 14px; border: 2px solid #cfe9d7; }
  .box.no { border-color: #f3d1c8; }
  h2 { font-size: 13px; margin: 0 0 12px; text-transform: uppercase; letter-spacing: .4px; }
  .row { margin-bottom: 14px; }
  label, .fake { cursor: pointer; }
  input[type="text"] { display: block; width: 100%; box-sizing: border-box; margin-top: 4px; padding: 7px 9px; font: inherit; }
  #log { margin-top: 12px; padding: 10px 12px; border-radius: 10px; background: #1d2330; color: #e5e7eb; font: 13px/1.5 ui-monospace, Consolas, monospace; }
</style>
</head>
<body>
<div class="grid">
  <div class="box">
    <h2>Connected</h2>
    <!-- for="..." matches the id of the control -->
    <div class="row">
      <input type="checkbox" id="news-a">
      <label for="news-a">Send me the newsletter</label>
    </div>
    <div class="row">
      <label for="city-a">City</label>
      <input type="text" id="city-a">
    </div>
  </div>
  <div class="box no">
    <h2>Not connected</h2>
    <!-- plain text beside the control: nothing links them -->
    <div class="row">
      <input type="checkbox" id="news-b">
      <span class="fake">Send me the newsletter</span>
    </div>
    <div class="row">
      <span class="fake">City</span>
      <input type="text" id="city-b">
    </div>
  </div>
</div>
<div id="log" aria-live="polite">Click the words, not the boxes.</div>

<script>
  const log = document.getElementById('log');
  // report what each control did, so a click on the text is visible
  document.querySelectorAll('input').forEach((el) => {
    const side = el.id.endsWith('-a') ? 'Connected' : 'Not connected';
    el.addEventListener('change', () => {
      if (el.type === 'checkbox') log.textContent = side + ': checkbox is now ' + (el.checked ? 'ON' : 'OFF');
    });
    el.addEventListener('focus', () => {
      if (el.type === 'text') log.textContent = side + ': the City box has focus';
    });
  });
  document.querySelectorAll('.fake').forEach((t) => {
    t.addEventListener('click', () => { log.textContent = 'Not connected: you clicked text, the control did nothing'; });
  });
</script>
</body>
</html>
Left: real labels. Right: the same text in a span. Only the left side reacts to a click on the words.

On the left, a click on "Send me the newsletter" toggles the checkbox and a click on "City" puts the cursor in the box. On the right the text looks the same, but nothing links it to the control.

Two ways to connect a label

There are two ways, and both are standard HTML. Pick one per control.

for and id point across the page. Wrapping puts the control inside the label.
for and id point across the page. Wrapping puts the control inside the label.
  1. for and id: give the control an id and put the same value in the label's for. The two elements can sit in different places in the layout, such as table cells or grid columns.
  2. Wrapping: put the control inside the <label>. No id is needed, which helps when the same snippet is repeated many times on a page.
<!-- 1. for + id -->
<input type="checkbox" id="news">
<label for="news">Send me the newsletter</label>

<!-- 2. wrapping -->
<label>
  <input type="checkbox" name="news"> Send me the newsletter
</label>

You can also do both at once: a wrapping label with a for that matches the wrapped control. If for names a different control, for wins.

What a connected label does

A connected label changes three things, and none of them need JavaScript.

Control Clicking the label What a screen reader hears
Checkbox Toggles it "Send me the newsletter, checkbox, not checked"
Radio button Selects it The label text, then "radio button" and its state
Text box, select, textarea Moves focus into it The label text, then the kind of field

The exact wording a screen reader uses differs between readers. The part that matters is that the label text is the control's accessible name, the name assistive technology uses for it. Without a label, the checkbox is announced with no name at all.

The same name is used by voice control software, so a user can say the label text to work the control. aria-label covers naming things that have no visible text. For form controls with visible text, a real label comes first.

A bigger click target for checkboxes and radios

A default checkbox is about the size of one letter. On a phone, a finger has to land on that square exactly. A connected label adds the text to the target.

Without a label, only the square counts. With one, the whole line does.
Without a label, only the square counts. With one, the whole line does.

Wrapping the checkbox and making the label a flex row turns the whole line into one target. Add padding and the target grows without making the box itself bigger:

label.option {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 0;
  cursor: pointer;
}

This matters most for groups of radio buttons. Put each radio in its own label, and group the set with a fieldset and legend so the question itself is announced too.

Laying out labels with CSS

A label is an inline element by default, so it sits on the same line as whatever comes next. Four layouts cover most forms.

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>Label layouts</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; }
  section { background: #fff; border-radius: 12px; padding: 14px; }
  h2 { font-size: 12px; margin: 0 0 10px; color: #6b7280; text-transform: uppercase; letter-spacing: .4px; }
  input[type="text"], input[type="search"], input[type="email"] {
    width: 100%; box-sizing: border-box; padding: 8px 10px; font: inherit;
    border: 1px solid #c7ccd4; border-radius: 8px;
  }

  /* 1. Stacked: the label is a block above the box */
  .stacked label { display: block; font-weight: 600; margin-bottom: 4px; }

  /* 2. Inline: the control sits inside the label, so the whole line is clickable */
  .inline label { display: flex; align-items: center; gap: 8px; padding: 6px 0; cursor: pointer; }
  .inline input { width: 18px; height: 18px; margin: 0; }

  /* 3. Floating: placeholder=" " lets :placeholder-shown tell empty from filled */
  .float { position: relative; }
  .float input { padding: 18px 10px 6px; }
  .float label {
    position: absolute; left: 11px; top: 12px; color: #6b7280;
    pointer-events: none; transition: .15s;
  }
  .float input:focus + label,
  .float input:not(:placeholder-shown) + label { top: 4px; font-size: 11px; color: #2563eb; }

  /* 4. Visually hidden: invisible on screen, still read by screen readers */
  .visually-hidden {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }
  .search { display: flex; gap: 6px; }
  .search button { padding: 0 14px; border: 0; border-radius: 8px; background: #2563eb; color: #fff; font: inherit; }
</style>
</head>
<body>
<div class="grid">
  <section class="stacked">
    <h2>1. Stacked</h2>
    <label for="full-name">Full name</label>
    <input type="text" id="full-name" autocomplete="name">
  </section>

  <section class="inline">
    <h2>2. Inline (checkbox, radio)</h2>
    <label><input type="checkbox" name="terms"> I agree to the terms</label>
    <label><input type="radio" name="plan" value="month" checked> Monthly</label>
    <label><input type="radio" name="plan" value="year"> Yearly</label>
  </section>

  <section>
    <h2>3. Floating label</h2>
    <div class="float">
      <input type="email" id="mail" placeholder=" ">
      <label for="mail">Email</label>
    </div>
  </section>

  <section>
    <h2>4. Visually hidden (search box)</h2>
    <form class="search" role="search">
      <label for="q" class="visually-hidden">Search the docs</label>
      <input type="search" id="q" placeholder="Search">
      <button type="submit">Go</button>
    </form>
  </section>
</div>

<script>
  // keep the example on this page when the search form is sent
  document.querySelector('.search').addEventListener('submit', (e) => e.preventDefault());
</script>
</body>
</html>
Stacked, inline, floating and visually hidden. Click each label to check it still works the control.
  • Stacked: display: block on the label puts it above the text box. It is the easiest to read on a narrow screen.
  • Inline: the checkbox or radio sits inside the label, before the text. Align the two with flexbox.
  • Floating: the label sits inside the box and moves up when the box has focus or text. The text input guide explains the :placeholder-shown trick behind it.
  • Visually hidden: the label is invisible on screen but still in the page, for a search box whose purpose is clear from the button next to it.

The visually hidden class is worth keeping in your stylesheet:

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

Do not use display: none or the hidden attribute for this. Both remove the label for screen readers as well.

A placeholder is not a label

A placeholder only shows while the box is empty. Once the user types, the name of the field is gone, and on a long form they have to delete their text to see what the box was for.

Clicking a placeholder is not a label click either. It lands on the box itself. Keep the name in a <label> and use the placeholder for an example, such as the format of a date.

One label, one control

A label is connected to exactly one control. If a label wraps two inputs, only the first one is connected. The second gets no label and no name.

The other direction is allowed: several labels can point at the same id. Clicking any of them works the control, and the accessible name joins the texts in page order.

That can produce an awkward name, so use it on purpose, for example for a unit such as "kg" after a number box.

Do not put a button or a second input inside a label. A label may only hold its own control.

A link is allowed. In "I agree to the terms", clicking the link opens it and does not toggle the box. Keep the link short so most of the line still toggles.

A finished example: toggle switches

A toggle switch is a checkbox with different paint. Keep the real <input type="checkbox">, hide it visually, and draw the switch from its state with :checked + .track. The label wraps everything, so the whole row is the click target.

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>Settings toggles</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .panel { max-width: 420px; margin: 0 auto; background: #fff; border-radius: 14px; padding: 6px 16px; box-shadow: 0 4px 16px rgba(0,0,0,.08); }
  h1 { font-size: 16px; margin: 12px 0 4px; }

  /* the whole row is the label: text and switch are one click target */
  .toggle {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 13px 0; border-top: 1px solid #eef0f3; cursor: pointer;
  }
  .toggle small { display: block; color: #6b7280; font-size: 12px; margin-top: 2px; }

  /* hide the real checkbox from the eye only: it stays focusable and clickable */
  .toggle input {
    position: absolute; width: 1px; height: 1px; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }

  /* the track and knob are drawn from the checkbox state */
  .track {
    flex: none; position: relative; width: 44px; height: 26px; border-radius: 99px;
    background: #c7ccd4; transition: background .15s;
  }
  .track::after {
    content: ""; position: absolute; left: 3px; top: 3px; width: 20px; height: 20px;
    border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.3); transition: transform .15s;
  }
  .toggle input:checked + .track { background: #16a34a; }
  .toggle input:checked + .track::after { transform: translateX(18px); }
  .toggle input:focus-visible + .track { outline: 3px solid #2563eb; outline-offset: 2px; }
  .toggle input:disabled + .track { opacity: .45; }
  .toggle.off { cursor: not-allowed; color: #9ca3af; }

  #state { margin: 12px auto 0; max-width: 420px; box-sizing: border-box; padding: 10px 12px; border-radius: 10px; background: #1d2330; color: #e5e7eb; font: 13px/1.5 ui-monospace, Consolas, monospace; }
</style>
</head>
<body>
<form class="panel" id="settings">
  <h1>Notifications</h1>

  <label class="toggle">
    <span>Email updates<small>A summary once a week</small></span>
    <input type="checkbox" role="switch" name="email" checked>
    <span class="track" aria-hidden="true"></span>
  </label>

  <label class="toggle">
    <span>Push alerts<small>When someone replies</small></span>
    <input type="checkbox" role="switch" name="push">
    <span class="track" aria-hidden="true"></span>
  </label>

  <label class="toggle">
    <span>Sounds<small>Play a sound for new alerts</small></span>
    <input type="checkbox" role="switch" name="sound">
    <span class="track" aria-hidden="true"></span>
  </label>

  <label class="toggle off">
    <span>Text messages<small>Not available on your plan</small></span>
    <input type="checkbox" role="switch" name="sms" disabled>
    <span class="track" aria-hidden="true"></span>
  </label>
</form>
<div id="state" aria-live="polite"></div>

<script>
  const form = document.getElementById('settings');
  const state = document.getElementById('state');

  // list the switches that are on, the same way the form would send them
  function show() {
    const on = [...new FormData(form).keys()];
    state.textContent = 'On: ' + (on.length ? on.join(', ') : 'nothing');
  }
  form.addEventListener('change', show);
  show();
</script>
</body>
</html>
Click a row, or press Tab and then Space. The box below lists the switches that are on.
<label class="toggle">
  <span>Push alerts</span>
  <input type="checkbox" role="switch" name="push">
  <span class="track" aria-hidden="true"></span>
</label>

Because the input is real, the switch gets everything a checkbox has for free: Tab reaches it, Space toggles it, disabled works, and its value is sent with the form.

role="switch" tells screen readers to announce it as on or off instead of checked or not checked.

display: none keeps the mouse working but drops the keyboard and screen readers.
display: none keeps the mouse working but drops the keyboard and screen readers.

The common mistake is hiding the input with display: none. A click on the label still toggles it, so it looks fine with a mouse.

But the input can no longer get focus, so keyboard users cannot reach it, and screen readers do not find it.

Show focus on the drawn switch, since the real input is invisible:

.toggle input:focus-visible + .track {
  outline: 3px solid #2563eb;
  outline-offset: 2px;
}

When it does not work

What you see Cause Fix
Clicking the label does nothing for and id differ, often by case or a typo Make them identical; Email and email are different
Clicking the label works a different control The same id is used twice; for finds the first one Make every id unique
The text is not clickable at all The text is in a <span> or <p>, not a <label> Change it to a <label>
Only the first of two inputs reacts One label wraps two controls One label per control
Custom switch works with a mouse but Tab skips it The input is display: none or visibility: hidden Hide it with the visually hidden class
Clicking the custom switch drawing does nothing The drawn .track sits outside the label Move it inside the label
The field has no name once the user types The name is only in the placeholder Add a <label>
A button inside the label does not toggle the box A label may hold only its own control, and a click on the button stays with the button Move the button out of the label

Label bugs show up when someone clicks, not in a screenshot. A picture of a form looks correct whether the labels are connected or not.

To let someone click through the real thing, 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 tap the labels and switches themselves. If you fix the code later, the same link shows the new version.

Questions people ask

What does the for attribute on a label do?

It names the id of the control the label belongs to. <label for="email"> is connected to the element with id="email". The value must match the id exactly, including upper and lower case.

Why is clicking my checkbox label not working?

The label is not connected to the checkbox. Check that for matches the checkbox id letter for letter, that the id is not used twice on the page, and that the text is a <label> and not a <span>. Wrapping the checkbox inside the label avoids the id entirely.

Can one input have more than one label?

Yes. Several labels can point at the same id, and clicking any of them works the control. The accessible name is built from all of them in page order, so a second label changes what a screen reader announces. One label per control is the safer default.

Is a placeholder enough instead of a label?

No. The placeholder disappears as soon as the user types, and clicking it is not a label click. Keep the name in a label and use the placeholder for an example value.

How do I hide a label but keep it for screen readers?

Use a visually hidden class: position: absolute, width and height of 1px, overflow: hidden and clip: rect(0 0 0 0). display: none or the hidden attribute removes the label for screen readers too.

Keep reading