skip to content

In the browser DOM, how do an element's HTML attributes relate to its JavaScript properties, and why can input.value disagree with input.getAttribute('value')?

level: middleimportance: must knowfreq 62%

answer

  1. markup string versus live object state
  2. most reflect, form controls do not
  3. attribute holds the default, property the current
  4. dirty value flag breaks the link
  5. presence, not value, for boolean attributes

basics

~20 s

Attributes are the strings the HTML parser read from the markup; properties are the live state of the element object in JavaScript. Most properties reflect their attribute both ways, but a form control's value attribute is only the initial value.

solid answer

~40 s

Attributes are the strings that came out of the markup; properties are the live state of the element object. For most of them the two are reflected — writing `el.id` updates the `id` attribute and reading it gives the attribute back. Form controls deliberately break that link: the `value` attribute is only the *initial* value, exposed in JavaScript as `input.defaultValue`, while `input.value` holds what the control currently shows. As soon as the value is set — by the user typing or by code assigning `input.value` — the element's dirty value flag is set, and changing the attribute afterwards no longer moves what is displayed. `checked` behaves the same way against `defaultChecked`. Attributes are also always strings, which is why `disabled="false"` still disables a control: for boolean attributes only presence counts, never the value.

code

javascript · 10 lines
javascript
const input = document.createElement('input');
input.setAttribute('value', 'default');
console.log(input.value, input.defaultValue); // "default" "default"

input.value = 'typed by user';                // sets the dirty value flag
console.log(input.getAttribute('value'));     // "default"
console.log(input.outerHTML);                 // <input value="default">

input.setAttribute('value', 'changed');       // no longer wins
console.log(input.value);                     // "typed by user"

go deeper

for a junior

Know that attributes come from the markup and properties are the live JavaScript state, and that you read a form field's current text with input.value, not getAttribute.

for a middle

Explain reflection and where it stops: value maps to defaultValue, checked to defaultChecked, and the dirty flag means the attribute stops driving the control once it has been set.

for a senior

Demonstrate the debugging payoff — why the typed text is missing from serialized HTML, why DevTools shows a stale value attribute, and when getAttribute is genuinely the right call for ARIA, SVG or custom attributes.

for a principal

Own the convention: which of the two surfaces the codebase treats as the source of truth for control state, and how state that must survive serialization is kept out of the DOM in the first place.

## Two parallel worlds When the parser reads `<input id="name" value="start">`, it builds an element object with an attribute list — a map of string keys to string values — and it exposes that element to JavaScript as an object with properties. `getAttribute`/`setAttribute`/`hasAttribute`/`removeAttribute` operate on the attribute list. Plain property access operates on the object. They are not the same storage, and the DOM specifications decide, attribute by attribute, how tightly the two are wired together. ## Reflection: the usual case Most properties *reflect* their attribute: reading the property returns the attribute's value, and writing the property writes the attribute back. `id`, `title`, `lang`, `hidden`, `href` on an anchor and `src` on an image all work this way, so `el.id = 'x'` and `el.setAttribute('id', 'x')` are interchangeable. Reflection is not always identity, though. Two wrinkles are worth naming: - **Name mismatches.** `class` and `for` are reserved-ish words in older JavaScript, so the properties are `className` and `htmlFor`. `class` also has the richer `classList` view (`add`, `remove`, `toggle`, `contains`) over the same attribute. - **Typed reflection.** The property need not be a string. `a.href` returns the URL *resolved against the document's base*, so `getAttribute('href')` on `<a href="/x">` returns `"/x"` while `a.href` returns `"https://example.com/x"`. `input.checked` is a real boolean, `el.style` is an object, and `el.tabIndex` is a number. ## Boolean attributes For boolean attributes — `disabled`, `readonly`, `required`, `checked`, `selected`, `hidden` (as a boolean form), `multiple` — the HTML rule is that *presence* means true and *absence* means false. The value is ignored entirely, which produces one of the most reliable interview traps: ```js el.setAttribute('disabled', 'false'); // the control is now DISABLED el.disabled; // true el.removeAttribute('disabled'); // this is how you enable it el.disabled = false; // ...or this, via the property ``` A string is never falsy in the attribute world; only removal is. ## Form controls: where reflection stops For `<input>`, `<textarea>` and `<select>`, the markup is a *starting point*, not a mirror of live state — otherwise every keystroke would rewrite the document. The HTML spec gives these elements an internal value plus a **dirty value flag**: - `input.defaultValue` reflects the `value` attribute. - `input.value` is the current value. Its getter returns the internal value; its setter sets the internal value **and sets the dirty value flag**. - While the dirty flag is clear, the current value tracks the attribute. Once it is set — by the user typing, or by code assigning `input.value` — the attribute stops driving it. ```js const input = document.createElement('input'); input.setAttribute('value', 'start'); input.value; // "start" (not dirty yet) input.value = 'typed'; // dirty flag now set input.getAttribute('value'); // "start" (attribute untouched) input.setAttribute('value', 'changed'); input.value; // "typed" (attribute no longer wins) ``` Checkboxes and radios have the identical structure one level over: the `checked` attribute reflects to `defaultChecked`, the `checked` property is current checkedness, and there is a dirty checkedness flag. `<select>` follows the same idea through `<option>`'s `selected` attribute versus `defaultSelected`. This explains two very common bug reports. First, "DevTools does not show my value": the Elements panel renders attributes, and you changed a property, so the typed text is genuinely not in the serialized markup. Second, and consequently, any code that reads a subtree back as markup — `outerHTML`, `innerHTML`, copying HTML around — silently loses what the user typed, because the current value was never written to an attribute. ## Everything in an attribute is a string `getAttribute` returns a string or `null`; `setAttribute` stringifies whatever you pass. So `el.setAttribute('data-count', 3)` stores `"3"`, and comparisons like `getAttribute('data-count') === 3` are always false. Properties can be genuinely typed, which is one more reason to prefer them when a typed property exists. `getAttribute` on a missing attribute returns `null`, not `undefined` — worth remembering when you write truthiness checks. ## The practical rule Use the property when one exists: it is typed, it is live, and it is what the rendering engine actually consults. Reach for `getAttribute`/`setAttribute` when there is no property for what you want — custom attributes, ARIA attributes, SVG attributes — or when you specifically want the *authored* value rather than the current state, which for form controls means you probably wanted `defaultValue` instead.

  • After the user has typed into a field, which property still holds the value written in the markup?
    `input.defaultValue`, which reflects the `value` attribute in both directions — writing it updates the attribute. For checkboxes and radios the equivalent is `defaultChecked` against the `checked` attribute, and for options it is `defaultSelected`. These are the honest way to read or reset the authored starting state without disturbing the current value.
  • Why can a.getAttribute('href') and a.href return different strings for the same element?
    `getAttribute` returns the literal authored string, such as `"/docs"` or `"../x"`. The `href` property is a reflected URL that is resolved against the document's base URL, so it returns the absolute form, `"https://example.com/docs"`. The same applies to `img.src` and `form.action`. Use the attribute when you need what the author wrote, the property when you need the resolved target.
  • How would you reset a form field back to what the markup said after the user edited it?
    Assign the default onto the current value: `input.value = input.defaultValue`, or `input.checked = input.defaultChecked` for a checkbox. Setting the attribute alone will not do it, because the dirty flag is already set. For a whole form, `form.reset()` performs exactly that restore across every control at once.

Attributes are the blueprint the building was constructed from; properties are the building as it stands today. Moving a wall does not redraw the blueprint, and redrawing the blueprint does not move the wall.

saying these in an interview costs you the question

  • getAttribute and the matching property always return the same string
  • Setting el.value updates the HTML shown in DevTools
  • disabled="false" turns the control back on
  • Attributes can store numbers and booleans, not just strings
  • You must use setAttribute to change class or id

context