Organizing HTML, CSS, and JS to Create Modular Website Components

Most websites contain the same or very similar layouts on multiple pages, e.g. header and footer. There also might be a few different hero section designs and a few different CTA section designs. Imagine having 10 product pages each containing three 2-column sections with a text description in the left column and a screenshot in the right column. Each of these product pages may also have a CTA section design but with slightly different text and links. It’s common to put shared CSS in a shared CSS file, e.g. shared.css, common.css, or global.css. This especially makes sense for the header and footer, which are usually shown on all pages. But over time, that shared CSS file can become very long because you may have a lot of CSS for many different common sections. This can make it difficult and dangerous to edit code for just one particular section. It can also make it very difficult if you want to copy a section on one page to add to another page. If the HTML, CSS, and JS for the section aren’t isolated, you may not copy all the necessary code, not to mention you could end up with inconsistencies between two or more sections that should have the same design.

Consolidating all CSS into the fewest files possible is good for website performance (the fewer files, the fewer network requests), but nowadays, it’s common for websites, including simple static websites, to go through an automated build process to optimize the files before publishing them. The build process can do many things like minify and combine multiple CSS and JS files into single CSS and JS files, add prefixes to CSS using tools like PostCSS auto-prefixer, etc.

Following is one simple approach to grouping HTML, CSS and JS by website section. This approach can also be used for any part of a website like blocks within a section, but to keep things simple, we’ll just look at sections which I define as horizontal rows of related content, e.g.

  • header section
  • hero section design 1
  • hero section design 2
  • features section design 1
  • features section design 2
  • resources section
  • CTA section
  • footer section

Here’s one possible file structure:

my-website/
├─ src/
│  ├─ index.njk
│  ├─ index.css
│  ├─ index.js
│  ├─ product1/
│  │  ├─ index.njk
│  │  ├─ index.css
│  │  ├─ index.js
│  ├─ product2/
│  │  ├─ index.njk
│  │  ├─ index.css
│  │  ├─ index.js
│  ├─ components/
│  │  ├─ header/
│  │  │  ├─ header.njk
│  │  │  ├─ header.css
│  │  │  ├─ header.js
│  │  ├─ footer/
│  │  │  ├─ footer.njk
│  │  │  ├─ footer.css
│  │  │  ├─ footer.js
│  │  ├─ section1/ (e.g. hero layout design 1)
│  │  │  ├─ section1.njk
│  │  │  ├─ section1.css
│  │  │  ├─ section1.js
│  │  ├─ section2/ (e.g. hero layout design 2)
│  │  │  ├─ section2.njk
│  │  │  ├─ section2.css
│  │  │  ├─ section2.js
│  │  ├─ section3/ (e.g. features layout design 1)
│  │  │  ├─ section3.njk
│  │  │  ├─ section3.css
│  │  │  ├─ section3.js
│  │  ├─ section4/ (e.g. CTA layout design 1)
│  │  │  ├─ section4.njk
│  │  │  ├─ section4.css
│  │  │  ├─ section4.js
├─ build/
│  ├─ index.html
│  ├─ index.css
│  ├─ index.js
│  ├─ product1/
│  │  ├─ index.html
│  │  ├─ index.css
│  │  ├─ index.js
│  ├─ product2/
│  │  ├─ index.html
│  │  ├─ index.css
│  │  ├─ index.js

In the src (source) folder, I’m using Nunjucks (njk) files instead of HTML files so they can include logic and pull in the components (partials). When the source files are processed, the built files show up in the “build” folder. For the home page source file (index.njk), the structure of the code could be like this

<html>
<head>

    {% include "/src/components/header/header.css" %}
    {% include "/src/components/footer/footer.css" %}
    {% include "/src/index.css" %}
</head>
<body>
    {% include "/src/components/header/header.njk" %}

    ... some HTML ...
    
    {% include "/src/components/footer/footer.njk" %}

    {% include "/src/components/header/header.js" %}
    {% include "/src/components/footer/footer.js" %}
    {% include "/src/index.js" %}
</body>
</html>

Note that the home page has its own CSS and JS files for elements that are not part of a component. When this file is built, the CSS and JS files will be combined (Netlify can do this automatically) and the included header and footer njk references will be replaced with their contents, e.g.

<html>
<head>
    <link rel="stylesheet" href="index-435kl3jl3j.css">
</head>
<body>
    <header>
        <p>FOO Website</p>
        <ul>
            <li>Home</li>
            <li>Product 1</li>
            <li>Product 2</li>
        </ul>
    </header>

    ... some HTML ...
    
    <footer>
        Copyright 2023
    </footer>

    <script src="index-43533lj344.js"></script>
</body>
</html>

Here’s another example. For product page 1 (product1/index.njk), the file contents may look like this

<html>
<head>

    {% include "/src/components/header/header.css" %}
    {% include "/src/components/section1/section1.css" %}
    {% include "/src/components/section4/section4.css" %}
    {% include "/src/components/header/footer.css" %}
    {% include "/src/product2/index.css" %}
</head>
<body>
    {% include "/src/components/header/header.njk" %}

    {% set title = "Product 1" %}
    {% set heroImage = "product1.jpg" %}
    {% include "/src/components/section1/section1.njk" %}

    ... some HTML ...

    {% set text = "Try Product 1 Now" %}
    {% set link = "/product1/free-trial/" %}
    {% include "/src/components/section4/section4.njk" %}
    
    {% include "/src/components/footer/footer.njk" %}

    {% include "/src/components/header/header.js" %}
    {% include "/src/components/section1/section1.js" %}
    {% include "/src/components/section4/section4.js" %}
    {% include "/src/components/footer/footer.js" %}
    {% include "/src/product2/index.js" %}
</body>
</html>

In the code example above, we’re passing some variables into components section1 and section 4. That allows us to reuse a component’s layout and design while changing its content. Since product pages usually look very similar, the code for product2/index.njk might look like this

<html>
<head>

    {% include "/src/components/header/header.css" %}
    {% include "/src/components/section1/section1.css" %}
    {% include "/src/components/section4/section4.css" %}
    {% include "/src/components/header/footer.css" %}
    {% include "/src/product2/index.css" %}
</head>
<body>
    {% include "/src/components/header/header.njk" %}

    {% set title = "Product 2" %}
    {% set heroImage = "product2.jpg" %}
    {% include "/src/components/section1/section1.njk" %}

    ... some HTML ...

    {% set text = "Try Product 2 Now" %}
    {% set link = "/product2/free-trial/" %}
    {% include "/src/components/section4/section4.njk" %}
    
    {% include "/src/components/footer/footer.njk" %}

    {% include "/src/components/header/header.js" %}
    {% include "/src/components/section1/section1.js" %}
    {% include "/src/components/section4/section4.js" %}
    {% include "/src/components/footer/footer.js" %}
    {% include "/src/product2/index.js" %}
</body>
</html>

I reused the components but changed the value of the variables that are referenced in the components.

To prevent code conflicts, you can specify an ID in the first element of each component. For example,

section1.njk

<div id="section1">
    ... some HTML ...
</div>

section2.njk

<div id="section2">
    ... some HTML ...
</div>

Then, in the component’s CSS, to prevent CSS conflicts, you can prefix all rules like this

section1.css

#section1 .intro {
    ... some CSS ...
}

#section1 .features {
    ... some CSS ...
}

section2.css

#section2 .intro {
    ... some CSS ...
}

#section2 .features {
    ... some CSS ...
}

Similarly, with the JavaScript component file, you can do something similar, e.g.

section1.js

$("#section1 .intro")...

section2.js

$("#section2 .intro")...

Another benefit of this approach is you can create a page showing a preview of all components you have. When you want to create a new page, you can browse the list of component previews to see if you can reuse an existing component or decide if you need to create a new component.