What Can You Build? — The NGN News Portal
A real news website built with only HTML, CSS, and JavaScript. Read each concept, study the code, then answer the quiz to test yourself.
This project covers some of the most commonly used real-world patterns in front-end development. Scroll effects, animated slideshows, dynamic card sliders, sticky headers that change on scroll — every single one of these is built from concepts you can learn and reuse in any project.
When you scroll the NGN homepage, the background hills, leaves, and text all move at different speeds, creating a sense of depth. This is called a parallax effect and it is achieved entirely in JavaScript by listening for the scroll event and changing each element's position based on how far down the user has scrolled.
The key is window.scrollY — a number that tells you exactly how many pixels the user has scrolled from the top. Every time it changes, JavaScript updates each element's style using multiplication to make some things move faster and others slower, which creates the 3D illusion.
const text = document.getElementById("text");
const leaf = document.getElementById("leaf");
const hill1 = document.getElementById("hill1");
window.addEventListener("scroll", () => {
let value = window.scrollY;
/* scrollY = how many pixels the user has scrolled */
text.style.marginTop = value * 2.5 + "px";
/* moves down fast — feels close */
leaf.style.top = value * -1.5 + "px";
/* moves UP as you scroll — creates floating feel */
hill1.style.top = value * 1 + "px";
/* moves down slowly — feels far away */
});The multiplier number controls the speed. A high number like 2.5 makes the element race across the screen. A low number like 1 makes it drift slowly. A negative number like -1.5 makes it move in the opposite direction — upward while the user scrolls down. By assigning a different multiplier to each layer, every element moves at a unique speed and the illusion of depth is created.
text.style.marginTop be set to?The NGN navbar starts slightly transparent and large. Once you scroll past 50 pixels, it shrinks, turns solid black, and sticks tightly to the top. This behaviour is controlled by JavaScript adding and removing a CSS class called sticky — the CSS already knows what the navbar should look like when that class is present, JavaScript just decides when to apply it.
This is one of the most important patterns in front-end development: JavaScript controls when, CSS controls what it looks like. Keep these responsibilities separate and your code stays clean and easy to maintain.
const header = document.querySelector("header");
window.addEventListener("scroll", () => {
if (window.scrollY > 50) {
header.classList.add("sticky");
/* user scrolled past 50px — apply the sticky style */
} else {
header.classList.remove("sticky");
/* back near the top — restore original style */
}
});
/* in CSS, .sticky overrides the default header padding and background */
/* header.sticky { padding: 1rem 2.2rem; background: #000; } */The number 50 is the threshold in pixels. You can change it to anything. The CSS .sticky class has a smaller padding and a solid background — so the moment JavaScript adds it, the header snaps into its compact form. The transition: all 0.3s ease on the header element in CSS makes that snap animate smoothly instead of jumping instantly.
classList.add("sticky") actually
do to the element?The "Quick Stories" section on the NGN homepage is a manual image slideshow — you click the arrows to move between slides, or click the dots to jump to a specific one. The logic is built from scratch using a single variable that tracks which slide is currently visible, and a function that shows the correct one and hides all the others.
This project is a perfect example of state management at its simplest — keeping track of where you are, and updating what the user sees based on that. This exact pattern — a variable that holds the current position, a function that reads it and updates the DOM — appears in almost every interactive project you will ever build.
let slideIndex = 1;
showSlides(slideIndex); /* show the first slide on page load */
function showSlides(n) {
let slides = document.getElementsByClassName("mySlides");
let dots = document.getElementsByClassName("dot");
if (n > slides.length) { slideIndex = 1; }
/* if we go past the last slide, wrap back to slide 1 */
if (n < 1) { slideIndex = slides.length; }
/* if we go before slide 1, wrap to the last slide */
for (let i = 0; i < slides.length; i++) {
slides[i].style.display = "none"; /* hide ALL slides */
dots[i].className = dots[i].className.replace(" active", "");
/* remove active class from all dots */
}
slides[slideIndex - 1].style.display = "block";
/* show only the current one (index -1 because arrays start at 0) */
dots[slideIndex - 1].className += " active";
/* highlight the matching dot */
}The trick is in the order of operations: first hide everything, then show only the one you want. This guarantees only one slide is ever visible at a time, regardless of which one was showing before. The slideIndex - 1 conversion exists because the slideshow counts from 1 (natural for humans) but arrays count from 0 (natural for computers).
The "Breaking News" section uses a horizontal card slider — you click the arrow buttons and the row of cards scrolls left or right. Unlike the slideshow above, this one does not use a JavaScript counter or a show/hide function. Instead it takes advantage of a CSS property called overflow-x: auto and simply adjusts the scrollLeft value of the container directly.
This is a much simpler approach and it is worth understanding the difference. When the number of items is fixed and small, a full index-based slideshow makes sense. When items might overflow naturally in a row, letting CSS handle the overflow and controlling scrollLeft with JavaScript is cleaner and far less code.
const slider = document.querySelector(".slider");
const nextBtn = document.querySelector(".sl-next");
const prevBtn = document.querySelector(".sl-prev");
const cardWidth = 320;
nextBtn.addEventListener("click", () => {
slider.scrollLeft += cardWidth; /* scroll right by one card */
});
prevBtn.addEventListener("click", () => {
slider.scrollLeft -= cardWidth; /* scroll left by one card */
});
/* auto-advance every 4 seconds */
setInterval(() => {
slider.scrollLeft += cardWidth;
if (slider.scrollLeft + slider.clientWidth >= slider.scrollWidth) {
slider.scrollLeft = 0; /* reset to start when reaching the end */
}
}, 4000);
/* in CSS, scroll-behavior: smooth makes the jump animate */
/* .slider { overflow-x: auto; scroll-behavior: smooth; } */scrollLeft is a built-in property on any scrollable element — it represents how many pixels the container is scrolled horizontally. Increasing it scrolls right, decreasing it scrolls left. setInterval runs a function repeatedly on a timer — the number 4000 is milliseconds, so the slider advances automatically every 4 seconds. The reset check at the end stops it from scrolling into empty space past the last card.
setInterval(fn, 4000) do?On screens narrower than 768px, the navigation links disappear and a hamburger button appears. Clicking it slides the nav in from the right side of the screen. This is a two-part system — CSS positions and hides the nav off-screen using right: -100%, and JavaScript toggles an .active class that slides it back into view. A third listener on the document closes it when you click anywhere outside.
/* CSS — nav is hidden off the right edge of the screen */
.navigation {
position: fixed;
right: -100%; /* fully off screen */
transition: right 0.3s ease;
}
.navigation.active {
right: 0; /* slide into view */
}
/* JavaScript — toggle, link-close, and outside-click */
hamburger.addEventListener("click", () => {
navMenu.classList.toggle("active");
if (navMenu.classList.contains("active")) {
hamburger.classList.add("hide"); /* hide button when menu is open */
}
});
document.addEventListener("click", (e) => {
if (!navMenu.contains(e.target) && !hamburger.contains(e.target)) {
navMenu.classList.remove("active");
hamburger.classList.remove("hide");
}
});Moving the nav off-screen with right: -100% instead of display: none is important — it keeps the CSS transition working. You cannot animate between display: none and display: block, but you can animate between two position values. The hamburger is also hidden once the menu opens (.hide sets display: none) so there is no confusion about which button to press while the nav is visible.
right: -100% used to hide the nav
instead of display: none?