Salesforce · · 15 min read

An Introduction to CSS for LWC

CSS fundamentals for Lightning Web Components — what CSS is, essential styling concepts for LWCs, how CSS scoping works in Shadow DOM, and how to debug CSS in the browser console.

Part 75: An Introduction to CSS for LWC

Welcome back to the Salesforce blog series. Over the last few posts we have been building up our understanding of Lightning Web Components from the ground up. In Part 72, we looked at the anatomy of an LWC and its file structure. In Part 73, we explored Shadow DOM in detail and discussed how it creates an encapsulation boundary around each component. In Part 74, we covered lifecycle hooks and how components initialize and render. Now it is time to talk about something that touches every single component you will ever build: CSS.

If you have been following along, you already know from Part 73 that Shadow DOM has significant implications for how styles work in LWC. This post is where we unpack all of that. We will start from the very basics of what CSS is, build up to the specific patterns you need for LWC development, and finish with a cheat sheet you can keep open while you work. Whether you are coming from a pure Salesforce admin background or you have some web development experience, this post will give you the foundation you need to style your components with confidence.


What is CSS?

CSS stands for Cascading Style Sheets. It is the language that controls how HTML elements look on a page. HTML defines the structure and content, JavaScript defines the behavior, and CSS defines the presentation. Colors, fonts, spacing, layout, animations — all of that is CSS territory.

The word “cascading” is important. It refers to the set of rules that determine which styles win when multiple styles compete for the same element. Styles cascade down from parent elements to children, and more specific selectors override less specific ones. Understanding this cascade is one of the keys to writing CSS that behaves the way you expect.

A basic CSS rule looks like this:

selector {
    property: value;
}

The selector targets which HTML elements the rule applies to. The property is what aspect of the element you want to style. The value is what you want to set that property to. Here is a concrete example:

p {
    color: blue;
    font-size: 16px;
}

This rule says: find all <p> elements and make their text blue with a font size of 16 pixels. Simple enough. But in the context of LWC, things get more interesting because of how Shadow DOM scoping changes the rules of the game.


The Basics of CSS for LWC

How CSS Files Work in LWC

Every Lightning Web Component can have a CSS file that shares the same name as the component. If your component is called myComponent, then the CSS file is myComponent.css. This file sits alongside your .html and .js files in the component bundle. You do not need to import it or link to it — the framework automatically associates the CSS file with the component and applies those styles.

myComponent/
    myComponent.html
    myComponent.js
    myComponent.css

Any styles you write in that CSS file are automatically scoped to your component. They will not leak out and affect other components on the page, and styles from other components will not leak in and affect yours. This is Shadow DOM encapsulation at work, exactly what we discussed in Part 73.

Selectors You Will Use Most Often

In standard web development, you have the full range of CSS selectors at your disposal. In LWC, you still have access to most of them, but the scoping behavior means you need to think about them a bit differently.

Element selectors target HTML tags directly:

p {
    margin-bottom: 12px;
}

h2 {
    font-weight: bold;
    color: #333333;
}

Class selectors target elements with a specific class attribute. This is by far the most common approach in LWC development:

.card {
    border: 1px solid #dddddd;
    border-radius: 8px;
    padding: 16px;
}

.card-header {
    font-size: 18px;
    font-weight: 600;
    margin-bottom: 8px;
}

.card-body {
    font-size: 14px;
    line-height: 1.5;
}

And in your template:

<template>
    <div class="card">
        <div class="card-header">Account Details</div>
        <div class="card-body">
            <p>Name: {accountName}</p>
            <p>Industry: {industry}</p>
        </div>
    </div>
</template>

Descendant selectors let you target elements nested inside other elements:

.card p {
    color: #555555;
}

This targets only <p> elements that are inside an element with the class card. Because styles are already scoped to your component, you often do not need deeply nested selectors, but they are useful when you want to differentiate between similar elements in different parts of your template.

The :host Selector

This is arguably the most important CSS concept specific to Shadow DOM components. The :host selector targets the component’s host element itself — the custom element tag that represents your component in the DOM.

:host {
    display: block;
    padding: 16px;
    background-color: #f9f9f9;
    border: 1px solid #e0e0e0;
    border-radius: 4px;
}

Without the :host selector, your component’s outermost element has no default styling. By default, custom elements are display: inline, which often is not what you want. Setting display: block on :host is a very common first line in LWC CSS files.

You can also use :host with a condition. The :host() functional form lets you apply styles based on a class or attribute on the host element:

:host(.highlighted) {
    border-color: #0070d2;
    box-shadow: 0 2px 8px rgba(0, 112, 210, 0.2);
}

This only applies when the host element has the highlighted class. A parent component could set this class when rendering your component, giving you a clean way to support visual variants without polluting your internal styles.

Understanding CSS Scoping in Shadow DOM

Let us revisit something from Part 73 because it is directly relevant here. When your LWC renders, its markup lives inside a shadow tree. The CSS you write in your component’s CSS file is scoped to that shadow tree. This means:

  1. Your styles do not affect anything outside your component. If you write p { color: red; } in your CSS file, it will only make paragraph elements inside your component red. Paragraphs in other components or on the rest of the page are untouched.

  2. External styles do not reach into your component. If someone adds a global stylesheet that says p { color: green; }, your paragraphs will not turn green. The shadow boundary blocks it.

  3. You cannot style child components from a parent. If your component renders another LWC inside it, you cannot write CSS in your file that targets elements inside that child component. Each component controls its own styles.

There are some exceptions and nuances. Inherited CSS properties like font-family and color do pass through the shadow boundary from parent to child. Also, CSS custom properties (variables) can cross the boundary, which is actually a powerful pattern for theming:

/* In a parent component */
:host {
    --brand-color: #0070d2;
    --text-color: #333333;
}
/* In a child component */
.title {
    color: var(--brand-color, #000000);
}

.body-text {
    color: var(--text-color, #666666);
}

The var() function reads the custom property value, and the second argument is a fallback in case the variable is not defined. This is the recommended approach for creating themeable components in LWC.

Common Styling Patterns

Here are a few patterns you will reach for regularly when styling LWCs.

Conditional styling with dynamic classes is handled in JavaScript, not in CSS. Your CSS defines the possible states, and your JavaScript decides which class to apply:

.status-badge {
    padding: 4px 8px;
    border-radius: 12px;
    font-size: 12px;
    font-weight: 600;
}

.status-active {
    background-color: #d4edda;
    color: #155724;
}

.status-inactive {
    background-color: #f8d7da;
    color: #721c24;
}

In your JavaScript, you would compute the class dynamically:

get statusClass() {
    return this.isActive
        ? 'status-badge status-active'
        : 'status-badge status-inactive';
}

And in your template:

<span class={statusClass}>{statusLabel}</span>

Notice there are no quotes around {statusClass} in the class attribute. This is an LWC-specific syntax requirement. When you use a dynamic value for the class attribute, you omit the quotes.

Responsive layouts with flexbox work great inside LWC components:

.container {
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
}

.column {
    flex: 1 1 300px;
}

Using SLDS (Salesforce Lightning Design System) classes is another common approach. SLDS provides a comprehensive set of utility classes and component styles. You can use SLDS classes directly in your templates without writing custom CSS:

<div class="slds-card">
    <div class="slds-card__header">
        <h2 class="slds-text-heading_medium">My Card</h2>
    </div>
    <div class="slds-card__body slds-card__body_inner">
        <p class="slds-text-body_regular">Card content here.</p>
    </div>
</div>

SLDS classes are available globally within the Lightning Experience context, so they can reach into your shadow tree. This is a special accommodation that Salesforce has built into the platform. When you need custom styles beyond what SLDS provides, that is when you write your own CSS.


How to Debug CSS in the Browser Console

Knowing how to write CSS is one thing. Knowing how to figure out why your CSS is not doing what you expect is another skill entirely. The browser developer tools are your best friend here.

Opening the Inspector

Right-click on the element you want to inspect and select “Inspect” (or “Inspect Element” depending on the browser). This opens the Elements panel in DevTools, showing you the DOM tree and the applied styles.

When inspecting an LWC, you will notice the shadow tree structure in the Elements panel. You will see your custom element tag, and nested inside it (often under a #shadow-root label) you will find the actual markup from your template. Click on elements within the shadow root to see their styles.

The Styles Pane

On the right side (or bottom, depending on your layout) of the Elements panel, you will see the Styles pane. This shows every CSS rule that applies to the selected element, in order of specificity. Rules that have been overridden are shown with a strikethrough.

A few things to look for:

  • Crossed-out properties mean something more specific is overriding that style. Look above it in the list to find what is winning.
  • The element.style section at the top shows inline styles. In LWC, you rarely use inline styles, but if something unexpected is showing up there, it might be coming from JavaScript.
  • The source link next to each rule tells you which CSS file it came from. Click it to jump to that file in the Sources panel.

The Computed Tab

Next to the Styles tab, there is a Computed tab. This is incredibly useful. It shows the final, resolved value of every CSS property on the selected element. If you are wondering what the actual font size or margin is after all the cascading and inheritance has been resolved, this is where you look.

The Computed tab also shows a box model diagram at the top, which visualizes the content area, padding, border, and margin of the element. This is indispensable when debugging layout issues.

Live Editing

One of the most powerful debugging features is the ability to edit styles live in the browser. In the Styles pane, you can click on any value and change it. You can add new properties by clicking in the empty space inside a rule. You can toggle properties on and off using the checkboxes that appear when you hover over them.

Changes you make in DevTools are temporary — they disappear when you refresh the page. But this is exactly what makes them so useful for experimentation. Try different values, see what works, and then go update your actual CSS file with the winning combination.

Debugging Shadow DOM Styles

There is one gotcha when debugging LWC styles in the browser. Salesforce uses synthetic shadow DOM by default (rather than native shadow DOM) for broader browser compatibility. This means the scoping is emulated using attribute selectors rather than a true shadow boundary. When you inspect elements, you might see attributes like data-lwc-host or component-specific attribute selectors being used to enforce scoping.

Do not be alarmed if the CSS rules in DevTools look slightly different from what you wrote. The framework may have transformed your selectors to add scoping attributes. The behavior is the same — your styles are still scoped to your component. Just be aware that the devtools representation might include some extra attributes you did not write yourself.

A practical tip: if you are struggling to figure out why a style is not applying, check whether the element you think you are targeting actually matches your selector. Use the search function in the Elements panel (Ctrl+F or Cmd+F) to search by class name and confirm the element exists where you expect it to.


Section Notes: CSS Cheat Sheet

Here is a practical reference you can keep handy while building LWCs. This covers the selectors, properties, and patterns you will use most often.

Selectors Quick Reference

SelectorExampleWhat It Targets
Elementp { }All <p> elements in the component
Class.card { }Elements with class="card"
ID#header { }Element with id="header"
Descendant.card p { }<p> inside .card
Direct child.card > p { }<p> that is a direct child of .card
Multiple.card, .panel { }Elements with either class
:host:host { }The component host element itself
:host():host(.active) { }Host element when it has the class active
Pseudo-class.btn:hover { }Element on mouse hover
Pseudo-class.input:focus { }Element when focused
First/last childli:first-child { }First <li> in a list

Most-Used Properties

/* Typography */
font-size: 14px;
font-weight: 400 | 600 | 700;
line-height: 1.5;
color: #333333;
text-align: left | center | right;
text-transform: uppercase | lowercase | capitalize;

/* Spacing */
margin: 16px;
margin-top: 8px;
padding: 12px 16px;
gap: 8px;                /* For flex/grid containers */

/* Box Model */
width: 100%;
max-width: 600px;
height: auto;
border: 1px solid #dddddd;
border-radius: 4px;
box-sizing: border-box;

/* Background */
background-color: #ffffff;
background: linear-gradient(to bottom, #ffffff, #f5f5f5);

/* Layout */
display: block | flex | grid | none;
flex-direction: row | column;
justify-content: flex-start | center | space-between;
align-items: flex-start | center | stretch;
flex-wrap: wrap;

/* Visibility and Overflow */
overflow: hidden | auto | scroll;
visibility: visible | hidden;
opacity: 0.5;

/* Positioning */
position: relative | absolute | fixed;
top: 0;
left: 0;
z-index: 10;

/* Transitions */
transition: all 0.2s ease;

LWC-Specific Patterns

/* Always set display on host */
:host {
    display: block;
}

/* CSS custom properties for theming */
:host {
    --primary-color: #0070d2;
    --spacing-md: 16px;
}

.title {
    color: var(--primary-color);
    margin-bottom: var(--spacing-md);
}

/* Conditional host styling */
:host(.compact) {
    padding: 8px;
}

:host(.full-width) {
    width: 100%;
}

Dynamic Class Pattern (JavaScript Side)

get containerClass() {
    let classes = 'container';
    if (this.isExpanded) classes += ' expanded';
    if (this.hasError) classes += ' error';
    return classes;
}

Common Debugging Checklist

  • Is the CSS file named exactly the same as the component?
  • Are you using :host to style the component’s own host element?
  • Did you forget display: block on :host?
  • Are you trying to style a child component from a parent? (You cannot do this.)
  • Is a more specific selector overriding your style? Check the Styles pane.
  • Are you using quotes around a dynamic class attribute? (Remove them.)
  • Have you checked the Computed tab to see the resolved values?
  • Is SLDS overriding your custom style? Check specificity.

SLDS Integration Tips

  • Use SLDS utility classes for spacing: slds-m-bottom_medium, slds-p-around_small
  • Use SLDS for text: slds-text-heading_large, slds-text-body_regular
  • Custom CSS and SLDS classes can coexist on the same element
  • When SLDS does not cover your need, write custom CSS in your component file

That wraps up our introduction to CSS for Lightning Web Components. We covered what CSS is at a fundamental level, walked through the selectors and properties you will use daily in LWC development, explored how Shadow DOM scoping changes the way you think about styles, and went through practical debugging techniques. The cheat sheet at the end should serve as a quick reference as you start styling your own components.

In the next part of this series, we will continue building on these fundamentals and start looking at more advanced patterns for component styling and layout. Until then, open up DevTools on a Lightning page, inspect a few components, and get comfortable navigating the shadow tree. The more you practice reading styles in the browser, the faster you will get at writing them in your editor. See you there.