Cover image for Vanilla JavaScript and HTML - No frameworks. No libraries. No problem.

Vanilla JavaScript and HTML - No frameworks. No libraries. No problem.

john_papa profile image John Papa Updated on ・9 min read

Are you using Vue, React, Angular, or Svelte to create web apps? I am, and if you are too, I bet it's been a while since you've written an app that renders content without these fine tools.

Armed with only what comes with the browser. Years ago, this is exactly how many of us wrote web apps. While today's tools help abstract that away (and add a bunch of other benefits), it is still useful to know what is happening under the covers.

Also, consider that if you are rendering small amounts of content, you may want to use HTML, JavaScript, and the DOM without any other tools. Recently I wrote some web fundamentals examples to help teach the DOM, HTML, JavaScript, and the basics of the browser. This experience made me realize that maybe other developers, maybe you, would appreciate a reminder of how you can render content without libraries.

If anything, it's fun, educational, and might make us all respect the value of modern libraries and frameworks which handle this for us.

With that, let's explore a few different ways you can render content. Oh, and keep those MDN Web docs handy!

When you're done, you can push your HTML app with lit-html to the cloud to see it in all of its glory! I included a link to a free Azure trial, so you can try it yourself.

The Sample App

Here is the app I'll demonstrate in this article. It fetches a list of heroes and renders them when you click the button. It also renders a progress indicator while it is fetching.

Tour of Heroes


The right tools are important, and in this exercise, we want to render some content to the browser using the essential tools that we all have. No frameworks. No libraries. No problem.

All we get to use are HTML, TypeScript/JavaScript, CSS, and the browser DOM (document object model). Now let's get to work and see how we can render HTML.

You can learn more about TypeScript coding with VS Code here

If you look closely, you may see I'm using TypeScript in the code. I'm using it because it is super helpful at avoiding bugs due to its typings, and I get to take advantage of how it transpiles to any JavaScript format I need. You could 100% use JavaScript if you prefer. I'll write more in a future article (soon) about how you can transpile TypeScript too.

The Approach

This sample app renders a list of heroes and a progress indicator (while it is working). Let's start by exploring the simpler of these, the progress indicator, and show various ways in which it renders. Then we'll shift to the list of heroes, and see how things change (or not) when there is more to render.

Rendering Progress Indicators

The progress indicator should appear while the app is determining which heroes to render. So it will not be visible until someone clicks the refresh button, then it will appear, and disappear after the heroes render. Let's explore a few ways this can work.

All of the techniques require some awareness of where to put the elements as they are built. For example, the progress indicator and the heroes list both have to go somewhere. One way to do this is to reference an existing element that will contain them. Another way is to reference an element and replace that element with the new content. We'll see a little of each throughout this article.

Rendering Progress with Inner HTML

Creating HTML and put it inside of another element is one of the oldest techniques, but hey, it works! You locate the element where the progress indicator will go. Then set it's innerHtml property to the new content. Notice we're also trying to make it readable using template strings that allow it to span multiple lines.

const heroPlaceholder = document.querySelector('.hero-list');
heroPlaceholder.innerHTML = `
    class="hero-list progress is-medium is-info" max="100"

Simple. Easy. Why isn't all code like this? I mean, come on, look at how quick this code solves the problem!

Alright, you're probably already thinking ahead at how fragile this code can be. One mistake in the HTML, and bam! We have a problem. And is it genuinely readable? This code is arguably readable, but what happens when the HTML becomes more complex, and you have 20 lines of HTML and classes and attributes, and values and ... you get the point. Yeah, this is not ideal. But for short bits of HTML, it does work, and I wouldn't cast it aside too quickly for one-liners.

Something else to consider is how embedded content might add to the readability and stability of the code. For example, if you add content inside of the progress bar for a loading message that changes, you could do this using the replacement technique like this ${message}. This isn't good or bad; it just adds to your considerations when creating large template strings.

One last point on innerHTML is that performance in rendering "may" be a concern. I say this because I do not subscribe to over-optimization. But it is worth testing the performance of rendering HTML using innerHTML as it can cause rendering and layout cycles in the browser. So keep an eye on it.

Rendering Progress with the DOM API

One way to reduce some of the clutter of the long strings is to use the DOM API. Here you create an element, add any classes it needs, add attributes, set values, and then add it to the DOM.

const heroPlaceholder = document.querySelector('.hero-list');
const progressEl = document.createElement('progress');
progressEl.classList.add('hero-list', 'progress', 'is-medium', 'is-info');
const maxAttr = document.createAttribute('max');
maxAttr.value = '100';

The upside here is that the code has more typing, and it relies on the API. In other words, if something was typed incorrectly, the chances are greater in this code than that of the innerHTML technique that an error will be thrown that will help lead to the problem and solution.

The downside here is that it took six lines of code to render what took one line with innerHTML.

Is the DOM API code more readable than the innerHTML technique to render the progress bar? I argue that it is not. But is that because this progress bar HTML is super short and simple? Maybe. If the HTML were 20 lines, the innerHTML would be less easy to read ... but so would the DOM API code.

Rendering Progress with Templates

Another technique is to create a <template> tag and use that to make it easier to render content.

Start by creating a <template> and giving it an id. The template will not render in the HTML page, but you can reference its contents and use those later. This is very useful so you can write the HTML where it makes the most sense: in the HTML page with all the helpful features of an HTML editor.

<template id="progress-template">
  <progress class="hero-list progress is-medium is-info" max="100"></progress>

Then the code can grab the template using the document.importNode() method of the DOM API. It can manipulate the content in the template if needed (in this case, there is no need). Then add that content to the DOM to render the progress bar.

const heroPlaceholder = document.querySelector('.hero-list');
const template = document.getElementById('progress-template') as HTMLTemplateElement;
const fetchingNode = document.importNode(template.content, true);

Templates are a powerful way to build HTML. I like this technique because it lets you write HTML where it makes sense, and the TypeScript/JavaScript code is doing less.

You might wonder if you can import templates from other files. The answer is that you can, but using other libraries. But this exercise is sticking with no libraries. Natively there has been a discussion about HTML imports for years, but as you can see in the "Can I Use" site, the support is not quite there yet in most modern browsers.

Render a List of Heroes

Let's shift to how we can render the list of heroes using those same three techniques. The differences here from rendering a single <progress> HTML element and rendering a list of heroes are that we are now rendering:

  • multiple HTML elements
  • adding multiple classes
  • adding child elements, a specific sequence
  • rendering a lot of similar content, for each hero
  • displaying dynamic text inside of elements

Render Heroes with Inner HTML

Using innerHTML it makes sense to first start with the array of heroes and iterate through them for each hero. This will help build each row, one at a time. The rows are identical other than the hero's name and description, which we can insert using template strings. Each hero in the array builds up a <li>, which maps to a rows array. Finally, the rows array transform into the raw HTML, wrapped with a <ul>, and set to the innerHTML.

function createListWithInnerHTML(heroes: Hero[]) {
  const rows = heroes.map(hero => {
    return `<li>
        <div class="card">
          <div class="card-content">
            <div class="content">
              <div class="name">${hero.name}</div>
              <div class="description">${hero.description}</div>
  const html = `<ul>${rows.join()}</ul>`;
  heroPlaceholder.innerHTML = html;

Again, this works. And it is "arguably" quite readable. Is it the most performant? Is it ripe with possible typing mistakes that won't be caught easily (or ever)? You be the judge. Let's hold judgment until we see some other techniques.

Rendering Heroes with the DOM API

function createListWithDOMAPI(heroes: Hero[]) {
  const ul = document.createElement('ul');
  ul.classList.add('list', 'hero-list');
  heroes.forEach(hero => {
    const li = document.createElement('li');
    const card = document.createElement('div');
    const cardContent = document.createElement('div');
    const content = document.createElement('div');
    const name = document.createElement('div');
    name.textContent = hero.name;
    const description = document.createElement('div');
    description.textContent = hero.description;

Rendering Heroes with Templates

The heroes list can be rendered using templates, using the same technique we used to render the <progress> element. First, the template is created in the HTML page. This HTML is slightly more complicated than what we saw with the <progress> element. You can easily imagine how even more HTML would not be a problem using this technique. It's just HTML in an HTML page, with all the benefits of fixing errors and formatting with a great editor like VS Code.

<template id="hero-template">
    <div class="card">
      <div class="card-content">
        <div class="content">
          <div class="name"></div>
          <div class="description"></div>

You could then write the code to create the heroes list by first creating the <ul> to wrap the heroes <li> rows. Then you can iterate through the heroes array and grab the same template for each hero, using the document.importNode() method once again. Notice the template can be used repeatedly to create each row. One template becomes the blueprint for as many here rows as you need.

The heroes list should show each respective hero's name and description. This means that the template only gets you so far, then you have to replace the hero specific values inside of the template. This is where it makes sense to have some way to identify and reference the places where you will put those hero specific values. In this example, the code uses the querySelector('your-selector') method to get a reference for each hero, before setting the name and description.

function createListWithTemplate(heroes: Hero[]) {
  const ul = document.createElement('ul');
  ul.classList.add('list', 'hero-list');
  const template = document.getElementById('hero-template') as HTMLTemplateElement;
  heroes.forEach((hero: Hero) => {
    const heroCard = document.importNode(template.content, true);
    heroCard.querySelector('.description').textContent = hero.description;
    heroCard.querySelector('.name').textContent = hero.name;

Is the template easier than the other techniques? I think "easy" is relative. I feel that this code follows a pattern that is reasonably repeatable, readable, and less error-prone than the other techniques.


Notice I have not mentioned how the major frameworks and libraries handle rendering content. Vue, React, Angular, and Svelte all make this significantly easier using less code. They also have their respective additional benefits beyond rendering. This article is only focusing on a relatively simple rendering using the DOM with pure HTML and TypeScript/JavaScript.

Where does this leave us?

Hopefully, this gives you an idea of how you can render content without any libraries. are there other ways? Absolutely. Can you make reusable functions to make the code more straightforward and more reusable? Absolutely. But at some point, you may want to try one of the very excellent framework tools such as Vue, React, Angular, or Svelte.

These popular frameworks are doing a lot for us. If you are like me, you recall using pure DOM code with JavaScript or jQuery to render content. Or you remember using tools like handlebars or mustache to render content. Or maybe you've never rendered to the DOM without a framework. You can imagine what today's libraries and frameworks are doing under the covers to render content for you.

It's helpful to know what can be done with pure HTML, TypeScript/JavaScript, and CSS even if you are using a framework that abstracts this from you.

Whatever your experience, I hope it was helpful to take a brief exploration through some of the techniques that you can use to render content.

Posted on Apr 18 by:

john_papa profile

John Papa


Husband, father, & Catholic enjoying life with my family. Working @ Microsoft. Disney fanatic, web and mobile developer


With our technology skills platform, companies can upskill teams and increase engineering impact.


markdown guide

Vanilla js is 10x faster than jquery, more faster than any other javascript framework because it has less overheads.

Frameworks expire very quickly. For example Angular is now at version 8.

Using frameworks eliminates control over your abilities to solve the problem in a different most probably in a better way.

Companies behind frameworks market them heavily and in turn you are nolonger an original developer but just a tools integrator.

For the past 40 or so decades of technology change, we have been groping in the dark, and you what? C/C++ remains the most efficient and critical mission language today. It surprised me php7 + swoole processes request per second far more than nodejs. Swoole is developed from C.

All said, now the advantages of frameworks:
They automate routine process, speed up develpment at the expense of efficiency.
They abstract common procedures and eliminate re-inventing the wheel.

Thank you John, this article takes us back to the great basics of web engineering.


Just wanted to say that nowadays Rust is the most efficient language, but not the most mission-critical language.

For web processes, swoole and actix tend to rank pretty high in the speed of processing. Swoole is mostly faster than JS because it isn't converting strings and creating as many objects to pass to the user from what I've explored... also, frameworks like express are really slow in surprising places.


I appreciate the comment and I love learning new languages. Rust is indeed very cool.

x is "... faster than JS ..." is not the point of the article. It can be powerful to explore performance, but in most cases in my many years of techhnology, asserting perf often is "it depends" and more often than not perf differences are negligible.

I don't think you are pushing for one over the other, as your comment reads very friendly. I just don't want to start a x is better than Y discussion as I'd rather focus on the point here ... which is there is value in learning plain vanilla js and html even when you mat use additional tools.

thanks for commenting

No worries. But in this case it was more "x is faster than node by a long shot" really; and I must agree. Swoole and Rust are both languages much faster than PHP and JS, and both have similar focus groups and people who use them.

In benchmarking (lol) both of them get really high marks (top 5--10 for actix, top 10--20 for swoole) but as you likely know those numbers aren't everything.


Yep I applied work @ Space X and they require a C++ background. I love it!! #efficiencyistheykey #everykeystrokecounts #calories #saveyourenergy


Sure! It's really helpful to remember where we came form, and to re-look at what can be done with lower level APIs.

There is huge value in frameworks ... but personally, I find it very helpful to know what is possible.


After seeing this message, I decided to give swoole another shot. Unfortunately, it still has a long ways to go in both documentation and community support for it for me, a huge PHP lover, to consider using it as a framework. I finally got a little demo going using MySQL co-routines within an HTTP server, but it took quite a bit of trial and error as there wasn't any full example and there was little to no documentation on the errors I was encountering. One very glaring issue is that the MySQL module is no longer part of swoole despite it being a major part of the documentation. Also, I could never really get a lot of requests going at once. With a better example on best practices I might have been unable to, but without such, it's a non-starter for me, and I'll continue using Node. I suppose this is for the best since everyone else at my workplace seems to have a weird hatred of PHP.


Great article! To be honest I'm now looking more and more into using <template> + WebComponents because they don't require any library. Although, I still prefer using abstractions like Stencil.

p.s. I'm (P)react dev


Stencil is cool. Template is powerful and I wish html imports would materialize.

But really the pint of this article is just to appreciate what we have. Ya know?


HTML Imports is not supported in most browsers and not trending that way from what I can tell

A few years ago, I used them extensively and had hoped that they'd become a standard, but they are, in fact, dead. The only major browser that still supports them is Chrome (Opera does, but, well... it's Opera), and they said that support is ending Feb. 2020. And polyfills won't work unless you purposely stay on version 1.x of webcomponents.js, which you shouldn't (github.com/webcomponents/polyfills...).


When you say “I wish ...” is it because there isn’t a way to do that right now?

HTML Imports is not supported in most browsers and not trending that way from what I can tell. caniuse.com/#feat=imports


In the last example, you placed const template = document.getElementById('hero-template') inside the loop.
Is the template content itself being modified after importing it, or can the template be queried only once before the loop?


Nice catch. Yes, you can get the template outside (before) looping. I'll move that, that's a good move.Thanks


Love the templates pattern, it's new for me
Thanks for sharing


Excellent article, we should not lose sight of the basics.
Any reason for not using documentFragment? developer.mozilla.org/en-US/docs/W...


Thanks. Yeah, documentFragment could be very helpful when you assemble a DOM tree outside of the page (to avoid reflow). Then attach it when ready.


sure. template strings with innerHTML work, and it's the first thing I showed here. or do yuo mean something else?

nope i DID miss it... I was looking for it further down in your example code but you had it covered :)


Little annoying so with gitlab.com/zeen3/z3util-dom:

import { $cc, $cec, filter_map, noemp } from 'z3util-dom';

function createListWithTemplate(heroes: Hero[]) {
  return $cec(
      hero => hero && $cec("li", [
        $cc("name", [hero.name]),
        $cc("description", [hero.description]),

though what's doing the work may end up a bit excessive:

/** Map an iterable list, filtering as necessary */
export function* filter_map<T, U>(list: T[] | IterableIterator<T>, f: ((t: T) => U | null | undefined)): IterableIterator<U> {
    let v, k
    for (k of list) if ((v = f(k)) != null) yield v;
export type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
type Appendable = HTMLElement | DocumentFragment | string;
type Queryable = Document | DocumentFragment | Element;
export type MaybeOr<T> = T | false | 0 | null | '' | undefined;
export type IterableOrNot<T> = MaybeOr<Iterable<MaybeOr<T>>>;
interface createelement<E> {
    <T extends keyof E>(el: T, o: Partial<Omit<E[T], "children" | "childNodes" | "classList" | "style">>): E[T];
/** Create element (with attributes) */
export const $ce: createelement<HTMLElementTagNameMap> = (el, o) => Object["assign"](document["createElement"](el), o);
/** Create element (with children, attributes) */
export const $cec = <T extends keyof HTMLElementTagNameMap>(
    name: T,
    children?: IterableOrNot<Appendable>,
    attrs?: Omit<Partial<HTMLElementTagNameMap[T]>, "children" | "childNodes" | "innerHTML" | "innerText" | "outerHTML">,
) => {
    let container = $ce(name, (attrs || {}) as unknown as any), el: MaybeOr<Appendable>;
    if (children) for (el of children) el && container["append"](el);
    return container
/** Create container (with classlist) */
export const $cc = (n: string, c?: IterableOrNot<Appendable>) => $cec("div", c, {"className": n});

But then again it's there to be specifically generic.


There's actually very little that's generated.

It's mostly typing information but even then...

Most of it is just expansion. If you expand all of it, it ends up being really short and roughly the same space as the template option.


This article is super important right now. I've seen a lot of new devs jumping directly to work with frameworks like react without knowing how "the magic" happens under the hood.
Thanks for your contribution John! I would like to hear your thoughts about Svelte.



I like tools that move the web forward.


Terrific article! I enjoy using frameworks but I find working in Vanilla JS to be really satisfying. :)

Also, am I the only one that didn't know about the progress bar element in html?? Holy moly that's awesome!


Thanks. Hey we all learn stuff every day. :)


The ES6 template string almost eliminates the need for a 3rd-party template library. With Atom (and I guess it's possible with VSCode and Sublime too), you can annotate the template strings so that their content is syntax-highlighted according to the language.

This way you get both the readability and the ability to quickly spot errors.

That said, I'd still rather use a lighweight framework such as Mithril or Svelte.


You can also create a cached element that will compare the previous rendered string with a new one, so if the new one is different that the old one then you can replace with innerHTML. Simple and easy.


Ah, thanks for the reminder !
Brings back memories et opens perspectives, just like when we worked with jQuery knowing this would be sooner or later replaced by native APIs.
What would be "faster" though ?

  • DOM nodes creation or
  • InnerHTML method

I used both in my "early days" but never knew or had the need to know what was most optimized.
Thanks John.


Thanks John. The ES5 -> ES6 leap was huge. We can build amazing things with pure unadulterated JS! Excellent demo.


Yes. We’ve come a long way.


do you have the code to share, so I can see the full working app ?


I haven’t released it yet. But I will


It costs measuring tech, nonetheless, you have to learn a new framework before using it, whereas raw - vanilla javascript doesn't require you to do that. You gain practice in development, but it's your practical material enterprise intangible asset, so to say. If costs aren't important to you or if you consider the government is funding you - subsidizing - while you are learning to cover your learning costs, then you are correct. The vanilla framework is more cost-effective, because you don't have to learn a framework, but you don't save nor improve - i. e. accelerate - dev rendering agility & delivery w/ a framework. Otherwise, you aren't entangled into major vendors like Google backup by angular.js.


Thanks for the comments.

Something else to consider is what you don’t get when you use vanilla js. One example is escaping/scrubbing html. Another is optimized updates of the dom for low reflow. And you do still have to learn the dom api.

All that said I do firmly believe that it only benefits us all if we experience the dom api and vanilla js


This discussion would not be complete without a link to VanillaJS:


<div class="name">${hero.name}</div>

i'm sure you know you have to escape the hero name:)


Yep. That would be a good idea and something a framework does for you.


A lot of times people try to learn frameworks first because it is a marketable skill. I run the largest Facebook group about JavaScript, we currently have over 104k members, and I use that platform to encourage developers to delve deeper into the language and learn how it does certain things. This knowledge makes it easier to use and modify these frameworks.



Hi John,
Thank you! Great article. Where do I learn more of this approach? VanillaJs? I mean any specific resource? Course? Tutorial? I need to learn well the fundamentals before I move on more with frameworks.


Can I suggest a little enhancement, checkout lit-html


Lit html is cool too. But the point here is pure vanilla


Ahhh totally get it. I feel that lit is like vanilla essence, it's like vanilla pods only cheeper.


Sitting in a coding meetup, the guy says something about why jQuery is so yesterday.
I immediately heard the Jim Gaffigan whispering to himself voice: "...hey, I LIKE jquery! fella..."


I spent a lot of time in that world too


With the 3rd approach I had a bug in IE11.

Usually the content inside tag is defended by shadow DOM, so that if you want to querySelect those elements in subsequent interactions, template stays immutable in the DOM. For example, you use template content with Load more button. Template should always be original. But IE11 doesn't really defend elements inside template tag, so if you decide to querySelector(..).parentElement.removeChild(), IE will get those templates and it will delete them, causing unpredictable behaviour!

What helped is replacing tag with .... That way IE11 won't even touch the so called template.


Unless there is some specific and unavoidable reason to use a library or framework, I always go vanilla js. I don't like limiting a client's ability to hire developers after I am done because of a personal preference. That always felt wrong to me. Anyone who understands js should be able to easily understand my comments and modify my scripts.


Thanks, John! It's a great addition to framework-oriented articles :)


Thanks for reminding us of the raw power of html css and Javascript devoid of frameworks.

***I love your Angular courses on Pluralsight. It helped me a lot. 💪🏾


Thanks. I appreciate that :)


This is pretty cool, thanks.


This is exactly what I was looking for!! I was using the second approach but I wanted something like the third one and I found it here. Thank you so much!


"Vue, React, Angular, or Svelte"

Where is Stencil? Stencil rox! \O/ stenciljs.com/
Where is Mithril? Mithril rox! \O/ mithril.js.org/


Where the value listing them all? Lol


Are the sources available for this little app?


Not yet. But in a few weeks


Were the actual array of Heroes implied and outside the article, or am I terrible at reskimming posts...?


It’s not shown. You didn’t miss it.


I love this. I put our stacktraces on a diet when I built our JIRA Server Plugin from scratch via atlassian-connect-express. Dope!! ⚡️ 💥 🙌🏿