Practice: Carousel, Video, Forms, Skeleton, Popup, Spinner
Build a small client carousel with local state. Keep controls keyboard-friendly. Prefer next/image for slides.
Image carousel
Build a small client carousel with local state. Keep controls keyboard-friendly. Prefer next/image for slides.
Real-life example: A carousel is a photo album you flip — one picture visible, next/prev buttons on the sides.
"use client";
import { useState } from "react";
import Image from "next/image";
const slides = [
{ src: "/slides/1.jpg", alt: "App Router diagram" },
{ src: "/slides/2.jpg", alt: "Auth flow" },
{ src: "/slides/3.jpg", alt: "Deploy checklist" },
];
export function LessonCarousel() {
const [i, setI] = useState(0);
const slide = slides[i];
return (
<div>
<Image src={slide.src} alt={slide.alt} width={800} height={450} />
<div>
<button
type="button"
onClick={() => setI((n) => (n === 0 ? slides.length - 1 : n - 1))}
>
Prev
</button>
<span>
{i + 1} / {slides.length}
</span>
<button
type="button"
onClick={() => setI((n) => (n + 1) % slides.length)}
>
Next
</button>
</div>
</div>
);
}Video player & form validation
Use the HTML5 video element for self-hosted files. For YouTube, prefer a privacy-friendly embed (next lesson).
Validate forms on the client for UX and again on the server for safety.
Real-life example: Client validation is spell-check before you send an email; server validation is the post office rejecting a blank address.
export function LessonVideo() {
return (
<video controls preload="metadata" poster="/video/poster.jpg">
<source src="/video/intro.mp4" type="video/mp4" />
Your browser does not support video.
</video>
);
}"use client";
import { useState } from "react";
export function ContactForm() {
const [email, setEmail] = useState("");
const [error, setError] = useState("");
function onSubmit(e: React.FormEvent) {
e.preventDefault();
if (!email.includes("@")) {
setError("Enter a valid email.");
return;
}
setError("");
// call Server Action or /api/contact
}
return (
<form onSubmit={onSubmit}>
<label>
Email
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</label>
{error && <p role="alert">{error}</p>}
<button type="submit">Send</button>
</form>
);
}Skeleton, popup, spinner, slider, date picker
Skeletons reduce layout jump while data loads. Modals need focus trap basics and Escape to close. Spinners need aria-busy or aria-live for accessibility.
A native range input is enough for many sliders. Prefer <input type="date"> before pulling a heavy date library.
Real-life example: A skeleton screen is the chalk outline of furniture while the real sofa is still on the delivery truck.
export function CourseSkeleton() {
return (
<div aria-hidden className="animate-pulse">
<div className="h-6 w-2/3 rounded bg-neutral-200" />
<div className="mt-3 h-4 w-full rounded bg-neutral-200" />
<div className="mt-2 h-4 w-5/6 rounded bg-neutral-200" />
</div>
);
}
export function Spinner() {
return (
<p role="status" aria-live="polite">
Loading…
</p>
);
}"use client";
import { useEffect, useState } from "react";
export function ConfirmModal() {
const [open, setOpen] = useState(false);
useEffect(() => {
if (!open) return;
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") setOpen(false);
}
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open]);
return (
<>
<button type="button" onClick={() => setOpen(true)}>
Delete lesson
</button>
{open && (
<div
role="dialog"
aria-modal="true"
aria-labelledby="confirm-title"
>
<h2 id="confirm-title">Delete this lesson?</h2>
<button type="button" onClick={() => setOpen(false)}>
Cancel
</button>
<button type="button">Confirm</button>
</div>
)}
</>
);
}"use client";
import { useState } from "react";
export function PaceControls() {
const [speed, setSpeed] = useState(1);
const [day, setDay] = useState("");
return (
<div>
<label>
Playback speed: {speed.toFixed(1)}x
<input
type="range"
min={0.5}
max={2}
step={0.1}
value={speed}
onChange={(e) => setSpeed(Number(e.target.value))}
/>
</label>
<label>
Study date
<input
type="date"
value={day}
onChange={(e) => setDay(e.target.value)}
/>
</label>
</div>
);
}