jQuery append(), prepend(), and html(): DOM Insertion Methods Compared
How I choose between jQuery append, prepend, html and text: insert positions, return values, moving vs cloning, why html() drops click handlers, and why I insert once, with output from jQuery 1.8.3.

The question I get: “I need to put some HTML into a page. Should I use jQuery append, prepend or html? And why did my click handler stop working afterwards?” The methods look alike until you hit the one detail that differs.
jQuery append, prepend and html are the insertion methods I use most. They differ in where the content goes and in what happens to what was already there. The manipulation section of the jQuery API lists the full family. I compare them here with output from a real run on jQuery 1.8.3, using this small fixture page:
<ul id="list"><li id="a">A</li><li id="b">B</li></ul>
<div id="box"><p class="msg">Old <b>text</b></p></div>
<div class="slot"></div><div class="slot"></div>
<table id="t"><tbody><tr><td>row</td></tr></tbody></table>
Finding the target element is the job of selectors, which I cover in jQuery selectors explained. Here I assume you already hold the target. The jQuery Learning Center page on manipulating elements is a good companion read.
Choose jQuery append or html by the existing content
My position: choose the method by what should happen to the existing content, not by what you add. html() says “throw away what is there.” append() says “keep what is there.” Most bugs here come from using one when you meant the other.
Where jQuery append puts content: four positions
var $list = $('#list');
$list.append('<li id="end">end</li>'); // last child, inside
$list.prepend('<li id="start">start</li>'); // first child, inside
$('#a').before('<li id="before-a">before A</li>'); // sibling, outside
$('#a').after('<li id="after-a">after A</li>'); // sibling, outside
console.log($list.children().map(function () { return this.id; }).get().join(' | '));
start | before-a | a | after-a | b | end
| Method | Position | Inside or beside |
|---|---|---|
append() |
Last child | Inside |
prepend() |
First child | Inside |
after() |
Next sibling | Outside |
before() |
Previous sibling | Outside |
html() |
Replaces all children | Inside |
The output reads in document order. First comes start (prepended), then before-a, the original a and after-a. The original b follows, and end (appended) closes the list. The two outside methods placed their items next to #a rather than inside it. My memory aid: append and prepend change a container’s children, while before and after change its neighbours.
html() versus text(): replacing the contents
var $box = $('#box');
console.log('html():', $box.html());
console.log('text():', $box.text());
$box.html('<em>hello</em>');
console.log('after html("<em>"):', $box.html());
$box.text('<em>hello</em>');
console.log('after text("<em>"):', $box.html());
console.log('text() now :', $box.text());
html(): <p class="msg">Old <b>text</b></p>
text(): Old text
after html("<em>"): <em>hello</em>
after text("<em>"): <em>hello</em>
text() now : <em>hello</em>
With no argument, html() reads the inner markup and text() reads the text without tags. With an argument, html() parses your string as markup. text() treats it as plain text and escapes it. The output shows this: jQuery stores <em>hello</em> as escaped entities, and the page displays literal angle brackets.
That escaping is a security feature. Say the string came from a user: a comment, a name, a search term or a URL value. Then text() is safe and html() is a cross-site scripting hole. The browser runs any <script> tag or onerror attribute in that string. The rule is short: if you did not write the string, use text().
What each method returns, and why chaining breaks
var $list = $('#list');
var a = $list.append('<li>x</li>');
var b = $('<li>y</li>').appendTo($list);
console.log('append() returns the container :', a[0] === $list[0]);
console.log('appendTo() returns the new element:', b[0].tagName, b.text());
console.log('html("x") returns the container :', $('#box').html('x')[0].id);
console.log('html() returns a string :', typeof $('#box').html());
append() returns the container : true
appendTo() returns the new element: LI y
html("x") returns the container : box
html() returns a string : string
append() returns the container you called it on. That suits adding several things to one parent. appendTo() is the mirror image. The content comes first, the target second, and it returns the new element. That matters in chains. $('<li>y</li>').appendTo($list).addClass('new') styles the new item. $list.append('<li>y</li>').addClass('new') styles the whole list. The same pairing exists for prependTo, insertBefore and insertAfter. Note that html('x') returns the container as a setter. As a getter it returns a string, so you cannot chain after reading.
Moving versus cloning existing elements
// One target: an existing element is MOVED
$('#a').appendTo('#box');
console.log('li#a count after moving to one target:', $('#a').length, '| parent:', $('#a').parent().attr('id'));
// Several targets: it is CLONED for all but the last
$('#b').appendTo('.slot');
console.log('li#b copies in the page :', $('.slot li').length, '(ids repeat:', $('li#b').length + ')');
li#a count after moving to one target: 1 | parent: box
li#b copies in the page : 2 (ids repeat: 2)
Pass jQuery append an element that is already on the page and it moves it instead of copying it, as the first line shows. With several targets, it clones the element for every target except the last. The second line shows the side effect. Two li#b elements now exist, so the ID repeats. That is invalid HTML, and $('#b') will only ever find the first. When I want a copy on purpose, I call .clone() myself and change the ID.
Scripts and event handlers: the part that bites
window.ran = 0;
$('#box').html('<p>one</p><script>window.ran += 1;<\/script>');
console.log('script run through .html():', window.ran);
$('#box').append('<script>window.ran += 10;<\/script>');
console.log('script run through .append():', window.ran);
// html() destroys child handlers, append() leaves them alone
$('#list').on('click', 'li', function () {});
$('#box').html('<button id="btn">x</button>');
var hits = 0;
$('#btn').click(function () { hits += 1; });
$('#box').append('<span>more</span>');
$('#btn').click();
console.log('click handler survives append():', hits);
$('#box').html($('#box').html()); // re-parse the same markup
$('#btn').click();
console.log('click handler survives html(html()):', hits, '(unchanged means it was lost)');
script run through .html(): 1
script run through .append(): 11
click handler survives append(): 1
click handler survives html(html()): 1 (unchanged means it was lost)
Two behaviours here surprise people. First, jQuery runs inline <script> blocks inside a string you pass to html() or append(). That is one more reason never to pass it untrusted text. Second, and this answers the opening question, jQuery append adds to the container and leaves existing children alone. So the click handler on #btn survived. But html() destroys the old child elements and builds new ones from the string. Any handler on the old button goes with it. In the output, the counter stayed at 1 after the second click.
The fix is not to avoid html(). Instead, bind the handler to something that stays, with a delegated handler on the container: $('#box').on('click', '#btn', fn). One handler on a stable parent listens for clicks that bubble up from whatever children exist at the time. I explain this pattern in my post on jQuery event handling and delegation. It is the standard fix for any page that replaces its content.
Batching jQuery append calls: touch the DOM once
var changes = 0;
var observer = new MutationObserver(function (records) { changes = records.length; });
observer.observe(document.getElementById('box'), { childList: true });
function reset() { $('#box').empty(); }
var items = [];
for (var i = 1; i <= 100; i++) items.push(i);
// Slow pattern: one DOM insertion per item
reset(); observer.takeRecords();
$.each(items, function (i, n) { $('#box').append('<span>' + n + '</span>'); });
console.log('100 appends in a loop -> DOM changes:', observer.takeRecords().length);
// Better: build one string, insert once
reset(); observer.takeRecords();
var html = $.map(items, function (n) { return '<span>' + n + '</span>'; }).join('');
$('#box').html(html);
console.log('one html() with a string -> DOM changes:', observer.takeRecords().length);
// Also fine: build detached nodes first
reset(); observer.takeRecords();
var $holder = $('<div></div>');
$.each(items, function (i, n) { $holder.append('<span>' + n + '</span>'); });
$('#box').append($holder.children());
console.log('detached holder, one append -> DOM changes:', observer.takeRecords().length);
100 appends in a loop -> DOM changes: 100
one html() with a string -> DOM changes: 1
detached holder, one append -> DOM changes: 1
The numbers count separate changes to #box, counted by a mutation observer on the test page. They are not timings. Calling jQuery append in a loop changed the page 100 times. Building one string and calling html() once changed it once. Assembling the items in a detached element and appending them together did the same. Every change can force the browser to recalculate layout. On a long list in Internet Explorer 7 or 8, 100 changes show up as a visible freeze. Build the markup first, then insert once.
Tables: the tbody trap
$('#t').append('<tr><td>new</td></tr>');
console.log('rows in table :', $('#t tr').length);
console.log('direct tr children of <table>:', $('#t > tr').length, '| inside tbody:', $('#t > tbody > tr').length);
rows in table : 2
direct tr children of <table>: 0 | inside tbody: 2
Browsers add a <tbody> around table rows when they parse HTML. So rows sit inside tbody, not directly in table. The call $('#t').append('<tr>…') works because jQuery handles the placement. But a selector such as #t > tr matches nothing, as the second line shows. Select #t tbody or #t tr instead.
Troubleshooting DOM insertion
| Symptom | Cause | Fix |
|---|---|---|
| Click handler stops working after inserting content | html() replaced the elements the handler was attached to |
Use a delegated handler on a container that stays |
| Element disappeared from its old place | Passing an existing element moves it | Use .clone() if you want a copy |
| Duplicate IDs after inserting | One element inserted into several targets | Clone and change the ID, or use a class |
| Chained call styles the wrong element | append() returns the container |
Use appendTo() to chain on the new element |
| User-typed text runs as code | html() or append() with an untrusted string |
Use text() or build the element and set its text |
| Page freezes while building a long list | Hundreds of separate insertions | Build one string or a detached fragment; insert once |
How I insert content on real sites
- I render lists from data in one pass. The code maps an array to markup strings, joins them, and calls
html()orappend()once. The same applies to markup that arrives from a jQuery AJAX request. - Templates keep markup out of JavaScript. I keep a template in the page, either a hidden element or a script block of another type. Then I clone it, fill the fields with
.text(), and insert it. - I escape user content by default. My convention is that dynamic values always go through
.text()or.attr(), never string concatenation into markup. That prevents the most common injection bug. - Containers stay and contents are disposable. Handlers live on the container, and I replace the inside freely. A refresh never breaks behaviour.
The bug report I see most often here: “the delete buttons stop working after I refresh the list.” The refresh calls html() with fresh markup. The code had bound the handlers straight to the old buttons. The fix takes one line: move the handler to the list container with a delegated .on(). Ever since, I bind handlers on containers by default. I also build list markup in one string and insert it once, so a refresh is a single DOM change.
Which method I reach for
- Do I want to keep what is already there? If yes,
append()orprepend(). If no,html()ortext(). - Inside or beside? Inside the target is
append/prepend. Next to it isafter/before. - Did a user write this string? If yes,
text()or build the element and set its text. - Do I need to keep working with the new element? If yes, use the
Toform, such asappendTo(). - Is it more than a handful of items? Build once, insert once.
When not to build markup in JavaScript
- The server can render it. Server-rendered content loads faster and works without JavaScript. Search engines also index it.
- Only visibility changes. If the content already exists, show and hide it, rather than building and removing it.
- Changing a single value. For an input, use
.val(). For a link, use.attr('href', …). You do not need to rebuild the element.
Common jQuery append mistakes
- Using
html()with text a user typed. Usetext(). - Attaching handlers directly, then replacing the elements. Delegate from a container.
- Appending inside a loop. Collect the markup, insert once.
- Inserting one element into several places and expecting unique IDs. Clones keep the ID.
- Selecting
table > tr. The browser added atbody. - Forgetting that
append()returns the container. Chain on the new element withappendTo().
FAQ
What is the difference between append() and html() in jQuery?
append() adds content after the existing children of an element and leaves them alone. html() replaces all the children with the new markup. It destroys the old elements and any event handlers bound directly to them.
What is the difference between append() and prepend()?
jQuery append and prepend both insert inside the target element. append() puts the new content at the end, as the last child. prepend() puts it at the start, as the first child. after() and before() place content outside the target as a sibling.
What is the difference between append() and appendTo() in jQuery?
They do the same insertion with the operands reversed. $(a).append(b) inserts b into a and returns a. $(b).appendTo(a) inserts b into a and returns b, so you can keep chaining on the new element.
What is the difference between html() and text() in jQuery?
html() reads or writes markup and parses strings as HTML. text() reads or writes plain text and escapes any markup you give it. That makes it the safe choice for content a user typed.
Why does my click handler stop working after I use html()?
Setting html() removes the old child elements and creates new ones. Any handler bound directly to an old child is lost. Bind the handler to a parent that stays, using delegation: $(parent).on('click', '.child', handler).
Insertion methods differ less in what they add than in what they do to the existing content and its handlers. The number of DOM changes matters as well. Get those right and the rest of DOM work is routine. My rule of thumb: jQuery append to keep, html to replace, text for anything a user wrote, and insert once.
Originally posted on 11 December 2012 and post last modified on 2 November 2026