Declarative vs Imperative UI
Date: 2026-08-19
Imperative code says how to get from the current screen to the next one. Declarative code says what the next screen looks like and lets something else work out the steps. The whole framework era is a bet that describing the destination is cheaper than scripting every route to it.
Imperative UI issues instructions that mutate the page. Declarative UI returns a description of the page for the given state, and the framework computes the mutations.
The same feature, both ways:
// IMPERATIVE — you own every transition
function addItem(item) {
const li = document.createElement('li');
li.textContent = item.name;
list.appendChild(li);
count.textContent = list.children.length; // remember to update this
if (list.children.length === 1) empty.remove(); // and this
checkoutBtn.disabled = false; // and this
}// DECLARATIVE — you own the destination only
function Basket({items}) {
return items.length === 0 ? <Empty /> : (
<>
<ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>
<p>{items.length} items</p>
<button>Checkout</button>
</>
);
}Notice what disappears: the word “and”. Imperative UI bugs are overwhelmingly forgotten consequences — a state change that updated three of the four things depending on it. The declarative version can’t forget, because it doesn’t enumerate consequences; it re-derives the whole picture.
What the framework does with the description
Every declarative framework needs a way to turn “here is what it should look like” into “here is what to change”. That’s the Reactivity question, and the three answers — diffing a Virtual DOM, tracking dependencies with Signals, or compiling the graph ahead of time — are the same problem solved at different times.
In plain terms: you write the target, the framework works out the diff.
What you give up
Transitions. A description of two states says nothing about the motion between them. This is the honest weak point: enter and exit animation, measuring an element’s position before and after a change, scroll preservation and focus management all live in the gap the model doesn’t address, which is why every framework has animation primitives bolted on the side, and why View Transitions became a platform feature.
Direct control of timing. “Focus this input after it appears” and “measure this element” are inherently imperative and have to be smuggled back in through Framework Escape Hatches.
A cost floor. Re-deriving the picture is more work than a targeted mutation. The bet is that developer errors are more expensive than CPU cycles — usually true, and audibly false on a 60fps drag interaction, which is why those are still written imperatively against the DOM.
The distinction is not framework versus no framework
It’s a property of an interface, and it recurs at other layers:
| Imperative | Declarative |
|---|---|
element.classList.add('open') | class={isOpen && 'open'} |
ALTER TABLE scripts run in order | A schema file the tool diffs — Database Migrations |
ssh in and install packages | A desired-state config the agent reconciles |
fetch() in an effect, then setState | A route loader that declares what the page needs |
The pattern is the same each time: name the desired end state, and hand the transition problem to something that can’t forget a step. The cost is also the same each time — you lose the ability to say “do exactly this, exactly now”, and you get it back only through an escape hatch.
Where it actually bites
The most common failure isn’t choosing wrong, it’s writing imperative code inside a declarative component — an effect that reaches into the DOM to fix up what the render “should” have produced. That code runs after the framework has already decided the state is settled, so it fights the next render rather than replacing it. If a render can produce the result directly, it should; see Framework Escape Hatches for when it genuinely can’t.