A foundational project to master HTML/JS and publish a functional utility app to Android.
In the world of web development, sometimes the simplest tools can be the most useful. A click counter is a fundamental project that not only helps you understand the basics of HTML and JavaScript, but also has concrete applications, such as counting event attendance or keeping track of repeating routines.
In this article, we will build a sleek click counter featuring custom increments and hardware haptic feedback, and then walkthrough how to wrap it natively for Android devices using a WebView container.
What are we going to build?
- An operational engine to increment and decrement numerical values.
- A step-interval option allowing custom values per action click.
- Haptic vibration feedback leveraging mobile hardware compatibility.
- A visual shake animation using CSS transforms.
Step 1: The Unified Web Interface (HTML & CSS)
To eliminate inline code noise and styling design violations, we clean up the layout by separating presentation layers into a declarative structure. Notice the implementation of the @keyframes selector to achieve a smooth mechanical vibration look:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Click Counter App</title>
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
color: #fff;
}
.counter-box {
text-align: center;
background: rgba(255, 255, 255, 0.1);
padding: 30px;
border-radius: 16px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
.display {
font-size: 5rem;
font-weight: bold;
margin: 20px 0;
}
.btn {
background-color: #ffffff;
color: #4a5568;
border: none;
padding: 15px 30px;
font-size: 1.5rem;
font-weight: bold;
border-radius: 8px;
cursor: pointer;
margin: 5px;
transition: transform 0.1s;
}
.btn:active { transform: scale(0.95); }
.btn-reset { background-color: transparent; color: #fff; border: 1px solid #fff; font-size: 1rem; }
/* CSS Shaking Effect Class */
.vibrando { animation: shake 0.15s infinite; }
@keyframes shake {
0% { transform: translate(2px, 1px); }
50% { transform: translate(-1px, -2px); }
100% { transform: translate(1px, 2px); }
}
</style>
</head>
<body>
<div class="counter-box" id="appInterface">
<div class="display" id="contador">0</div>
<div style="margin-bottom: 20px;">
<label>Increment Step: </label>
<input type="number" id="campoIncremento" value="1" style="width: 50px; text-align: center;">
</div>
<div>
<button class="btn" onclick="restar()">-</button>
<button class="btn" id="btnSuma" onclick="incrementar()">+</button>
</div>
<br>
<button class="btn btn-reset" onclick="reiniciar()">Reset</button>
</div>
</body>
</html>
Step 2: Safe Execution Logic (JavaScript)
Our JavaScript logic parses operational parameters securely. It includes a fallback mechanism using isNaN() to make sure the app defaults back to 1 instead of breaking if a user leaves the input form blank.
function obtenerPaso() {
let paso = parseInt(document.getElementById("campoIncremento").value, 10);
return isNaN(paso) ? 1 : paso; // Prevención de errores de tipeo vacío
}
function ejecutarHaptica() {
if (navigator.vibrate) {
navigator.vibrate(30); // Ejecuta feedback háptico en hardware compatible
}
}
function incrementar() {
let contadorNode = document.getElementById("contador");
let valorActual = parseInt(contadorNode.textContent, 10);
contadorNode.textContent = valorActual + obtenerPaso();
ejecutarHaptica();
}
function restar() {
let contadorNode = document.getElementById("contador");
let valorActual = parseInt(contadorNode.textContent, 10);
contadorNode.textContent = valorActual - obtenerPaso();
ejecutarHaptica();
}
function reiniciar() {
document.getElementById("contador").textContent = 0;
ejecutarHaptica();
}
// Enlace de efectos CSS dinámicos mediante Event Listeners
document.getElementById('btnSuma').addEventListener('click', function() {
let tarjeta = document.getElementById('appInterface');
tarjeta.classList.add('vibrando');
setTimeout(() => {
tarjeta.classList.remove('vibrando');
}, 150);
});
Step 3: Compiling into a Mobile APK Wrapper
To port this web utility straight into Android devices, compile the asset bundle natively by instantiating a full-screen view browser instance:
- Initialize Android Studio: Create a new project based on an Empty Activity template.
- Port Frontend Code: Create an
assetsdirectory undersrc/main/and place your web files inside (index.html). - Request Vibration System Access: Open the
AndroidManifest.xmlconfigurations file and declare permissions right before the opening application block:AndroidManifest.xml<uses-permission android:name="android.permission.VIBRATE" /> - Mount WebView Inside Activity Controller: Implement the initialization routine inside your main Java class to bridge JavaScript runtimes seamlessly:
MainActivity.java
WebView myWebView = (WebView) findViewById(R.id.webview); myWebView.getSettings().setJavaScriptEnabled(true); myWebView.getSettings().setDomStorageEnabled(true); // Habilita almacenamiento local si expandes código myWebView.loadUrl("file:///android_asset/index.html");
Conclusion
Creating lightweight mobile tools using standard web APIs illustrates how straightforward it is to deploy native cross-platform services without heavy structural dependencies. By building optimized core components, you can write clean code that scales easily from a browser tab to an active Android application package.