document.getElementById: get one element and change it

Give an element an id in the HTML, pass that id to document.getElementById, and you get the element back to read or change. If nothing matches, you get null.

document.getElementById('name') returns the one element whose id attribute is exactly name. Pass the id without a #. You get the element itself, so you can read what is typed in it, change its text, restyle it or switch its classes.

When no element matches, it returns null.

Try it first. Each button runs one line on elements it found by id, and prints that line underneath.

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>getElementById: read and change elements</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .card { background: #fff; border-radius: 12px; padding: 14px; box-shadow: 0 4px 14px rgba(0, 0, 0, .08); }
  label { font-size: 13px; color: #5b6270; }
  input { display: block; width: 100%; box-sizing: border-box; margin: 4px 0 10px; padding: 8px 10px;
          font: inherit; border: 1px solid #c9cdd4; border-radius: 8px; }
  #greeting { margin: 0; padding: 10px; border-radius: 8px; background: #eef1f5; font-weight: 600; }
  #greeting.highlight { background: #fde68a; }
  .buttons { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0; }
  button { font: inherit; font-size: 13px; padding: 7px 10px; border: 0; border-radius: 8px;
           background: #1d4ed8; color: #fff; cursor: pointer; }
  #code { margin: 0; padding: 10px; border-radius: 8px; background: #1d2330; color: #d1fae5;
          font: 12.5px/1.5 ui-monospace, Consolas, monospace; white-space: pre-wrap; min-height: 38px; }
</style>
</head>
<body>
<div class="card">
  <label for="name">Your name</label>
  <input id="name" value="Sam">
  <p id="greeting">Hello!</p>

  <div class="buttons">
    <button id="read">Read .value</button>
    <button id="text">Set .textContent</button>
    <button id="style">Set .style</button>
    <button id="toggle">Toggle a class</button>
    <button id="clear">Clear .value</button>
  </div>

  <pre id="code">Press a button to see the line it runs.</pre>
</div>

<script>
  // Grab each element once, by its id (no '#')
  const nameInput = document.getElementById('name');
  const greeting = document.getElementById('greeting');
  const code = document.getElementById('code');

  // Show the line that ran and what it returned
  function show(line, result) {
    code.textContent = line + (result === undefined ? '' : '\n// → ' + result);
  }

  document.getElementById('read').addEventListener('click', () => {
    show("document.getElementById('name').value", JSON.stringify(nameInput.value));
  });

  document.getElementById('text').addEventListener('click', () => {
    greeting.textContent = 'Hello, ' + nameInput.value + '!';
    show("greeting.textContent = 'Hello, ' + nameInput.value + '!';");
  });

  document.getElementById('style').addEventListener('click', () => {
    const next = greeting.style.color ? '' : '#b91c1c';  // toggle red on and off
    greeting.style.color = next;
    show("greeting.style.color = '" + next + "';");
  });

  document.getElementById('toggle').addEventListener('click', () => {
    const on = greeting.classList.toggle('highlight');
    show("greeting.classList.toggle('highlight');", on);
  });

  document.getElementById('clear').addEventListener('click', () => {
    nameInput.value = '';
    nameInput.focus();
    show("nameInput.value = '';");
  });
</script>
</body>
</html>
A name field, a greeting and five buttons. Each button shows the exact line it runs.

The return value is the element object, the same thing the browser draws on screen. Change a property on it and the page updates at once. There is no copy to save back.

One id in, one element out, and the four properties used most often.
One id in, one element out, and the four properties used most often.

Store the result in a const once and reuse it. Calling getElementById again every time works, but a named variable is easier to read.

Reading .value and changing text, style and classes

Form fields keep their current content in .value. That covers <input>, <textarea> and <select>.

const bill = document.getElementById('bill');
console.log(bill.value);   // what the user typed, as a string
bill.value = '0';          // replace it

.value is always a string, even on type="number". '10' + 5 gives '105', so wrap it in Number() before any math.

The value attribute in the HTML only sets the starting content. Once the user types, .value has the new text and the attribute stays as it was.

Most elements are not form fields. The table puts .value next to the three properties that work on any element.

Property Use it for Example
.value Content of input, textarea, select bill.value
.textContent Plain text inside any element total.textContent = '12.50'
.style One inline CSS property total.style.color = 'red'
.classList Adding and removing classes bill.classList.toggle('invalid')
  • .textContent treats what you give it as text. A <b> in the string shows up as the characters, which is what you want for anything a user typed.
  • .style uses camelCase names: fontSize, not font-size. It only reads inline styles, not rules from a stylesheet.
  • .classList keeps the look in CSS. Define .invalid once in the stylesheet and toggle it from JavaScript.

Prefer classList over many style lines. The design stays in one place and is easy to change later.

Why getElementById returns null

null means nothing with that id existed when the line ran. The most common reason is timing. A script in <head> runs while the browser is still reading the page, before the body has been built.

The browser reads top to bottom. A head script looks for an element that has not been created yet.
The browser reads top to bottom. A head script looks for an element that has not been created yet.

The next line that touches the result then throws a TypeError.

In Chrome it reads Cannot read properties of null when you read a property, and Cannot set properties of null when you assign one. Other browsers word it differently, but it is the same problem.

This page runs the same lookup from four places and catches the error, so you can see which ones fail:

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>Why getElementById returns null</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  #target { margin: 0 0 12px; padding: 10px; border-radius: 8px; background: #fff; border: 1px dashed #9aa3b2; font-size: 14px; }
  .row { background: #fff; border-radius: 10px; padding: 10px 12px; margin-bottom: 8px;
         border-left: 5px solid #9aa3b2; font-size: 13px; }
  .row.bad { border-left-color: #c2410c; background: #fff7f5; }
  .row.good { border-left-color: #15803d; background: #f4fbf6; }
  .row b { display: block; margin-bottom: 3px; }
  .row code { font: 12px/1.45 ui-monospace, Consolas, monospace; word-break: break-word; }
</style>
<script>
  // Each test reports here. The list is drawn once the page has a place for it.
  const results = [];
  function report(where, el, error) {
    results.push({ where, el, error });
    const out = document.getElementById('out');
    if (out) draw(out);
  }
  function draw(out) {
    out.innerHTML = '';
    results.sort((a, b) => a.where.localeCompare(b.where));  // show 1-4 in order
    for (const r of results) {
      const row = document.createElement('div');
      row.className = 'row ' + (r.el ? 'good' : 'bad');
      const title = document.createElement('b');
      title.textContent = r.where;
      const line = document.createElement('code');
      line.textContent = r.el
        ? 'Found <' + r.el.tagName.toLowerCase() + ' id="target">'
        : 'null → ' + r.error;
      row.append(title, line);
      out.append(row);
    }
  }
</script>

<!-- 1. Plain script in the head: the element does not exist yet -->
<script>
  try {
    const el = document.getElementById('target');
    el.textContent = 'changed';  // throws, because el is null
    report('1. <script> in <head>', el);
  } catch (e) {
    report('1. <script> in <head>', null, e.name + ': ' + e.message);
  }
</script>

<!-- 2. defer on an inline script is ignored: still null -->
<script defer>
  try {
    const el = document.getElementById('target');
    el.textContent = 'changed';
    report('2. <script defer> in <head>, inline', el);
  } catch (e) {
    report('2. <script defer> in <head>, inline', null, e.name + ': ' + e.message);
  }
</script>

<!-- 3. Wait for the page to be parsed -->
<script>
  document.addEventListener('DOMContentLoaded', () => {
    const el = document.getElementById('target');
    report('3. <head> + DOMContentLoaded', el);
  });
</script>
</head>
<body>
<p id="target">I am &lt;p id="target"&gt;, written after the head scripts.</p>
<div id="out"></div>

<!-- 4. Script placed after the element -->
<script>
  report('4. <script> after the element', document.getElementById('target'));
</script>
</body>
</html>
Four script positions. Two find the element, two get null.

There are three fixes:

  1. Move the script to the end of <body>, after the elements it uses.
  2. Add defer to an external script: <script src="app.js" defer></script>. It runs after the page is parsed.
  3. Wait for DOMContentLoaded if inline code must stay in the head.

Note case 2 in the demo. defer does nothing on an inline script without src, so it still gets null. The wider list of script problems is in HTML JavaScript not working.

Mistakes in the id itself

When the timing is right and the result is still null, check the argument character by character.

Same page, five calls. The # and a capital letter both give null.
Same page, five calls. The # and a capital letter both give null.
  • A # in the argument. getElementById('#total') searches for an id that literally starts with #. The # belongs to CSS selectors only.
  • Letter case. Total and total are different ids.
  • Duplicate ids. Only the first element in the page comes back. The second one never changes, which looks like the code "half works".
  • Wrong object. The method lives on document. someDiv.getElementById is not a function, because elements do not have it.

getElementById vs querySelector('#id')

For a plain id, both lines return the same element:

document.getElementById('total');
document.querySelector('#total');

The difference is the argument. getElementById takes the raw id. querySelector takes a CSS selector, so it needs the #, and it can also find by class, attribute or position.

Pick getElementById when you are fetching one known element by id.

Some ids are valid HTML but not valid CSS, such as an id that starts with a digit: querySelector('#2col') throws a SyntaxError, while getElementById('2col') just works. Reach for querySelector when you need anything other than an id.

A finished example: a tip calculator

Everything above in one small tool. Every input is looked up by id once. An input listener reads the values, does the math and writes the results with textContent.

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>Tip calculator</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .calc { max-width: 380px; margin: 0 auto; background: #fff; border-radius: 14px; padding: 16px;
          box-shadow: 0 6px 20px rgba(0, 0, 0, .08); }
  h2 { margin: 0 0 12px; font-size: 18px; }
  label { display: block; font-size: 13px; color: #5b6270; margin-top: 10px; }
  input[type=number] { width: 100%; box-sizing: border-box; margin-top: 4px; padding: 9px 10px;
                       font: inherit; font-size: 16px; border: 1px solid #c9cdd4; border-radius: 8px; }
  input[type=number].invalid { border-color: #c2410c; background: #fff7f5; }
  input[type=range] { width: 100%; margin-top: 6px; }
  .out { margin-top: 14px; border-radius: 10px; background: #f4fbf6; padding: 12px; display: grid;
         grid-template-columns: 1fr auto; gap: 6px; font-size: 14px; }
  .out b { text-align: right; }
  .out .big { font-size: 20px; color: #0f5132; }
</style>
</head>
<body>
<div class="calc">
  <h2>Tip calculator</h2>

  <label for="bill">Bill</label>
  <input id="bill" type="number" min="0" step="0.01" value="84.50" inputmode="decimal">

  <label for="tip">Tip: <span id="tipLabel">15</span>%</label>
  <input id="tip" type="range" min="0" max="30" value="15">

  <label for="people">People</label>
  <input id="people" type="number" min="1" step="1" value="3" inputmode="numeric">

  <div class="out">
    <span>Tip</span><b id="tipAmount"></b>
    <span>Total</span><b id="total"></b>
    <span>Each person pays</span><b id="each" class="big"></b>
  </div>
</div>

<script>
  // Look up every element once
  const bill = document.getElementById('bill');
  const tip = document.getElementById('tip');
  const people = document.getElementById('people');
  const tipLabel = document.getElementById('tipLabel');
  const tipAmount = document.getElementById('tipAmount');
  const total = document.getElementById('total');
  const each = document.getElementById('each');

  const money = (n) => n.toFixed(2);

  function update() {
    // .value is always a string, so turn it into a number first
    const b = Number(bill.value);
    const pct = Number(tip.value);
    const n = Math.floor(Number(people.value));

    tipLabel.textContent = pct;

    const ok = bill.value !== '' && b >= 0 && n >= 1;
    bill.classList.toggle('invalid', !(bill.value !== '' && b >= 0));
    people.classList.toggle('invalid', !(n >= 1));
    if (!ok) {
      tipAmount.textContent = total.textContent = each.textContent = '-';
      return;
    }

    const t = b * pct / 100;
    tipAmount.textContent = money(t);
    total.textContent = money(b + t);
    each.textContent = money((b + t) / n);
  }

  // 'input' fires on every keystroke and every slider move
  for (const el of [bill, tip, people]) el.addEventListener('input', update);
  update();
</script>
</body>
</html>
Type a bill, drag the tip slider, change the number of people. The result updates on every keystroke.
  • Numbers from strings: Number(bill.value) before multiplying.
  • Live update: the input event fires on every key and every slider move, unlike change.
  • Invalid input: classList.toggle('invalid', ...) marks the field red and the results show a dash.

For more calculators built this way, see HTML calculator code.

When it does not work

What you see (Chrome wording) Cause Fix
Cannot read properties of null Script runs before the element exists Script at the end of <body>, defer with src, or DOMContentLoaded
Always null, script placement is fine # in the argument Pass the bare id: 'total'
Null for one id only Letter case or a typo differs from the HTML Copy the id from the HTML
Only the first of two elements changes Two elements share the id Make ids unique, or use a class with querySelectorAll
getElementById is not a function Called on an element, not document Use document.getElementById, or el.querySelector('#id')
.value is undefined or setting it shows nothing <div>, <p> and <span> have no value Use .textContent
Math gives '105' instead of 15 .value is a string Wrap it in Number()

A calculator is easier to try than to describe. A screenshot cannot take input, and an .html attachment often opens as 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 type into the fields themselves. If you change the code later, the same link shows the new version.

Questions people ask

Why does getElementById return null?

No element with that exact id exists at the moment the line runs. The usual reasons are a script that runs before the element is parsed, a # in the argument, a different letter case, or a typo in the id.

How do I get the value of an input with getElementById?

Read .value on the element: document.getElementById('name').value. It is always a string, even for type="number", so convert it with Number() before doing math.

Should I use getElementById or querySelector?

Both return the same element for a simple id. getElementById takes the bare id and only exists on document. querySelector takes any CSS selector, so it needs the # and can also search inside one element.

What happens if two elements share the same id?

getElementById returns the first one in document order and ignores the rest. Duplicate ids are invalid HTML, so give each element its own id, or use a class and querySelectorAll.

Can I use the id as a variable without calling getElementById?

Browsers do expose elements with an id as properties of window, so id="total" becomes total in scripts. It breaks as soon as a variable or another global has the same name, so calling getElementById is the reliable choice.

Keep reading