Build a JS Counter App Beginner DOM Mini Project
Build a small counter app with plain JavaScript to practice selecting elements, handling clicks, and updating the page with the DOM.
A JavaScript counter app is a small page with a number on screen and two buttons that increase or decrease it. It is one of the first real projects beginners build because it uses the exact pattern behind most interactive websites: select an element, listen for a click, then update the page.
You will build a page with a count display, an increment button, and a decrement button. By the end, clicking a button will update the number instantly, without reloading the page.
What You Will Build
The finished app is three pieces of HTML and about ten lines of JavaScript. Here is the behavior you are aiming for:
- The page shows a number, starting at zero.
- Clicking the plus button increases the number by one.
- Clicking the minus button decreases the number by one.
- The count never goes below zero, no matter how many times you click minus.
This is a small project, but it covers the same three ideas that almost every interactive web page relies on: finding elements on the page, reacting to user input, and keeping the visible page in sync with a value your code is tracking.
Every click follows the same four steps: the browser fires an event, your handler function runs, a variable changes, and the page text updates to match. This loop is the core idea behind the whole project, and you will build it piece by piece below.
Step 1: Build the HTML
Start with a small page that has a number display and two buttons.
<div class="counter">
<button id="decrement">-</button>
<span id="count">0</span>
<button id="increment">+</button>
</div>Each element carries an id attribute so JavaScript can find it later. The middle element holds the number, and the two buttons on either side will trigger the increase and decrease actions once the script is wired up.
Step 2: Select the Elements
The next step is grabbing each of these three elements from JavaScript and storing a reference to them in a variable.
const countDisplay = document.getElementById("count");
const incrementButton = document.getElementById("increment");
const decrementButton = document.getElementById("decrement");
let count = 0;The first three lines point to the number display and the two buttons. The last line adds a separate variable that holds the real count as a number, kept apart from whatever text happens to be on the page. Storing the value here, rather than reading it back out of the page later, is what keeps the app reliable as it grows. You can see a related selection approach in how to use querySelector and querySelectorAll.
Step 3: Handle the Button Clicks
With the elements selected, attach a click handler to each button that updates the count and reflects it on the page.
incrementButton.addEventListener("click", () => {
count += 1;
countDisplay.textContent = count;
});
decrementButton.addEventListener("click", () => {
if (count > 0) {
count -= 1;
countDisplay.textContent = count;
}
});The first handler adds one to the count every time someone clicks the plus button, then writes the new value onto the page. The second handler does the same in reverse, but only runs the subtraction when the count is still above zero.
That single check is what stops the number from ever going negative. For more detail on wiring up clicks like this, see handling click events in JavaScript.
Why the Count Lives in a Variable, Not Just the Page
A common beginner approach skips the separate variable entirely and reads the number straight out of the page text on every click.
incrementButton.addEventListener("click", () => {
const current = Number(countDisplay.textContent);
countDisplay.textContent = current + 1;
});This version works, but it converts text back into a number on every single click. Page text is always a string, so a missed conversion anywhere in the code causes the app to silently start concatenating text instead of adding numbers.
Keeping the count as a real number in its own variable avoids that entire category of bug. The page is only ever used to display the value, never to store it.
Common Mistakes
| Mistake | Why it breaks | Fix |
|---|---|---|
| Skipping the number conversion when reading from the page | Page text is always a string, so adding to it appends characters instead of doing math | Keep the count in its own variable instead of reading it back from the page each time |
| Selecting elements before the HTML has loaded | Element lookups return nothing if the script runs too early | Place the script tag at the end of the body, or select elements after the page finishes loading |
| Overwriting the whole element to change one number | Rewriting more markup than necessary is slower and riskier than it needs to be | Update just the text of the display element on each click |
Next Step
You now have the core pattern for interactive JavaScript: select, listen, update. The same structure scales up to bigger projects, like a JavaScript todo list app or a JavaScript calculator, where you track more state and respond to more kinds of input.
Rune AI
Key Insights
- A counter app selects a display element and two buttons, then updates a count variable on each click.
- addEventListener connects a button click to a function that changes the page.
- textContent updates the visible number without re-rendering the whole page.
- Keeping the count in a variable, not just in the page text, avoids bugs when you need to read the current value.
- The select, listen, update pattern used here is the foundation for most interactive JavaScript projects.
Frequently Asked Questions
Do I need a framework to build a counter app?
Why does the count reset when I refresh the page?
How do I stop the counter from going below zero?
Conclusion
A counter app is a small but complete example of the pattern behind most interactive JavaScript pages: select an element, listen for an event, update a variable, then reflect that variable back on the page. Once this loop feels natural, you can reuse it for bigger projects like a todo list or a shopping cart.
More in this topic
Using Reflect and Proxy Together in JavaScript
Proxy traps intercept operations, but Reflect is what makes them behave correctly. Learn why Reflect belongs inside every trap and what breaks when you skip it.
Top JS Array Methods Interview Questions to Know
The array method questions that come up most often in JavaScript interviews, answered directly with short examples: map vs forEach, mutating vs non-mutating methods, reduce, and more.
JavaScript Reflect API: Advanced Architecture
The Reflect object exposes JavaScript's own internal operations as plain functions. Learn every Reflect method, what it returns, and why it exists as its own API.