JavaScript for the HTML/CSS-First Microbiologist
A modern beginner tutorial for someone who likes HTML and CSS, understands visual structure, and is now learning when JavaScript is actually useful.
JavaScript is powerful, but it is not the answer to every frontend problem. Modern HTML and CSS can already handle many things that older tutorials used to solve with JavaScript: accordions, validation, hover states, focus states, responsive layout, basic animations, popovers, and a lot of visual switching.
So the goal is not "use JavaScript everywhere."
The goal is:
- use HTML for meaning and native behavior
- use CSS for layout, appearance, visual states, and motion
- use JavaScript for memory, decisions, data, calculations, events that change data, and communication with other places
Think of a web page like a microbiology lab.
HTML is the labeled equipment and sample layout. CSS is the staining, arrangement, lighting, spacing, and visual contrast. JavaScript is the lab assistant who counts, records, compares, responds, fetches records, and updates the notebook.
Do not make the lab assistant hold the wall paintbrush all day. Let CSS do the painting.
0. A Note Before We Start: Math Is Not the Gatekeeper
You do not need to identify as a "math person" to learn JavaScript. Frontend JavaScript is usually not advanced mathematics. It is more often naming things clearly, noticing patterns, responding to events, organizing data, and making a page feel alive.
Programming can be art. HTML gives you materials. CSS gives you composition, color, rhythm, contrast, and motion. JavaScript gives you behavior: the part of the artwork that reacts, remembers, changes, and surprises.
Math can be art too, but you do not have to believe that before you begin. Start smaller: a loop is a repeated pattern, like brush marks. A function is a motif you can reuse. A conditional is contrast: if this, then that. An array is a tray of related specimens or a palette of colors.
When JavaScript uses math, it often uses math as a tool, not as a test of identity.
const colonies = 37;
const plates = 3;
const average = colonies / plates;
console.log(Math.round(average)); // 12
Math.round is just a built-in function that rounds a number. It is not judging you. It is a tool on the bench.
The useful question is not "Am I naturally mathematical?"
The useful question is "Can I describe the next tiny step?"
That is programming.
1. The HTML/CSS-First Rule
Before writing JavaScript, ask this:
Can HTML or CSS already do this in a clear, accessible, native way?
If yes, start there.
Use JavaScript when the page needs to remember something, calculate something, fetch something, save something, build something from data, or coordinate behavior that HTML/CSS cannot express cleanly.
A practical decision list:
- Need a button hover effect? Use CSS
:hover. - Need a keyboard focus style? Use CSS
:focus-visible. - Need a required form field? Use HTML
required. - Need a number range? Use HTML
min,max, andstep. - Need a simple accordion? Use HTML
detailsandsummary. - Need a basic popover? Use HTML
popoverandpopovertarget. - Need responsive layout? Use CSS grid, flexbox, media queries, and container queries.
- Need show/hide based on a checkbox or radio? CSS may be enough.
- Need to count colonies as the user clicks? Use JavaScript.
- Need to add new samples to a list? Use JavaScript.
- Need to filter an array of sample objects? Use JavaScript.
- Need to fetch JSON from a server? Use JavaScript.
- Need to save a theme preference? Use JavaScript plus CSS.
The modern frontend skill is not "how can I force JavaScript to do this?"
The better skill is "which layer owns this job?"
A one-glance decision table:
| Task | HTML/CSS first | JavaScript earns its place when |
|---|---|---|
| Open and close a small note | details and summary |
The open state must be saved, tracked, or synchronized with app data |
| Show a small floating explanation | popover and popovertarget |
The content is generated from data or must trigger app logic |
| Validate required fields or number ranges | required, type, min, max, pattern |
Rules depend on multiple fields or existing records |
| Change appearance on hover, focus, checked, invalid, or open | CSS pseudo-classes and attributes | The visual state comes from data JavaScript calculated |
| Make layout responsive | CSS grid, flexbox, media queries, container queries | The layout depends on measured data or custom user settings |
| Build a list from samples, paintings, or API data | HTML gives the container, CSS styles it | JavaScript creates the items from data |
| Remember a theme, sample list, or filter | CSS supplies the visual theme | JavaScript stores and restores the choice |
This tutorial therefore avoids teaching JavaScript as a substitute for good HTML and CSS. It teaches JavaScript where JavaScript has a real job.
2. The Four Moves of Programming
All programming can be understood through four core moves.
Assignment
Assignment means storing a value under a name.
const organism = "E. coli";
const magnification = 400;
let colonyCount = 0;
Use const when the name should keep pointing to the same value. Use let when the value needs to change.
const species = "Saccharomyces cerevisiae";
let colonies = 12;
colonies = colonies + 3;
console.log(colonies); // 15
Default to const. Reach for let when the value is genuinely changing.
Conditional
A conditional chooses a path.
const colonyCount = 145;
if (colonyCount > 100) {
console.log("Heavy growth");
} else {
console.log("Light or moderate growth");
}
The condition becomes either true or false.
const isGramPositive = true;
if (isGramPositive) {
console.log("Use the purple palette.");
} else {
console.log("Use the pink palette.");
}
Loop
A loop repeats work.
const samples = ["soil", "pond water", "yogurt", "leaf surface"];
for (const sample of samples) {
console.log(`Prepare slide for ${sample}`);
}
Read it as: for each sample in samples, do this.
Function
A function is a reusable recipe.
function describeColony(color, shape) {
return `A ${color}, ${shape} colony`;
}
const description = describeColony("cream", "circular");
console.log(description); // A cream, circular colony
A function can accept inputs, do work, and return an output.
Those four moves appear everywhere, even inside advanced-looking browser features.
3. Values: What JavaScript Works With
JavaScript works with values. The most useful beginner values are strings, numbers, booleans, arrays, objects, null, and undefined.
Strings
A string is text.
const stain = "Crystal violet";
const note = "Incubated for 24 hours";
Template literals use backticks and let you insert values with ${...}.
const species = "Lactobacillus";
const medium = "MRS agar";
const label = `${species} grown on ${medium}`;
console.log(label);
Numbers
Numbers are for counts, measurements, positions, and calculations.
const plateA = 35;
const plateB = 47;
const total = plateA + plateB;
const average = total / 2;
console.log(average); // 41
Useful operators:
const a = 10;
const b = 3;
console.log(a + b); // 13
console.log(a - b); // 7
console.log(a * b); // 30
console.log(a / b); // 3.333...
console.log(a % b); // 1
The % operator gives the remainder. It is useful for patterns.
const brushStrokeNumber = 7;
if (brushStrokeNumber % 2 === 0) {
console.log("Use a thin stroke.");
} else {
console.log("Use a thick stroke.");
}
Booleans
A boolean is either true or false.
const isContaminated = false;
const isIncubated = true;
Comparisons create booleans.
const colonyCount = 80;
console.log(colonyCount > 50); // true
console.log(colonyCount === 80); // true
console.log(colonyCount < 10); // false
Use === for equality and !== for not equal.
const stain = "Gram stain";
if (stain === "Gram stain") {
console.log("Check purple or pink result.");
}
You can combine conditions.
const colonyCount = 150;
const isContaminated = false;
if (colonyCount > 100 && !isContaminated) {
console.log("Good candidate for subculture.");
}
Useful boolean operators:
// && means and
// || means or
// ! means not
const hasPurpleStain = true;
const hasRodShape = true;
if (hasPurpleStain && hasRodShape) {
console.log("Possibly a Gram-positive bacillus.");
}
Null and Undefined
undefined usually means JavaScript does not have a value there.
let selectedSample;
console.log(selectedSample); // undefined
null means you intentionally set the value to nothing.
let activePainting = null;
activePainting = "Watercolor colony study";
activePainting = null;
Beginner rule: use null when you deliberately mean "nothing selected yet." Treat undefined as JavaScript's default "no value was provided."
4. Assignment with const and let
Good JavaScript uses names that reduce mental load.
const x = 37;
const data = true;
const thing = "purple";
Those names force the reader to remember too much.
Better:
const colonyCount = 37;
const isGramPositive = true;
const stainColor = "purple";
For booleans, names often start with is, has, or should.
const isContaminated = false;
const hasLabel = true;
const shouldShowWarning = true;
Use let for changing values.
let colonyCount = 0;
colonyCount = colonyCount + 1;
colonyCount += 1;
colonyCount++;
console.log(colonyCount); // 3
All three increments are common. While learning, colonyCount = colonyCount + 1 is perfectly respectable because it says exactly what is happening.
5. Conditionals: Choosing the Path
A basic if statement:
const pH = 6.8;
if (pH < 7) {
console.log("Acidic");
}
An if/else statement:
const pH = 7.4;
if (pH < 7) {
console.log("Acidic");
} else {
console.log("Neutral or basic");
}
Multiple paths:
const pH = 7.4;
if (pH < 7) {
console.log("Acidic");
} else if (pH === 7) {
console.log("Neutral");
} else {
console.log("Basic");
}
A short either/or value can use the conditional operator.
const count = 120;
const message = count > 100 ? "Heavy growth" : "Normal growth";
console.log(message);
Use this for short choices. Use if/else when the logic needs room.
HTML/CSS Alternative: Do Not Use JS for Simple Visual States
If the only decision is visual, CSS is often the right layer.
Bad reason to use JavaScript:
const button = document.querySelector("button");
button.addEventListener("mouseenter", () => {
button.style.backgroundColor = "purple";
});
button.addEventListener("mouseleave", () => {
button.style.backgroundColor = "white";
});
Better CSS:
button:hover {
background: purple;
color: white;
}
button:focus-visible {
outline: 3px solid purple;
outline-offset: 3px;
}
JavaScript should not micromanage hover paint. CSS is built for that.
6. Arrays: Trays of Values
An array is an ordered list.
const samples = ["pond water", "soil", "yogurt", "leaf surface"];
Use arrays when you have multiple values of the same kind.
const pigments = ["ultramarine", "burnt sienna", "sap green"];
const colonyCounts = [12, 45, 8, 101];
Indexes start at 0.
const samples = ["pond water", "soil", "yogurt"];
console.log(samples[0]); // pond water
console.log(samples[1]); // soil
console.log(samples[2]); // yogurt
Arrays have a length.
const samples = ["pond water", "soil", "yogurt"];
console.log(samples.length); // 3
You can add an item.
const samples = ["pond water", "soil"];
samples.push("yogurt");
console.log(samples); // ["pond water", "soil", "yogurt"]
This is allowed even though samples uses const. The name still points to the same array; the contents of the array changed.
Looping Through Arrays
Use for...of when you want to do something with every item.
const samples = ["pond water", "soil", "yogurt"];
for (const sample of samples) {
console.log(`Label jar: ${sample}`);
}
Use a traditional for loop when you need the index.
const samples = ["soil", "water", "yogurt"];
for (let index = 0; index < samples.length; index++) {
console.log(`${index + 1}. ${samples[index]}`);
}
Useful Array Methods
includes asks whether an array contains a value.
const stains = ["crystal violet", "safranin", "iodine"];
if (stains.includes("safranin")) {
console.log("Counterstain is available.");
}
map creates a new array by transforming each item.
const samples = ["soil", "water", "yogurt"];
const labels = samples.map((sample) => {
return `Sample: ${sample}`;
});
console.log(labels);
filter creates a new array with only the items that pass a test.
const colonyCounts = [12, 145, 3, 87, 220];
const heavyGrowthCounts = colonyCounts.filter((count) => {
return count > 100;
});
console.log(heavyGrowthCounts); // [145, 220]
find returns the first item that passes a test.
const samples = [
"soil - normal",
"pond water - contaminated",
"yogurt - normal"
];
const contaminatedSample = samples.find((sample) => {
return sample.includes("contaminated");
});
console.log(contaminatedSample);
forEach performs an action for every item.
const colors = ["purple", "pink", "cream"];
colors.forEach((color) => {
console.log(`Mix paint: ${color}`);
});
Beginner guideline:
- Use
for...ofwhen clarity matters. - Use
mapwhen you want a transformed array. - Use
filterwhen you want a smaller array. - Use
findwhen you want one matching item. - Use
forEachwhen you want to perform an action for every item.
7. Objects: Sample Cards
An object groups related information.
const sample = {
name: "Pond water A",
source: "campus pond",
colonyCount: 87,
isContaminated: false
};
Think of an object as a sample card.
console.log(sample.name); // Pond water A
console.log(sample.colonyCount); // 87
You can update properties.
sample.colonyCount = 92;
console.log(sample.colonyCount); // 92
Objects can contain arrays and other objects.
const painting = {
title: "Colony Bloom",
medium: "watercolor",
colors: ["violet", "rose", "warm gray"],
dimensions: {
width: 24,
height: 18,
unit: "cm"
}
};
console.log(painting.colors[0]); // violet
console.log(painting.dimensions.width); // 24
Real frontend data is often an array of objects.
const samples = [
{
id: 1,
name: "Soil sample",
colonyCount: 120,
stain: "purple"
},
{
id: 2,
name: "Yogurt sample",
colonyCount: 45,
stain: "pink"
},
{
id: 3,
name: "Leaf surface",
colonyCount: 8,
stain: "cream"
}
];
Now you can loop and filter.
for (const sample of samples) {
console.log(`${sample.name}: ${sample.colonyCount} colonies`);
}
const highGrowthSamples = samples.filter((sample) => {
return sample.colonyCount > 100;
});
Objects are where JavaScript starts to feel useful for real data.
8. Functions: Protocols You Can Reuse
A function packages logic.
function calculateAverage(a, b) {
return (a + b) / 2;
}
const average = calculateAverage(40, 50);
console.log(average); // 45
Parameters are the names inside the function. Arguments are the actual values you pass in.
function makeSampleLabel(sampleName, count) {
return `${sampleName}: ${count} colonies`;
}
const label = makeSampleLabel("Soil A", 128);
console.log(label);
A function without return gives back undefined.
function logSample(sampleName) {
console.log(sampleName);
}
const result = logSample("Pond water");
console.log(result); // undefined
That is not bad. Some functions calculate values. Other functions do actions.
Pure Functions
A pure function depends only on its inputs and returns an output without changing the outside world.
function isHeavyGrowth(colonyCount) {
return colonyCount > 100;
}
console.log(isHeavyGrowth(80)); // false
console.log(isHeavyGrowth(150)); // true
Pure functions are easy to test because the same input always gives the same output.
Functions with Effects
Some functions intentionally change the page.
function showWarning(message) {
const warning = document.querySelector("#warning");
warning.textContent = message;
}
showWarning("Possible contamination detected.");
This is a side effect. Frontend JavaScript needs side effects because pages must update.
A good habit is to separate calculation from DOM updates.
function isHeavyGrowth(colonyCount) {
return colonyCount > 100;
}
function updateGrowthMessage(colonyCount) {
const message = document.querySelector("#growthMessage");
if (isHeavyGrowth(colonyCount)) {
message.textContent = "Heavy growth";
} else {
message.textContent = "Normal growth";
}
}
Arrow Functions
Modern JavaScript often uses arrow functions.
const isHeavyGrowth = (colonyCount) => {
return colonyCount > 100;
};
Shorter version:
const isHeavyGrowth = (colonyCount) => colonyCount > 100;
Arrow functions are especially common with array methods and events.
const highCounts = colonyCounts.filter((count) => count > 100);
button.addEventListener("click", () => {
console.log("Button clicked");
});
9. Scope: Where a Name Exists
Scope means where a variable can be used.
const organism = "E. coli";
function printOrganism() {
console.log(organism);
}
printOrganism(); // E. coli
A variable created inside a function only exists inside that function.
function prepareSlide() {
const stain = "crystal violet";
console.log(stain);
}
prepareSlide();
// console.log(stain); // not available here
Think of scope as bench space. A reagent on the main bench can be used by nearby protocols. A reagent inside one closed box is not available outside that box.
Blocks also create scope.
if (true) {
const note = "Visible only inside this block";
console.log(note);
}
// console.log(note); // not available here
10. Native HTML/CSS That Replaces Common JavaScript
This is the section that many older JavaScript tutorials miss.
A lot of examples that used to require JavaScript are now better as HTML and CSS.
Disclosure and Accordions: details and summary
Do not write JavaScript just to show and hide a small answer.
Use HTML:
<details>
<summary>Why did the colony turn purple?</summary>
<p>The cells retained crystal violet during the Gram stain process.</p>
</details>
For an accordion where only one section should be open at a time, modern HTML supports grouping details elements with name.
<details name="stain-notes" open>
<summary>Crystal violet</summary>
<p>The primary stain.</p>
</details>
<details name="stain-notes">
<summary>Iodine</summary>
<p>The mordant that helps form the dye complex.</p>
</details>
<details name="stain-notes">
<summary>Safranin</summary>
<p>The counterstain.</p>
</details>
Then style it with CSS.
details {
border: 1px solid #ddd;
border-radius: 0.75rem;
padding: 0.75rem 1rem;
}
summary {
cursor: pointer;
font-weight: 700;
}
details[open] {
border-color: purple;
}
Use JavaScript only if opening the section must update data, analytics, local storage, or some other state outside the disclosure itself.
Basic Form Validation: HTML First
Do not write JavaScript just to check that a required field is empty.
Use HTML attributes.
<form id="sampleForm">
<label>
Sample name
<input id="sampleName" required minlength="2" />
</label>
<label>
Colony count
<input id="colonyCount" type="number" min="0" max="10000" required />
</label>
<label>
Sample code
<input id="sampleCode" pattern="[A-Z]{2}-[0-9]{3}" placeholder="AB-123" />
</label>
<button>Add sample</button>
</form>
CSS can style validity states.
input:invalid {
border-color: crimson;
}
input:valid {
border-color: seagreen;
}
form:has(input:invalid) .hint {
display: block;
}
Use JavaScript when validation depends on app data or relationships between fields.
Example: a sample cannot have a colony count if it is marked "not incubated yet." That is a real conditional based on multiple values.
function validateSample({ colonyCount, isIncubated }) {
if (!isIncubated && colonyCount > 0) {
return "A non-incubated sample should not have a colony count yet.";
}
return null;
}
Popovers: HTML Can Do Basic Ones
For a small floating explanation, modern HTML has popovers.
<button popovertarget="gramHelp">What is a Gram stain?</button>
<div id="gramHelp" popover>
A Gram stain helps classify bacteria by cell wall structure and staining
result.
</div>
That needs no JavaScript for the basic open/close behavior.
Use JavaScript only if the popover content is generated from data or if opening it must affect app state.
Dialogs: Use the Native Element, Add Only Small JS
For a modal, avoid building a fake modal from many divs if the native dialog element works.
HTML:
<button id="openDialog">Show sample note</button>
<dialog id="sampleDialog">
<h2>Sample note</h2>
<p>Heavy growth observed after 24 hours.</p>
<form method="dialog">
<button>Close</button>
</form>
</dialog>
JavaScript:
const openDialog = document.querySelector("#openDialog");
const sampleDialog = document.querySelector("#sampleDialog");
openDialog.addEventListener("click", () => {
sampleDialog.showModal();
});
The JavaScript is tiny because HTML handles the dialog behavior.
CSS State Selectors
CSS understands many states without JavaScript.
button:hover {
transform: translateY(-1px);
}
button:active {
transform: translateY(0);
}
input:focus-visible {
outline: 3px solid purple;
}
.card:has(input:checked) {
border-color: purple;
background: lavender;
}
If the state is visual and already exists in HTML, CSS should usually handle it.
Motion: CSS First
Do not use JavaScript timers for simple fades or hover movement.
.card {
transition: transform 180ms ease, box-shadow 180ms ease;
}
.card:hover {
transform: translateY(-3px);
box-shadow: 0 16px 30px rgb(0 0 0 / 0.12);
}
@media (prefers-reduced-motion: reduce) {
.card {
transition: none;
}
}
Use JavaScript for animation only when the animation depends on changing data, measured layout, or a sequence that CSS cannot express clearly.
11. The DOM: JavaScript Meets the Page
The DOM is the browser's live object version of your HTML.
Given this HTML:
<h1 id="title">Lab Notes</h1>
<p class="note">No sample selected.</p>
<button id="selectButton">Select sample</button>
JavaScript can find elements.
const title = document.querySelector("#title");
const note = document.querySelector(".note");
const button = document.querySelector("#selectButton");
querySelector uses CSS selector syntax.
document.querySelector("#title"); // id
document.querySelector(".note"); // class
document.querySelector("button"); // tag
document.querySelector(".sample-card strong"); // descendant
To find multiple elements, use querySelectorAll.
const cards = document.querySelectorAll(".sample-card");
for (const card of cards) {
console.log(card.textContent);
}
Changing Text
const note = document.querySelector(".note");
note.textContent = "Sample selected: Pond water A";
Use textContent for plain text, especially user-provided text.
Changing Classes, Not Micromanaging Styles
Let JavaScript decide that something changed. Let CSS decide how that change looks.
CSS:
.sample-card.warning {
border-color: crimson;
background: mistyrose;
}
JavaScript:
const card = document.querySelector(".sample-card");
const isContaminated = true;
if (isContaminated) {
card.classList.add("warning");
} else {
card.classList.remove("warning");
}
This is better than setting five style properties in JavaScript.
But do not use JavaScript for states CSS already understands. Hover, focus, checked, invalid, and open states belong in CSS.
button:hover {
transform: translateY(-1px);
}
input:invalid {
border-color: crimson;
}
details[open] {
border-color: purple;
}
.card:has(input:checked) {
background: lavender;
}
Use classList when the class depends on app state that JavaScript calculated: contamination status, current filter, saved theme, selected sample, or data returned from a server.
Direct style changes are fine when the value is genuinely dynamic.
const swatch = document.querySelector(".swatch");
const selectedColor = "purple";
swatch.style.backgroundColor = selectedColor;
If the color is one of a few known options, a class may be cleaner.
swatch.classList.add("swatch-purple");
Creating Elements
JavaScript is appropriate when new DOM elements come from data.
const list = document.querySelector("#sampleList");
const item = document.createElement("li");
item.textContent = "Soil sample: 120 colonies";
list.append(item);
This is like preparing a new labeled slide and placing it into a tray.
12. Events: Let the Page React
An event is something that happens: a click, a key press, a form submission, a change, or a page load.
addEventListener means: when this event happens, run this function.
const button = document.querySelector("#saveButton");
button.addEventListener("click", () => {
console.log("Save button clicked");
});
The second argument is a function. It does not run immediately. The browser stores it and runs it later when the event happens.
function handleClick() {
console.log("Clicked");
}
button.addEventListener("click", handleClick);
Do not add parentheses when you are handing the function to the browser.
button.addEventListener("click", handleClick); // correct
button.addEventListener("click", handleClick()); // runs too early
The Event Object
The browser can pass information about the event into your function.
const input = document.querySelector("#sampleName");
input.addEventListener("input", (event) => {
console.log(event.target.value);
});
event.target is the element where the event happened. For an input, event.target.value is the current typed value.
A Toy addEventListener Under the Hood
This is not the real browser code, but it shows the idea.
function createTinyButton() {
const clickCallbacks = [];
return {
addEventListener(eventName, callback) {
if (eventName === "click") {
clickCallbacks.push(callback);
}
},
click() {
for (const callback of clickCallbacks) {
callback();
}
}
};
}
const tinyButton = createTinyButton();
tinyButton.addEventListener("click", () => {
console.log("The tiny button was clicked.");
});
tinyButton.click();
The internals still use the four moves: assignment, conditionals, loops, and functions.
Browser APIs feel advanced because someone else already wrote many functions for you. You do not need to rebuild the microscope. You need to learn the focus knob.
13. Forms: HTML Validates, JavaScript Processes
Start with semantic HTML and native validation.
HTML:
<form id="sampleForm">
<label>
Sample name
<input id="sampleName" required minlength="2" />
</label>
<label>
Colony count
<input id="colonyCount" type="number" min="0" required />
</label>
<button>Add sample</button>
</form>
<ul id="sampleList"></ul>
CSS:
input:invalid {
border-color: crimson;
}
input:valid {
border-color: seagreen;
}
JavaScript should process the valid data and update state.
const form = document.querySelector("#sampleForm");
const sampleNameInput = document.querySelector("#sampleName");
const colonyCountInput = document.querySelector("#colonyCount");
const sampleList = document.querySelector("#sampleList");
form.addEventListener("submit", (event) => {
event.preventDefault();
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const sampleName = sampleNameInput.value;
const colonyCount = Number(colonyCountInput.value);
const item = document.createElement("li");
item.textContent = `${sampleName}: ${colonyCount} colonies`;
sampleList.append(item);
form.reset();
});
Input values are strings by default. Use Number(...) when you want a number.
const typedValue = "42";
const numberValue = Number(typedValue);
console.log(numberValue + 1); // 43
Without Number, + joins text.
const typedValue = "42";
console.log(typedValue + 1); // "421"
14. State and Render: The Pattern That Unlocks Frontend
Frontend apps become easier when you separate two ideas:
- state: the data right now
- render: the function that displays that data
State is the lab notebook. Render is drawing the current notebook page onto the screen.
HTML:
<form id="sampleForm">
<input id="sampleName" required placeholder="Sample name" />
<input
id="colonyCount"
type="number"
min="0"
required
placeholder="Colonies"
/>
<button>Add sample</button>
</form>
<ul id="sampleList"></ul>
<p id="summary"></p>
JavaScript:
const form = document.querySelector("#sampleForm");
const sampleNameInput = document.querySelector("#sampleName");
const colonyCountInput = document.querySelector("#colonyCount");
const sampleList = document.querySelector("#sampleList");
const summary = document.querySelector("#summary");
const samples = [];
function renderSamples() {
sampleList.innerHTML = "";
for (const sample of samples) {
const item = document.createElement("li");
item.textContent = `${sample.name}: ${sample.colonyCount} colonies`;
sampleList.append(item);
}
summary.textContent = `Total samples: ${samples.length}`;
}
form.addEventListener("submit", (event) => {
event.preventDefault();
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const newSample = {
name: sampleNameInput.value,
colonyCount: Number(colonyCountInput.value)
};
samples.push(newSample);
renderSamples();
form.reset();
});
renderSamples();
The flow is:
- User does something.
- Update state.
- Render the page from state.
That pattern later appears in frameworks like React, but you can learn it in vanilla JavaScript first.
Why This Is JavaScript-Worthy
CSS can style a list. HTML can define a form. But CSS cannot maintain an array of sample objects, calculate totals, or add new samples from user input.
This is where JavaScript belongs.
15. innerHTML, textContent, and Safety
innerHTML is convenient.
sampleList.innerHTML = "";
That line clears the list. This is fine.
But avoid using innerHTML with user-provided text.
const userTypedName = sampleNameInput.value;
sampleList.innerHTML = `<li>${userTypedName}</li>`;
If someone types text that looks like HTML, the browser may treat it as HTML.
Safer version:
const item = document.createElement("li");
item.textContent = userTypedName;
sampleList.append(item);
Beginner-safe rule:
- Use
textContentfor user-provided text. - Use
createElementandappendwhen building DOM from user input. - Use
innerHTMLmostly for clearing or for HTML you fully control.
16. Destructuring and Spread
Destructuring pulls values out of objects or arrays.
const sample = {
name: "Soil",
colonyCount: 140,
stain: "purple"
};
const { name, colonyCount } = sample;
console.log(name); // Soil
console.log(colonyCount); // 140
It is useful in functions.
function makeSampleLabel({ name, colonyCount }) {
return `${name}: ${colonyCount} colonies`;
}
const sample = {
name: "Pond water",
colonyCount: 210
};
console.log(makeSampleLabel(sample));
Spread syntax uses ... to copy or combine.
const stains = ["crystal violet", "iodine"];
const updatedStains = [...stains, "safranin"];
console.log(updatedStains);
With objects:
const sample = {
name: "Soil",
colonyCount: 140
};
const updatedSample = {
...sample,
colonyCount: 150
};
console.log(updatedSample);
Do not obsess over this early. Recognize it, use it when it improves clarity, and move on.
17. Modules: Splitting Code into Files
When JavaScript grows, split it into modules.
sampleUtils.js:
export function isHeavyGrowth(colonyCount) {
return colonyCount > 100;
}
export function makeSampleLabel(sample) {
return `${sample.name}: ${sample.colonyCount} colonies`;
}
main.js:
import { isHeavyGrowth, makeSampleLabel } from "./sampleUtils.js";
const sample = {
name: "Soil",
colonyCount: 140
};
console.log(makeSampleLabel(sample));
console.log(isHeavyGrowth(sample.colonyCount));
In your HTML, use type="module".
<script type="module" src="main.js"></script>
Modules help you keep files organized. You do not need them for every tiny project.
18. Fetch, JSON, and Async
Sometimes your page needs data from another place. JavaScript uses fetch for that.
async function loadSamples() {
const response = await fetch("/samples.json");
const samples = await response.json();
console.log(samples);
}
loadSamples();
fetch is asynchronous. The browser starts the request and continues functioning while waiting.
Think of it like sending a sample to another lab. You do not freeze while waiting. You send the request, wait for the result, and continue when it returns.
A more complete example:
const sampleList = document.querySelector("#sampleList");
const statusMessage = document.querySelector("#statusMessage");
async function loadSamples() {
statusMessage.textContent = "Loading samples...";
try {
const response = await fetch("/samples.json");
if (!response.ok) {
throw new Error("Could not load samples");
}
const samples = await response.json();
sampleList.innerHTML = "";
for (const sample of samples) {
const item = document.createElement("li");
item.textContent = `${sample.name}: ${sample.colonyCount} colonies`;
sampleList.append(item);
}
statusMessage.textContent = "Samples loaded.";
} catch (error) {
statusMessage.textContent = "Could not load samples.";
console.error(error);
}
}
loadSamples();
JSON is a data format that looks like JavaScript objects.
{
"name": "Soil sample",
"colonyCount": 140,
"isContaminated": false
}
When using fetch, response.json() converts JSON into JavaScript values.
19. Local Storage: Remembering Small Things
localStorage saves small strings in the browser.
localStorage.setItem("favoriteColor", "violet");
const favoriteColor = localStorage.getItem("favoriteColor");
console.log(favoriteColor); // violet
Because local storage stores strings, arrays and objects need JSON.
const samples = [
{ name: "Soil", colonyCount: 140 },
{ name: "Yogurt", colonyCount: 45 }
];
localStorage.setItem("samples", JSON.stringify(samples));
Read them back:
const savedSamples = localStorage.getItem("samples");
const samples = savedSamples ? JSON.parse(savedSamples) : [];
console.log(samples);
Local storage is good for small beginner projects: theme choice, draft notes, a sketchbook catalog, or saved sample lists. It is not a real database.
20. Mini Project: Native Lab Notes Accordion
This project intentionally uses no JavaScript.
HTML:
<section class="lab-notes">
<details name="lab-notes" open>
<summary>Gram stain result</summary>
<p>Purple rods observed under oil immersion.</p>
</details>
<details name="lab-notes">
<summary>Colony morphology</summary>
<p>Cream, circular colonies with smooth margins.</p>
</details>
<details name="lab-notes">
<summary>Art study idea</summary>
<p>Use violet washes with small repeating rod forms.</p>
</details>
</section>
CSS:
.lab-notes {
display: grid;
gap: 0.75rem;
}
.lab-notes details {
border: 1px solid #d8cfe8;
border-radius: 1rem;
padding: 0.85rem 1rem;
}
.lab-notes summary {
cursor: pointer;
font-weight: 700;
}
.lab-notes details[open] {
background: #f3edff;
border-color: #8057ff;
}
Lesson: if the browser already gives you accessible open/close behavior, do not recreate it with JavaScript just for practice. Practice JavaScript where JavaScript is the right tool.
21. Mini Project: Colony Counter
This one is JavaScript-worthy because the page needs memory. It must remember the current count.
HTML:
<h1>Colony Counter</h1>
<button id="addButton">Add colony</button>
<button id="resetButton">Reset</button>
<p id="countDisplay">Colonies: 0</p>
CSS:
#countDisplay.heavy-growth {
color: crimson;
font-weight: 800;
}
JavaScript:
const addButton = document.querySelector("#addButton");
const resetButton = document.querySelector("#resetButton");
const countDisplay = document.querySelector("#countDisplay");
let colonyCount = 0;
function renderCount() {
countDisplay.textContent = `Colonies: ${colonyCount}`;
if (colonyCount > 100) {
countDisplay.classList.add("heavy-growth");
} else {
countDisplay.classList.remove("heavy-growth");
}
}
addButton.addEventListener("click", () => {
colonyCount = colonyCount + 1;
renderCount();
});
resetButton.addEventListener("click", () => {
colonyCount = 0;
renderCount();
});
renderCount();
Notice the split:
- JavaScript owns the count and the decision that growth is heavy.
- CSS owns how heavy growth looks.
That is the right relationship.
22. Mini Project: Micro Gallery Filter
There are two versions of this idea.
If all gallery cards are static HTML, CSS may be enough. If the gallery comes from data, JavaScript is appropriate.
Static CSS Version
HTML:
<form class="filters">
<label>
<input type="radio" name="filter" value="all" checked />
All
</label>
<label>
<input type="radio" name="filter" value="bacteria" />
Bacteria
</label>
<label>
<input type="radio" name="filter" value="fungi" />
Fungi
</label>
</form>
<div class="gallery">
<article class="card" data-category="bacteria">Purple Rods</article>
<article class="card" data-category="fungi">Yeast Constellation</article>
<article class="card" data-category="bacteria">Pink Cocci</article>
</div>
CSS idea:
body:has(input[value="bacteria"]:checked)
.card:not([data-category="bacteria"]) {
display: none;
}
body:has(input[value="fungi"]:checked)
.card:not([data-category="fungi"]) {
display: none;
}
For a small fixed gallery, this is elegant.
Data-Driven JavaScript Version
Use JavaScript when the cards come from an array.
HTML:
<div class="toolbar">
<button data-filter="all">All</button>
<button data-filter="bacteria">Bacteria</button>
<button data-filter="fungi">Fungi</button>
<button data-filter="art">Art studies</button>
</div>
<div id="gallery" class="gallery"></div>
JavaScript:
const gallery = document.querySelector("#gallery");
const filterButtons = document.querySelectorAll("[data-filter]");
const studies = [
{
title: "Purple Rods",
category: "bacteria",
description: "A Gram-stain inspired color study."
},
{
title: "Yeast Constellation",
category: "fungi",
description: "Circular forms arranged like stars."
},
{
title: "Agar Bloom",
category: "art",
description: "Watercolor shapes inspired by colony edges."
},
{
title: "Pink Cocci",
category: "bacteria",
description: "Small round forms with a warm counterstain palette."
}
];
let currentFilter = "all";
function getVisibleStudies() {
if (currentFilter === "all") {
return studies;
}
return studies.filter((study) => study.category === currentFilter);
}
function createStudyCard(study) {
const card = document.createElement("article");
card.classList.add("card");
const title = document.createElement("h2");
title.textContent = study.title;
const category = document.createElement("p");
category.textContent = study.category;
const description = document.createElement("p");
description.textContent = study.description;
card.append(title, category, description);
return card;
}
function renderGallery() {
gallery.innerHTML = "";
const visibleStudies = getVisibleStudies();
for (const study of visibleStudies) {
const card = createStudyCard(study);
gallery.append(card);
}
}
for (const button of filterButtons) {
button.addEventListener("click", () => {
currentFilter = button.dataset.filter;
renderGallery();
});
}
renderGallery();
Lesson: CSS is excellent when the content already exists in HTML. JavaScript is better when the content is data.
23. Mini Project: Stain Palette Picker
If the palette is only visual, CSS can do a lot.
HTML:
<form class="palette-picker">
<label>
<input type="radio" name="stain" value="positive" checked />
Gram positive
</label>
<label>
<input type="radio" name="stain" value="negative" />
Gram negative
</label>
</form>
<div class="palette-preview">
<span class="swatch one"></span>
<span class="swatch two"></span>
<span class="swatch three"></span>
</div>
CSS:
.palette-preview .swatch {
display: inline-block;
width: 3rem;
height: 3rem;
border-radius: 999px;
}
body:has(input[value="positive"]:checked) .one {
background: purple;
}
body:has(input[value="positive"]:checked) .two {
background: lavender;
}
body:has(input[value="negative"]:checked) .one {
background: pink;
}
body:has(input[value="negative"]:checked) .two {
background: coral;
}
But if the palettes are data and the description changes, JavaScript is clearer.
const stainSelect = document.querySelector("#stainSelect");
const palette = document.querySelector("#palette");
const paletteDescription = document.querySelector("#paletteDescription");
const palettes = {
gramPositive: {
colors: ["purple", "lavender", "deep violet"],
description: "Inspired by crystal violet retention."
},
gramNegative: {
colors: ["pink", "rose", "coral"],
description: "Inspired by safranin counterstain."
},
endospore: {
colors: ["green", "soft pink", "cream"],
description: "Inspired by malachite green and counterstain contrast."
}
};
function renderPalette() {
const selectedKey = stainSelect.value;
const selectedPalette = palettes[selectedKey];
palette.innerHTML = "";
for (const color of selectedPalette.colors) {
const swatch = document.createElement("div");
swatch.classList.add("swatch");
swatch.textContent = color;
swatch.style.backgroundColor = color;
palette.append(swatch);
}
paletteDescription.textContent = selectedPalette.description;
}
stainSelect.addEventListener("change", renderPalette);
renderPalette();
Lesson: if it is a visual switch, CSS may be best. If it is a data lookup and render, JavaScript is best.
24. Mini Project: Culture Sketchbook
This is the slightly bigger project: a tiny personal app for saving microbiology notes, stain palettes, and art-study ideas as cards.
It is still small enough to understand, but it behaves like a real frontend feature:
- HTML provides the form, labels, validation, and reusable card template.
- CSS creates the layout, visual rhythm, focus states, and responsive cards.
- JavaScript remembers the cards, renders them, filters them, deletes them, and saves them in
localStorage.
The shape is:
form input -> JavaScript state -> render cards -> save state
That pattern appears everywhere in frontend development.
HTML:
<main class="culture-sketchbook">
<header class="sketchbook-intro">
<p class="eyebrow">mini app</p>
<h1>Culture Sketchbook</h1>
<p>
Save microbiology notes, stain palettes, and art-study ideas as cards.
</p>
</header>
<form id="studyForm" class="entry-form">
<label>
Card title
<input
id="title"
name="title"
type="text"
minlength="2"
placeholder="Agar bloom study"
required
/>
</label>
<label>
Type
<select name="type" required>
<option value="culture">Culture note</option>
<option value="stain">Stain palette</option>
<option value="art">Art study</option>
</select>
</label>
<label>
Mood color
<input name="color" type="color" value="#7b4dff" />
</label>
<label class="field-full">
Observation or idea
<textarea
name="note"
maxlength="220"
placeholder="Cream colonies, violet shadows, soft edges..."
required
></textarea>
</label>
<button class="primary-button">Add card</button>
</form>
<section class="filter-bar" aria-label="Filter sketchbook cards">
<button type="button" data-filter="all" aria-pressed="true">All</button>
<button type="button" data-filter="culture">Culture</button>
<button type="button" data-filter="stain">Stain</button>
<button type="button" data-filter="art">Art</button>
</section>
<section id="cardGrid" class="card-grid" aria-live="polite"></section>
<template id="cardTemplate">
<article class="study-card">
<div class="study-card__top">
<span class="study-card__swatch" aria-hidden="true"></span>
<div>
<h2></h2>
<p class="study-card__meta"></p>
</div>
</div>
<p class="study-card__note"></p>
<footer>
<span class="study-card__date"></span>
<button type="button" data-action="delete">Delete</button>
</footer>
</article>
</template>
</main>
Useful HTML choices:
- Wrapping an
inputinside alabelkeeps the label and field connected. required,minlength, andmaxlengthlet the browser handle basic validation.templateis a clean place to store HTML that JavaScript will clone.aria-live="polite"is a small accessibility improvement for a section that changes.
CSS:
.culture-sketchbook {
--accent: #6f55ff;
max-width: 64rem;
margin-inline: auto;
padding: clamp(1rem, 4vw, 3rem);
color: #251d19;
}
.sketchbook-intro {
max-width: 48rem;
margin-block-end: 1.5rem;
}
.eyebrow {
color: var(--accent);
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.entry-form {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
gap: 1rem;
border: 1px solid #dfcdbb;
border-radius: 1.25rem;
padding: 1rem;
background: #fff9ef;
}
.entry-form label {
display: grid;
gap: 0.35rem;
font-weight: 700;
}
.entry-form .field-full {
grid-column: 1 / -1;
}
.entry-form :is(input, select, textarea, button) {
font: inherit;
}
.entry-form :is(input, select, textarea) {
width: 100%;
border: 1px solid #d8c8bb;
border-radius: 0.8rem;
padding: 0.7rem 0.8rem;
background: white;
}
.entry-form :is(input, select, textarea):focus-visible {
outline: 3px solid color-mix(in srgb, var(--accent) 35%, white);
border-color: var(--accent);
}
.entry-form textarea {
min-height: 7rem;
resize: vertical;
}
.primary-button,
.filter-bar button,
.study-card button {
border: 0;
border-radius: 999px;
padding: 0.7rem 1rem;
cursor: pointer;
font-weight: 800;
}
.primary-button,
.filter-bar button[aria-pressed="true"] {
color: white;
background: var(--accent);
}
.filter-bar {
display: flex;
flex-wrap: wrap;
gap: 0.55rem;
margin-block: 1rem;
}
.filter-bar button {
color: #4630c9;
background: #efeaff;
}
.card-grid {
container-type: inline-size;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
gap: 1rem;
}
.empty-state {
grid-column: 1 / -1;
border: 1px dashed #c9b9de;
border-radius: 1rem;
padding: 1rem;
color: #63554e;
background: #fff4df;
}
.study-card {
--card-color: #6f55ff;
display: grid;
gap: 0.9rem;
border: 1px solid color-mix(in srgb, var(--card-color) 30%, #dfcdbb);
border-radius: 1.25rem;
padding: 1rem;
background:
radial-gradient(
circle at top left,
color-mix(in srgb, var(--card-color) 20%, transparent),
transparent 9rem
),
#fffaf4;
}
.study-card__top {
display: flex;
gap: 0.75rem;
align-items: start;
}
.study-card__swatch {
flex: 0 0 auto;
width: 2.8rem;
aspect-ratio: 1;
border-radius: 999px;
background: var(--card-color);
}
.study-card h2,
.study-card p {
margin: 0;
}
.study-card h2 {
font-size: 1.05rem;
}
.study-card__meta,
.study-card__date {
color: #63554e;
font-size: 0.9rem;
}
.study-card footer {
display: flex;
justify-content: space-between;
gap: 0.75rem;
align-items: center;
}
.study-card button {
color: #7a2448;
background: #ffe3ed;
}
@container (max-width: 22rem) {
.study-card footer {
display: grid;
}
}
Useful CSS choices:
auto-fitandminmax()make the form and cards responsive without JavaScript.:is()keeps grouped selectors readable.:focus-visiblegives keyboard users a clear focus ring.--card-colorlets JavaScript hand one value to CSS, then CSS handles the look.- The container query adjusts each card according to its own space.
JavaScript:
const form = document.querySelector("#studyForm");
const titleInput = document.querySelector("#title");
const grid = document.querySelector("#cardGrid");
const template = document.querySelector("#cardTemplate");
const filterButtons = document.querySelectorAll("[data-filter]");
const storageKey = "culture-sketchbook-v1";
const starterStudies = [
{
id: "starter-1",
title: "Violet rod rhythm",
type: "stain",
color: "#7b4dff",
note: "Crystal-violet shapes repeating like brush marks.",
date: "starter"
},
{
id: "starter-2",
title: "Agar bloom edges",
type: "art",
color: "#e98565",
note: "Soft circular colonies as a watercolor composition.",
date: "starter"
}
];
let studies = loadStudies();
let currentFilter = "all";
function loadStudies() {
const savedStudies = localStorage.getItem(storageKey);
try {
return savedStudies ? JSON.parse(savedStudies) : starterStudies;
} catch {
return starterStudies;
}
}
function saveStudies() {
localStorage.setItem(storageKey, JSON.stringify(studies));
}
function getVisibleStudies() {
if (currentFilter === "all") {
return studies;
}
return studies.filter((study) => study.type === currentFilter);
}
function createStudyFromForm() {
const formData = new FormData(form);
return {
id: crypto.randomUUID(),
title: String(formData.get("title")).trim(),
type: String(formData.get("type")),
color: String(formData.get("color")),
note: String(formData.get("note")).trim(),
date: new Date().toLocaleDateString(undefined, {
month: "short",
day: "numeric"
})
};
}
function createCard(study) {
const card = template.content.firstElementChild.cloneNode(true);
card.dataset.id = study.id;
card.style.setProperty("--card-color", study.color);
card.querySelector("h2").textContent = study.title;
card.querySelector(".study-card__meta").textContent = study.type;
card.querySelector(".study-card__note").textContent = study.note;
card.querySelector(".study-card__date").textContent = study.date;
const deleteButton = card.querySelector("[data-action='delete']");
deleteButton.ariaLabel = `Delete ${study.title}`;
return card;
}
function renderCards() {
const visibleStudies = getVisibleStudies();
if (visibleStudies.length === 0) {
const message = document.createElement("p");
message.classList.add("empty-state");
message.textContent = "No cards match this filter yet.";
grid.replaceChildren(message);
return;
}
grid.replaceChildren(...visibleStudies.map(createCard));
}
function updateFilterButtons() {
for (const button of filterButtons) {
const isSelected = button.dataset.filter === currentFilter;
button.setAttribute("aria-pressed", String(isSelected));
}
}
form.addEventListener("submit", (event) => {
event.preventDefault();
const study = createStudyFromForm();
studies = [study, ...studies];
saveStudies();
renderCards();
form.reset();
titleInput.focus();
});
for (const button of filterButtons) {
button.addEventListener("click", () => {
currentFilter = button.dataset.filter;
updateFilterButtons();
renderCards();
});
}
grid.addEventListener("click", (event) => {
const deleteButton = event.target.closest("[data-action='delete']");
if (!deleteButton) {
return;
}
const card = deleteButton.closest(".study-card");
studies = studies.filter((study) => study.id !== card.dataset.id);
saveStudies();
renderCards();
});
updateFilterButtons();
renderCards();
Useful JavaScript choices:
studiesis the app's memory.currentFilteris memory for the current view.FormDatareads the form without manually selecting every input.renderCardsmakes the page match the current data.localStoragekeeps the sketchbook after refresh.- One click listener on
gridhandles all delete buttons, even buttons created later.
That final idea is event delegation. Instead of putting a listener on every delete button, listen on the parent and ask, "Did this click come from a delete button?"
This is a real frontend pattern. Many bigger apps are this same shape with more data, more components, and more polish.
25. Common Beginner Errors
Cannot read properties of null
You probably tried to use an element that JavaScript did not find.
const button = document.querySelector("#saveButton");
button.addEventListener("click", saveSample);
If there is no element with id="saveButton", then button is null.
Check:
- Does the selector match the HTML exactly?
- Is the script running before the HTML exists?
- Did you use
#for id and.for class?
A simple fix is placing your script at the end of the body.
<body>
<button id="saveButton">Save</button>
<script src="app.js"></script>
</body>
Assignment to constant variable
You tried to reassign a const.
const count = 0;
count = count + 1; // error
Use let for values that change.
let count = 0;
count = count + 1;
NaN
NaN means "Not a Number".
const result = Number("hello");
console.log(result); // NaN
This often happens when reading form values.
const count = Number(colonyCountInput.value);
if (Number.isNaN(count)) {
console.log("Please enter a valid number.");
}
Equals versus triple equals
= assigns a value.
let stain = "purple";
=== compares values.
if (stain === "purple") {
console.log("Gram-positive style palette");
}
The function runs too early
button.addEventListener("click", handleClick()); // wrong for event listeners
Use the function name without calling it.
button.addEventListener("click", handleClick); // correct
Or use an arrow function.
button.addEventListener("click", () => {
handleClick();
});
26. Debugging: Observation, Not Failure
Every programmer debugs. Debugging is not failure. It is observation.
A microbiologist does not say, "The plate grew unexpected colonies, therefore I am bad at science." She checks contamination, medium, incubation, labeling, technique, and assumptions.
Do the same with code.
Use console.log
console.log("Button was clicked");
console.log(sampleNameInput.value);
console.log(samples);
Place logs before and after important steps.
console.log("Before push", samples);
samples.push(newSample);
console.log("After push", samples);
Check One Assumption at a Time
If a click does not work:
const button = document.querySelector("#addButton");
console.log(button);
button.addEventListener("click", () => {
console.log("clicked");
});
If button logs null, the selector or script position is the problem.
If the button logs correctly but "clicked" never appears, the event listener is the problem.
If "clicked" appears but the page does not update, the update logic is the problem.
Debugging is narrowing the search area.
27. How to Read JavaScript You Do Not Understand Yet
When you see confusing code, ask:
- What values are being assigned?
- What conditions are being checked?
- What loops are repeating?
- What functions are being defined?
- What functions are being called?
- What is changing the DOM?
- What event starts this code?
- Could HTML/CSS have handled this instead?
Example:
filterButtons.forEach((button) => {
button.addEventListener("click", () => {
currentFilter = button.dataset.filter;
renderGallery();
});
});
Slow translation:
For each filter button:
when that button is clicked:
set currentFilter to the button's data-filter value
render the gallery again
The mysterious parts are vocabulary:
forEachmeans repeat for each itemaddEventListenermeans run this later when the event happensdataset.filterreadsdata-filter="..."from HTMLrenderGalleryis a function someone wrote
The core logic is still the four moves.
28. A Calm Learning Path
A good order for learning:
- HTML/CSS-first decision making
- values and variables
- conditionals
- loops
- functions
- arrays
- objects
- DOM selection and text changes
- events that update state
- forms with native validation
- state and render
- fetch and async
- modules
Good beginner projects:
- colony counter
- sample note taker
- flashcard app for microbiology terms
- art inspiration gallery with filters
- checklist for lab protocol steps
- quiz app for Gram stain interpretation
- stain palette picker
- local-storage sketchbook catalog
For every project, ask:
- What can be plain HTML?
- What can be CSS?
- What data must JavaScript remember?
- What event changes that data?
- How should the page render after the data changes?
That is the frontend heartbeat.
29. Final Mental Model
JavaScript is not a fog. It is a small set of ideas repeated in many costumes.
When the DOM appears, it feels like the language changed. It did not. You are just using pre-built objects and functions from the browser.
A browser function like this:
document.querySelector("#sampleList");
is not conceptually different from a lab instrument button. It performs a task someone else engineered.
Your job is to learn:
- what the function expects
- what it gives back
- what you can do with the result
A final small app usually has this shape:
const form = document.querySelector("#form");
const list = document.querySelector("#list");
const items = [];
function render() {
list.innerHTML = "";
for (const item of items) {
const li = document.createElement("li");
li.textContent = item;
list.append(li);
}
}
form.addEventListener("submit", (event) => {
event.preventDefault();
items.push("New item");
render();
});
render();
That is a huge amount of frontend development in miniature.
Keep returning to these questions:
HTML: what is the meaning and native structure?
CSS: what is the layout, appearance, and visual state?
JavaScript: what must be remembered, calculated, fetched, saved, or re-rendered?
Then return to the four programming moves:
assignment: what values are stored?
conditional: what decisions are made?
loop: what repeats?
function: what steps are packaged and reused?
Everything else is naming, practice, and patience.
30. A Note on the Page Design
This tutorial is also a small example of using HTML and CSS before reaching for JavaScript. The page layout, typography, table styling, responsive behavior, focus states, and reduced-motion preference are all handled by CSS. The generated site does not need client-side JavaScript to look good.
The design tries to be readable without becoming sterile. The typography follows a few practical rules: readable body text, comfortable line spacing, and headings that feel like section markers rather than billboards. Around that, the CSS adds a softer lab-notebook-and-sketchbook mood: warm paper, ink-like text, painterly gradients, rounded cards, and small decorative accents. Code examples are formatted to avoid horizontal scrolling where practical, and ordinary prose uses the same generous content column as the examples.
31. References Worth Bookmarking
These references are not homework. They are useful when a feature name appears and you want the official explanation.