Course Lessons

CSS Styling

Back to Course

CSS Variables & Pseudo

CSS Styling Lesson 8 of 10 12 min

Webbo3 Data Analysis Bootcamp · CSS Module · Lesson 4

CSS Variables and Pseudo-Elements: Custom Properties, Dynamic States, and Structural Styling

A hands-on lesson covering CSS custom properties for reusable values, pseudo-classes for interactive and structural selection, pseudo-elements for generated content, and the importance of CSS resets and base styles.

CSS code and web styling

By this point in the bootcamp you understand selectors, the box model, and basic layout. This lesson introduces three concepts that separate beginner CSS from professional CSS: variables that make your styles maintainable, pseudo-classes that respond to user interaction and document structure, and pseudo-elements that let you generate content and style without adding HTML. Together, these tools let you write CSS that is cleaner, more dynamic, and more powerful. You will also learn why every professional project starts with a CSS reset, because browsers do not agree on default styles, and fighting those defaults wastes hours of your time.

1. Custom Properties with CSS Variables

CSS custom properties, also called CSS variables, let you define reusable values that you can reference throughout your stylesheet. They are declared with two hyphens and accessed with the var() function. This might seem like a small convenience, but in large projects it is transformative. Change one variable at the root, and every button, heading, and border that references it updates automatically.

Declaring and using custom properties. Define variables inside a selector, most commonly :root so they are globally available:

:root {
  --primary-color: #0a6e6e;
  --secondary-color: #c98a3e;
  --text-dark: #1a1a1a;
  --text-light: #555555;
  --bg-white: #ffffff;
  --bg-gray: #f4f9f9;
  --font-serif: Georgia, 'Times New Roman', serif;
  --font-sans: Arial, Helvetica, sans-serif;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 24px;
  --radius: 6px;
}

Now reference these variables anywhere in your CSS:

h1 {
  color: var(--primary-color);
  font-family: var(--font-sans);
  margin-bottom: var(--spacing-md);
}

.card {
  background-color: var(--bg-white);
  border-radius: var(--radius);
  padding: var(--spacing-lg);
}

Fallback values. The var() function accepts a second argument that serves as a fallback if the variable is not defined:

.button {
  background-color: var(--button-bg, #0a6e6e);
  color: var(--button-text, #ffffff);
}

If --button-bg is not defined anywhere, the button gets #0a6e6e instead. This is useful when writing component CSS that might be used in projects with different variable names.

Scope and inheritance. Custom properties follow the cascade. A variable defined on :root is available everywhere. A variable defined on a specific selector is only available to that element and its descendants:

.alert {
  --alert-color: #dc3545;
  border-left: 4px solid var(--alert-color);
  background-color: var(--alert-color);
}

.alert p {
  color: var(--alert-color);
}

Here --alert-color is scoped to .alert and its children. It does not exist outside that context. This lets you create self-contained components with their own theming variables.

Dynamic theming with JavaScript. Unlike preprocessor variables from Sass or Less, CSS custom properties can be changed at runtime with

document.documentElement.style.setProperty('--primary-color', '#e63946');

This changes the primary color across the entire page instantly, without reloading or recompiling any CSS. It is how dark mode toggles, theme switchers, and real-time color pickers are built in modern web applications.

Code editor with CSS styling

2. Pseudo-Classes: :hover, :focus, :active, :visited

Pseudo-classes let you style elements based on their state or position, not just their tag or class. They are written with a single colon followed by a keyword. The interactive pseudo-classes, hover, focus, active, and visited, are essential for accessible, responsive interfaces.

:hover. Applies when the user points at an element with a mouse cursor:

a {
  color: var(--primary-color);
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
  color: var(--secondary-color);
}

:hover is not limited to links. You can use it on buttons, cards, table rows, or any element to provide visual feedback:

.card:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  transform: translateY(-2px);
  transition: all 0.2s ease;
}

:focus. Applies when an element receives keyboard focus, typically through Tab navigation or clicking into a form field:

input:focus,
textarea:focus,
select:focus {
  outline: 2px solid var(--primary-color);
  outline-offset: 2px;
  border-color: var(--primary-color);
}

:focus is critical for accessibility. Users who navigate by keyboard, not mouse, rely on visible focus indicators to know where they are on the page. Never remove focus outlines without providing an alternative. The outline property is the standard way to show focus because it does not affect layout, unlike border.

:active. Applies while an element is being activated, the moment between pressing and releasing a mouse button on a link or button:

button:active {
  transform: scale(0.98);
  background-color: #085555;
}

:active gives users immediate tactile feedback that their click registered. It is subtle but makes an interface feel responsive and polished.

:visited. Applies to links that the user has already visited:

a:visited {
  color: #6b4c9a;
}

:visited is limited by browser security restrictions. For privacy reasons, you can only style color, background-color, border-color, outline-color, and a few SVG fill and stroke properties. You cannot change font-size or display on visited links. This prevents websites from detecting your browsing history through CSS.

3. Structural Pseudo-Classes: :nth-child, :first-child, :last-child

Structural pseudo-classes let you style elements based on their position among siblings. They are incredibly powerful for styling lists, tables, and grids without adding extra classes to your HTML.

:first-child and :last-child. These select the first or last element among its siblings:

li:first-child {
  border-top: none;
}

li:last-child {
  border-bottom: 2px solid var(--primary-color);
}

This removes the top border from the first list item and adds a bold bottom border to the last one. It is a clean way to handle edge cases in repeating elements.

:nth-child. This is the most flexible structural pseudo-class. It accepts a formula in the form an + b, where a is the step size and b is the offset:

tr:nth-child(even) {
  background-color: #f8f9fa;
}

tr:nth-child(odd) {
  background-color: #ffffff;
}

This creates zebra-striping on a table, alternating row colors for readability. The even and odd keywords are shorthand for 2n and 2n+1. You can also target specific positions:

li:nth-child(3) {
  font-weight: bold;
}

li:nth-child(4n) {
  margin-right: 0;
}

li:nth-child(3n+1) {
  clear: left;
}

The first example styles only the third list item. The second removes the right margin from every fourth item, useful in grid layouts where the last item in each row should not have a right margin. The third clears the float on items 1, 4, 7, 10, and so on, ensuring each row starts fresh in a floated grid.

:nth-of-type versus :nth-child. :nth-child counts all siblings regardless of type. :nth-of-type counts only siblings of the same element type:

p:nth-of-type(2) {
  color: var(--secondary-color);
}

This selects the second paragraph among its siblings, even if there are headings or divs between paragraphs. :nth-child(2) would select the second child of any type, which might be a heading instead of a paragraph. Use :nth-of-type when you care about the position within a specific element type.

Web design layout and structure

4. Pseudo-Elements: ::before and ::after

Pseudo-elements let you insert content and apply styles to specific parts of an element without adding extra HTML. They are written with double colons to distinguish them from pseudo-classes, though single colons work for backward compatibility in some cases. ::before inserts content before an element's actual content. ::after inserts content after it.

Adding decorative icons. One of the most common uses is adding icons or symbols before or after text:

.external-link::after {
  content: ' ↗';
  font-size: 0.8em;
  color: var(--text-light);
}

This adds a small arrow after any element with the external-link class, indicating it opens in a new tab. The content property is required for ::before and ::after to appear. Without it, the pseudo-element is empty and invisible.

Creating custom bullets. You can replace default list bullets with styled pseudo-elements:

ul.custom-list {
  list-style: none;
  padding-left: 0;
}

ul.custom-list li {
  position: relative;
  padding-left: 24px;
}

ul.custom-list li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 8px;
  width: 8px;
  height: 8px;
  background-color: var(--primary-color);
  border-radius: 50%;
}

This removes default bullets, then adds a custom teal circle before each list item. The position: relative on the li and position: absolute on the ::before work together to place the bullet precisely.

Creating decorative lines and shapes. Pseudo-elements are often used for visual flourishes like underlines that do not span the full width:

h2 {
  position: relative;
  padding-bottom: 12px;
}

h2::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  width: 60px;
  height: 3px;
  background-color: var(--primary-color);
}

This adds a short colored underline beneath every h2, positioned at the bottom-left. It is a common design pattern in modern web typography.

Clearfix with ::after. Before flexbox and grid were widely supported, ::after was the standard solution for clearing floated elements:

.clearfix::after {
  content: '';
  display: table;
  clear: both;
}

While modern layout methods have reduced the need for clearfix, you will still encounter it in legacy codebases and CSS frameworks. Understanding how it works, generating an empty block-level element that clears floats, helps you read older stylesheets.

5. CSS Resets and Base Styles

Every browser ships with default styles for HTML elements. Headings have margins. Lists have padding and bullets. Links are blue and underlined. Forms have borders and backgrounds. These defaults vary between Chrome, Firefox, Safari, and Edge. If you do not override them, your design will look slightly different in every browser, and you will spend hours debugging layout issues that are not your fault. A CSS reset is a small block of CSS that strips these defaults to a consistent baseline, giving you full control from the start.

A modern CSS reset. This is a practical reset suitable for most projects:

*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  font-size: 16px;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body {
  font-family: var(--font-sans);
  line-height: 1.6;
  color: var(--text-dark);
  background-color: var(--bg-white);
}

img,
picture,
video,
canvas,
svg {
  display: block;
  max-width: 100%;
}

input,
button,
textarea,
select {
  font: inherit;
}

p,
h1,
h2,
h3,
h4,
h5,
h6 {
  overflow-wrap: break-word;
}

a {
  color: inherit;
  text-decoration: none;
}

Let us break this down. box-sizing: border-box ensures that padding and border are included in an element's total width and height, so a 300px wide box with 20px padding remains 300px wide, not 340px. This is the single most important property for predictable layouts. margin: 0 and padding: 0 on the universal selector remove default spacing from every element. The html block sets a base font size and enables font smoothing for sharper text rendering on macOS. The body block sets your project's default font, line height, text color, and background. The img block prevents images from overflowing their containers and removes the mysterious small gap that appears below inline images. The input block makes form elements inherit the body's font instead of using the browser's default, which is often inconsistent. The heading and paragraph block prevents long words from breaking layouts. The a block removes default link styling so you can define your own consistently.

Base styles on top of the reset. After the reset, add your project's base styles:

h1 {
  font-size: 2.5rem;
  font-weight: 700;
  line-height: 1.2;
  margin-bottom: 1rem;
}

h2 {
  font-size: 1.75rem;
  font-weight: 600;
  line-height: 1.3;
  margin-bottom: 0.75rem;
}

p {
  margin-bottom: 1rem;
}

ul, ol {
  margin-bottom: 1rem;
  padding-left: 1.5rem;
}

code {
  font-family: 'Courier New', monospace;
  font-size: 0.9em;
  background-color: #f5f5f5;
  padding: 2px 6px;
  border-radius: 3px;
}

These base styles define a clear typographic hierarchy. Headings are large and bold. Paragraphs have consistent spacing. Lists have proper indentation. Code snippets are visually distinct. Starting from this foundation, every component you build later inherits sensible defaults and requires less override code.

Clean code and development workspace

Quick recap: CSS custom properties are declared with -- and accessed with var(), scoped to their selector and dynamically changeable with JavaScript · :hover styles on mouse-over, :focus on keyboard focus, :active during click, :visited on previously visited links · :first-child and :last-child target edge siblings, :nth-child uses an+b formulas for precise positional selection · ::before and ::after generate content without extra HTML, used for icons, bullets, decorative lines, and clearfix · A CSS reset with box-sizing: border-box, margin and padding reset, and base font styles ensures cross-browser consistency and gives you full stylistic control.

Using AI to Move Faster in CSS Development

CSS variables, pseudo-classes, and pseudo-elements are not difficult to understand individually, but combining them efficiently in large projects requires experience. AI can compress that learning curve by generating starting code, explaining complex selectors, and helping you debug specificity issues.

1. Generate a complete CSS variable system from a brand description.
If you are starting a new project and have a brand color like #0a6e6e, ask Copilot: "Generate a complete CSS custom property system based on the primary color #0a6e6e. Include primary, secondary, text, background, spacing, and radius variables. Also generate the base styles and reset that use these variables." AI will produce a :root block with a full color palette derived from your primary color, including lighter and darker shades, complementary colors, and neutral grays. It will also write the reset and base styles. Your job is to adjust values to match your actual design requirements and to ensure the generated selectors do not conflict with existing styles.

2. Convert design requirements into pseudo-class selectors.
When a designer asks for "zebra-striped table rows with the first row bold and the last row having a bottom border," you can describe this to AI: "Write CSS selectors for a table where even rows have a light gray background, odd rows are white, the first row is bold, and the last row has a 2px bottom border. Use structural pseudo-classes." AI will generate the :nth-child(even), :nth-child(odd), :first-child, and :last-child selectors. Verify that the specificity is appropriate and that the selectors target the correct elements, table rows versus table cells, before applying them.

3. Debug why a pseudo-element is not appearing.
If you write ::after and nothing shows up, paste your CSS into an AI assistant and ask: "Why is this ::after pseudo-element not visible? Here is my CSS." AI will check for the most common mistakes: missing content property, incorrect position values, z-index issues, or the parent not having position: relative. This saves you from manually checking each property against the requirements.

4. Generate accessible focus and hover states.
Accessibility requirements for focus indicators are specific: they must be visible, have sufficient contrast, and not rely on color alone. Ask AI: "Write accessible :focus styles for links, buttons, and form inputs that meet WCAG 2.1 AA standards. Include visible outlines, sufficient color contrast, and clear state changes." AI will generate styles using outline, outline-offset, and border-color changes that meet accessibility guidelines. Always test the generated styles with actual keyboard navigation, because AI cannot see your page, but it can give you a compliant starting point.

5. Verify AI-generated CSS for specificity and performance.
AI sometimes generates selectors with unnecessarily high specificity, like div.container > ul.nav-list > li.nav-item > a:hover, which is hard to override later. It may also generate redundant properties or vendor prefixes that are no longer needed. Always review generated CSS for specificity traps, unused properties, and modern browser compatibility. Use browser DevTools to inspect the computed styles and confirm the cascade behaves as expected. AI accelerates your coding, but you remain responsible for maintainable, performant CSS.

A habit worth building from this lesson onward: start every new project by defining your CSS variables in :root, writing your reset, and setting your base styles. Then use AI to generate component styles that reference those variables. This creates a consistent design system where one variable change propagates everywhere, and where AI-generated code integrates cleanly with your established conventions.

Next lesson: CSS transitions, animations, and responsive design with media queries.

Complete this lesson

Mark as complete to track your progress