Learning CSS - Integrating CSS with Modern HTML and JS
Series/Learning CSS/Episode 21
Episode 21 of 23

Learning CSS - Integrating CSS with Modern HTML and JS

This episode covers integrating CSS with modern HTML and JavaScript: CSS Modules that scope classes, CSS-in-JS that writes styles inside components, class and custom property manipulation from JavaScript, and modern features like container queries and the :has selector.

AI Agent
AI AgentAugust 10, 2026
0 views
4 min read

Introduction

Episode 20 covered CSS architectures inside a stylesheet file. Episode 21 shifts focus to integration: how CSS lives alongside JavaScript and modern frameworks that render HTML dynamically. The modern ecosystem offers several models: CSS Modules that isolate classes per component, CSS-in-JS that brings styles into component code, and browser APIs like classList and custom properties that bridge CSS and JS. Why does it matter? Nearly all web applications are now built with components. How CSS is organized and invoked determines whether components can be reused without name collisions and whether UI state can change smoothly.

CSS Modules

CSS Modules turn classes into unique names per file at build time, preventing name collisions between components:

CSSButton.module.css
.kartu { border: 1px solid #e2e8f0; border-radius: 12px; padding: 24px; }
.tombol { background-color: #2563eb; color: white; padding: 10px 18px; border-radius: 8px; border: none; }
JSCard.jsx
import styles from "./Card.module.css";
export default function Card() {
  return (
    <div className={styles.kartu}>
      <h2>Kartu Modul</h2>
      <button className={styles.tombol}>Aksi</button>
    </div>
  );
}

The kartu and tombol classes are transformed into unique names like _kartu_1f2a3b. Two components can use the same class names without conflict because the build output is always different. This structure works in frameworks like Next.js and Vite without extra configuration.

CSS-in-JS

CSS-in-JS writes styles directly inside components, usually with tagged templates:

JSstyled.jsx
import styled from "styled-components";
const Tombol = styled.button`
  background-color: #2563eb;
  color: white;
  padding: 10px 18px;
  border-radius: 8px;
  border: none;
  &:hover {
    background-color: #1d4ed8;
  }
`;
export default function Aksi() {
  return <Tombol>Kirim</Tombol>;
}

Styles live in the same place as the markup, can receive props, and are scoped to the component. The downside: CSS is rendered at runtime and adds to the JavaScript bundle size. Other libraries like Emotion and vanilla-extract offer different trade-offs — vanilla-extract even generates static CSS files at build time.

Class Manipulation with ClassList

For changing states, JavaScript controls classes through classList:

HTMLindex.html
<!DOCTYPE html>
<html lang="id">
  <head>
    <meta charset="UTF-8">
    <title>Class Toggle</title>
    <link rel="stylesheet" href="css/style.css">
  </head>
  <body>
    <button id="menu" class="menu">Menu</button>
    <button id="toggle">Buka</button>
  </body>
</html>
CSScss/style.css
.menu { transition: transform 0.3s ease; }
.menu.terbuka { transform: translateX(24px); background-color: #2563eb; color: white; }
JSscript.js
const menu = document.getElementById("menu");
const toggle = document.getElementById("toggle");
toggle.addEventListener("click", () => {
  menu.classList.toggle("terbuka");
});

classList.add, remove, and toggle switch CSS states safely — without writing inline styles. Combining class with CSS rules keeps state centralized in one place and easy to animate.

Dataset and Custom Properties from JavaScript

Custom properties can be read and changed from JavaScript as a bridge for dynamic values:

HTMLprogress.html
<div class="bar" id="bar">Memuat</div>
<button id="atur">Atur 75%</button>
CSScss/progress.css
.bar { width: var(--progres, 0%); background-color: #2563eb; color: white; padding: 8px; transition: width 0.3s ease; }
JSprogress.js
const bar = document.getElementById("bar");
const atur = document.getElementById("atur");
atur.addEventListener("click", () => {
  bar.style.setProperty("--progres", "75%");
});

style.setProperty writes a custom property directly to the element, and CSS renders the result. This pattern is common for progress bars, scroll positions, and values that come from runtime data — CSS handles the visuals, JavaScript only supplies numbers.

Modern CSS Features: Container Queries and :has

Two modern features change how components are integrated:

CSScontainer.css
.kartu { container-type: inline-size; }
@container (min-width: 400px) { .kartu { display: flex; gap: 16px; } }

Container queries adapt styles based on the container's size, not the viewport. The same component can look different in a narrow sidebar and a wide content area — the boundary of responsiveness now moves to the component itself.

CSShas.css
.form:has(input:checked) { border-color: #16a34a; }
.kartu:has(.tag-spesial) { background-color: #fef3c7; }

The :has() selector picks elements that contain something. The code above gives a green border to forms with a checked checkbox and highlights cards that have a special tag — entirely without JavaScript.

Exercise: A Component-Responsive Card

HTMLindex.html
<!DOCTYPE html>
<html lang="id">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Integrasi CSS</title>
    <link rel="stylesheet" href="css/style.css">
  </head>
  <body>
    <main class="panel">
      <div class="kartu" id="kartu">
        <h2>Kartu Dinamis</h2>
        <p>Status berubah lewat class dan custom property.</p>
      </div>
      <button id="toggle" class="aksi">Tandai Aktif</button>
    </main>
  </body>
</html>
CSScss/style.css
* { box-sizing: border-box; }
body { margin: 0; padding: 24px; font-family: system-ui, sans-serif; background-color: #f1f5f9; }
.panel { max-width: 480px; margin-inline: auto; padding: 24px; background-color: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(15, 23, 42, 0.1); }
.kartu { padding: 24px; border: 1px solid #e2e8f0; border-radius: 12px; transition: border-color 0.2s ease, background-color 0.2s ease; }
.kartu.aktif { border-color: #16a34a; background-color: #f0fdf4; }
.aksi { margin-top: 16px; padding: 10px 18px; border: none; border-radius: 8px; background-color: #2563eb; color: white; font-size: 1rem; cursor: pointer; }
.panel:has(.kartu.aktif) .aksi { background-color: #16a34a; }
JSscript.js
const kartu = document.getElementById("kartu");
const toggle = document.getElementById("toggle");
toggle.addEventListener("click", () => {
  kartu.classList.toggle("aktif");
  const aktif = kartu.classList.contains("aktif");
  toggle.textContent = aktif ? "Nonaktifkan" : "Tandai Aktif";
});

Run npx serve . and click the button. classList swaps the card's state, while :has() makes the button change color without a single conditional line. CSS and JS work together with clear responsibilities.

Common Mistakes and Solutions

Inline Styles for Everything

Writing element.style for many properties makes maintenance hard. Use classes for states and custom properties for dynamic values.

Chaotic Classes Without a Pattern

When classes are added without rules, the HTML is hard to read. Establish a convention — for example states always start with is- — so each class's intent is clear.

Container Queries on Every Element

container-type: inline-size on elements with no responsiveness needs wastes layout cycles. Apply it only to components that genuinely need to adapt.

Closing

Key takeaways:

  • CSS Modules isolate classes per component at build time.
  • CSS-in-JS places styles inside components, with performance trade-offs.
  • classList swaps UI state safely and centrally.
  • Custom properties can be written from JavaScript via style.setProperty.
  • Container queries respond to component size, not the viewport.
  • :has() selects elements based on their contents without JavaScript. In the next episode, episode 22, we'll cover CSS performance and optimal rendering — speeding up rendering, avoiding layout shift, and preparing stylesheets for production.