Skip to main content
    Courses/HTML & CSS/Reusable Components

    Lesson 37 • Advanced Track

    Building Reusable UI Components

    By the end of this lesson you'll be able to design a small, themeable component system — a button with variants and sizes, and a compound card — using BEM naming, modifier classes, and CSS custom properties as the component's public API.

    What You'll Learn

    Split a component into a base class plus modifier classes
    Name parts and variants clearly with BEM (block__element--modifier)
    Build a button with colour variants, sizes, and a disabled state
    Build a compound card with optional header, body, and footer parts
    Expose a theming API through CSS custom properties (tokens)
    Avoid leaks: keep selectors flat instead of deep descendant chains
    Before this lesson: you should be comfortable writing CSS rules and classes, and know how custom properties work — declaring --name and reading it with var(). If variables feel new, review CSS Custom Properties first.

    💡 Think of It Like This

    A reusable component is like a LEGO brick. A single 2×4 brick is one shape (the base), but it comes in red, blue, or green (variants) and in tall or short heights (sizes). You design each brick type once, then snap copies together into any structure — you never re-mould a brick for every model.

    The studs on top are the brick's API: a fixed, documented way to connect to it. Your component's CSS custom properties are exactly that — a small set of named knobs (--primary, --radius) that anyone can turn from the outside, without prising the brick open to see how it's made.

    1. The Pattern: Base Class + Modifier Classes

    A component system follows one consistent shape: a base class that holds the shared structure (layout, spacing, corner radius), plus small modifier classes that change one visual thing each — a colour, a size, a state. You put several classes on one element and they compose: class="btn btn--primary btn--lg".

    The win is that the base stays written once. Five colours times three sizes is eight tiny classes that stack, not fifteen fat ones that each repeat the padding and radius. BEM naming keeps this readable: block (the component), block__element (a part inside it), and block--modifier (a variant or state).

    RoleBEM formExample
    Block (base).block.btn · .card
    Element (a part).block__part.card__header
    Modifier (variant).block--variant.btn--primary
    Modifier (state).block--state.form-input--error

    2. Worked Example — A Button System

    Here is the pattern in full. The .btn base sets layout, padding, and radius and no colour at all. Each .btn--* modifier touches only the properties it owns, so colour, size, and state never fight each other and can be combined freely. Every colour and the radius come from :root tokens — that block is the button's theming API.

    Worked example: button base + variants + sizes

    One .btn base, small modifiers stacked on top

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Button system</title>
      <style>
        :root {
          /* Design tokens = the button's public API. Override these to re-theme. */
          --primary: #1976D2;        /* brand colour, reused by every primary button */
          --primary-hover: #1565C0;  /* darker shade for :hover */
          --danger: #D32F2F;
          --danger-hover: #B71C1C;
          --radius: 8px;             /* one corner radius for the whole UI */
        }
    
        body { font-f
    ...

    3. Worked Example — A Compound Card

    A card is a compound component: a block made of several named parts — .card__header, .card__body, .card__footer. Each part is optional, so the same block works whether a card has all three or just a body. Notice the selectors stay flat — one class per part — instead of brittle chains like .card div p, so a card's styles never leak into content you drop inside it.

    Worked example: compound card with optional parts

    Header, body, footer elements plus block-level modifiers

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Card system</title>
      <style>
        :root {
          --surface: #ffffff;       /* card background */
          --border: #e0e0e0;
          --muted: #757575;         /* secondary text */
          --primary: #1976D2;
          --radius: 12px;
          --shadow: 0 2px 8px rgba(0,0,0,0.08);
        }
    
        body { font-family: system-ui, sans-serif; padding: 24px; background: #fafafa; }
        .grid { display: grid; grid-template-columns: repeat(auto-fill, 
    ...

    🎯 Your Turn #1 — Add a variant and a size to the button

    The .btn base is done. Add a success colour variant and a small size, then apply both to a button. Fill in the blanks marked ___ and run it.

    Your Turn #1: a success variant + small size

    Add .btn--success and .btn--sm, then compose them

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Your Turn 1</title>
      <style>
        /* 🎯 YOUR TURN — fill in the blanks marked ___ */
    
        :root {
          --primary: #1976D2;
          --success: #2E7D32;   /* a green token, ready to use */
          --radius: 8px;
        }
    
        body { font-family: system-ui, sans-serif; padding: 24px; }
        .demo { display: flex; gap: 8px; align-items: center; }
    
        /* BASE class — already complete, do not change it. */
        .btn {
          padding: 10
    ...

    🎯 Your Turn #2 — Add a card element and a modifier

    This card has a body but no footer, and no way to highlight it. Add a .card__footer element and a .card--accent modifier, then use them in the markup.

    Your Turn #2: a footer element + accent modifier

    Add .card__footer and .card--accent, then apply both

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Your Turn 2</title>
      <style>
        /* 🎯 YOUR TURN — fill in the blanks marked ___ */
    
        :root { --surface: #fff; --border: #e0e0e0; --primary: #1976D2; --radius: 12px; }
    
        body { font-family: system-ui, sans-serif; padding: 24px; background: #fafafa; }
    
        /* BLOCK + existing elements — already complete. */
        .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow
    ...

    🧩 Mini-Challenge — A badge component from scratch

    Support is faded now — only an outline is given. Build a small badge component (a pill label) with a base class and colour variants, driven by tokens. Use the button worked example as your reference if you get stuck.

    Mini-Challenge: a tokenised badge system

    A .badge base, colour variants, and tokens in :root

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Mini-Challenge</title>
      <style>
        /* 🧩 MINI-CHALLENGE: a reusable badge (pill label) component
           1. In :root, declare colour tokens:
                --info (a blue), --success (a green), --danger (a red)
           2. Write a BASE class .badge with the shared pill shape:
                inline-block, small padding (e.g. 2px 10px), border-radius: 99px,
                a small bold font. Set NO colour here.
           3. Write three VA
    ...

    ⚠️ Common Errors (and the fix)

    • Overly specific selectors. Targeting parts with .card div p or styling by ID (#submit) raises specificity, so a later variant class can't override it and an unrelated nested paragraph gets caught too. Fix: give each part one class — .card__body — and target that single class.
    • Hard-coded values instead of tokens. Writing background: #1976D2 in ten rules means ten edits to rebrand, and one missed instance breaks consistency. Fix: declare --primary once and use background: var(--primary) everywhere.
    • Leaking styles. Putting colour on the base class (.btn { background: blue; }) forces every variant to fight it back. Fix: keep the base structural only; let each --modifier own exactly the properties it changes.
    • One class per combination. Writing .btn-primary-large-disabled explodes into dozens of classes. Fix: compose small modifiers on the element — class="btn btn--primary btn--lg".
    • Wrong BEM separators. .card-header reads as a sibling block, not a part of the card. Fix: use __ for elements and -- for modifiers: .card__header, .card--accent.

    📋 Quick Reference

    GoalUse this
    Define the base (structure only).btn { padding; border-radius; ... }
    Add a colour variant.btn--primary { background: var(--primary); }
    Add a size variant.btn--lg { padding: 14px 28px; }
    Compose on an elementclass="btn btn--primary btn--lg"
    Name an inner part (element).card__header
    Name a variant (modifier).card--accent
    Expose a theming API:root { --primary: #1976D2; }
    Map a state to an attribute.btn:disabled { opacity: 0.5; }

    ❓ Frequently Asked Questions

    What is BEM and why do component classes look like .card__title--active?

    BEM stands for Block, Element, Modifier — a naming convention that makes a class name tell you what it does at a glance. The Block is the standalone component (.card). An Element is a part that only exists inside it, joined with two underscores (.card__title). A Modifier is a variant or state, joined with two dashes (.card--featured or .btn--lg). You never have to guess whether .title belongs to the card or the modal — the name carries that scope. It keeps specificity flat (everything is a single class) so styles are easy to override and never leak into unrelated parts of the page.

    Why should I avoid descendant selectors like .card div p for components?

    A selector like .card div p reaches into any paragraph anywhere inside a card, including ones nested in a component you dropped in later. It is fragile (a new wrapper div breaks it), it is hard to override (the longer the selector, the higher its specificity), and it leaks (an unrelated paragraph that happens to land inside a card gets your styling). Give each part its own single class — .card__body — and target that. One class, one job, no surprises.

    Should I hard-code colours in a component or use CSS variables?

    Use variables (design tokens). If you write background: #1976D2 in twenty rules, rebranding means twenty find-and-replace edits and one missed instance breaks consistency. Declare --primary: #1976D2 once and write background: var(--primary). Now the component has a small, documented API: anyone can re-theme it by overriding --primary on a parent — without ever editing your component's stylesheet. Hard-coded values are fine only for one-off, genuinely-never-reused pixels.

    How do I combine a variant and a size on the same element?

    Put multiple classes on the element: class="btn btn--primary btn--lg". The base class .btn carries the shared structure (padding, radius, flex layout), .btn--primary sets the colour, and .btn--lg overrides the size. Because each modifier only touches the properties it owns and they do not conflict, they compose cleanly. This is why a base-plus-modifier system scales: five colours times three sizes is eight small classes, not fifteen big ones.

    How does a component expose a customisation API without exposing its internals?

    Declare the knobs you want consumers to turn as CSS variables on the component's base class, reading them with a fallback: padding: var(--btn-pad, 10px 20px). Consumers set --btn-pad on the element or a parent to customise it; everything else stays private. The variable list is effectively the component's public API — small, named, and documented — while the structural CSS underneath is free to change.

    What is the difference between a variant class and a state class?

    A variant is a design choice you pick when you place the component — .btn--primary versus .btn--danger, .card--bordered versus .card--accent. A state reflects what is happening to it right now — disabled, error, loading — and often maps to a real attribute or pseudo-class (:disabled, .form-input--error). Keeping them as separate modifier classes means a danger button can still be disabled, and an error input can still be large, without you writing a class for every combination.

    🎉 Lesson Complete

    You can now design small, themeable component systems instead of one-off styles. The essentials:

    • ✅ A component = a base class (structure) plus small modifier classes (one job each)
    • ✅ Compose variants on an element: class="btn btn--primary btn--lg"
    • ✅ Name with BEM: block, block__element, block--modifier
    • ✅ Compound components keep each part a flat, optional class — no deep .card div p chains
    • ✅ Custom properties are the component's public API — re-theme by overriding a token
    • ✅ Separate variants (a design choice) from states (disabled, error) so they compose freely

    Next up: Canvas, where you'll draw and animate graphics directly in the browser.

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service