How to Style Your Web Components

When working within your subsystem, there are several approaches to styling web components. Choosing your subsystem's CSS strategy will depend on your context and what you want to accomplish. This document will guide you through our recommended styling options.

Assumptions

All recommendations and examples presented in this document are not concerned with how you build a web component. The focus is on the final product. You are free to use your framework of choice as long as the web component that comes out at the end aligns with our recommendations.

Although you could apply concepts in this document outside of SDS, this document assumes you are working with SDS.

Note: SDS components as an importable package are in active development but may not be available at the time of this writing.

How to Style with Styling Hooks

We recommend Styling Hooks over all other options whenever possible. Refer to our Styling Hooks technical documentation for a deep-dive into the concept.

Install SDS Styling Hooks

Install the sds-styling-hooks NPM package:

npm i @salesforce-ux/sds-styling-hooks

Import Globals

In our example, we will be importing and using all of the global hooks. But you may pick and choose which types you want if needed (e.g., color, spacing, typography).

The sds-styling-hooks package provides several different formats for you to use.

For our example, we'll use the out of the box custom properties provided by the package:

/* Your global CSS file */

@import '@salesforce-ux/sds-styling-hooks/dist/hooks.custom-props.css';

After importing, your project's :root scope will have ready-to-use SDS Global Hooks applied to it.

Choose Your Selector

When writing your CSS custom properties, you will place them within a valid CSS selector. The selector will determine how much of the application will inherit the values of the custom properties you set within the selector.

Any valid selector can be a scope; the following examples are only the most common use-cases. The best choice depends on your needs and context.

Target Your App

Choose the following selectors when you want to target large portions of your app.

Root

Use :root when you want app-wide effects. :root is the best location for Global Styling Hooks.

Note: the following example sets --sds-g-spacing-medium, an SDS Global Hook, to a new value. This example overrides the original SDS hook, and the subsystem will not receive updates for this hook from the SDS service until the subsystem removes the override. This use-case, which is one of many, is pertinent if the subsystem wants to define an SDS Global Hook's value. Keep these interconnections in mind when interacting with Global Hooks within a subsystem.

/* Affects whole app */
:root {
  --sds-g-spacing-medium: 1rem;
}
Class

Use classes for region-based effects. They let you target a larger, specific portion of your app.

/* Affects class and its descendents */
.my-region {
  --sds-c-button-spacing-block-start: 1rem;
}
<main>
  <!-- Not affected -->
  <section class="my-region">
    <!-- All children are affected -->
  </section>
  <section class="other-region">
    <!-- Not affected -->
  </section>
  <!-- Not affected -->
</main>

Target Your Component

Choose the following selectors when you want to work within your web component.

Host

:host is your go-to selector for working within the shadow DOM. It will apply your styles to the shadow host and all of the host's descendants.

:host {
  /* Adding extra inline padding to all <sds-button> in the shadow host */
  --sds-c-button-spacing-inline-start: 1rem;
  --sds-c-button-spacing-inline-end: 1rem;
}
<!-- Markup of the custom element as rendered in the browser -->
<host-element>
  #shadow-host
    <p>How awesome are web components?</p>
    <sds-button>Super awesome!</sds-button>
    <sds-button>Web components are life</sds-button>
</host-element>

If you don't have access to a web component's :host, you can target the custom element itself since it lives in the light DOM.

host-element {
  --sds-c-button-color-background: var(--sds-g-color-brand-base-contrast-3);
}
Attributes

Use the attribute selector for targeting a specific part or state of a web component.

/* Styles within the web component's shadow DOM */

/* Affects the web component when it is in a disabled state */
:host([disabled]) {

}

/* Affects a part of the web component designated as a 'listbox' part */
[part~='listbox'] {

}

Choose Your Scope

Next you will need to choose the scope of your Styling Hook. There are three levels of scope in SDS:

/* Scope Examples */

/* Global - Affects whole application */
--sds-g-spacing-medium

/* Shared - Affects all button-like components */
--sds-s-button-text-color

/* Component - Affects only button */
--sds-c-button-spacing-inline-start

Priority Order

  1. Component
  2. Shared
  3. Global

Hooks prioritize local scope over global scope, i.e component-level hooks will override shared hooks, which will override global hooks

Bring It Altogether and Add Your Hooks

Finally, you combine your chosen selectors with the scope of your Styling Hooks.

In the following example, the value of the global spacing hook --sds-g-spacing-large is changed to be larger than the default provided by the earlier imported sds-styling-hook package.

/* Updating the app with a global hook */
:root {
  --sds-g-spacing-large: 2.5rem;
}

Let's say you want all <sds-button> in a specific region to pick up on the updated hook:

/* Updating all <sds-button> within a container that has the .my-region class */
.my-region {
  --sds-c-button-spacing-block-start: var(--sds-g-spacing-large);
}

And <sds-button> within a specific component to pick up on the updated hook:

:host {
  /* Adding extra inline padding to all <sds-button> in the shadow host */
  --sds-c-button-spacing-inline-start: var(--sds-g-spacing-large);
  --sds-c-button-spacing-inline-end: var(--sds-g-spacing-large);
}

How to Style with part

Parts allow you to reach into the shadow DOM and target specific areas of a component for styling. Learn more about parts on MDN.

Warning: styling with part makes your CSS susceptible to breaking changes in the future. Only use part if Styling Hooks cannot solve your use-case.

my-component::part(button):hover {
  transform: scale(1.1);
}

When styling with part, keep these rules in mind:

How to Style with Custom CSS

You may need to add custom CSS to your web component if neither Styling Hooks nor part solves your need. The most common use-case is adding new markup and wanting to style it.

Warning: when adding markup, do not remove any markup or attributes SDS expects. If you attempt to override SDS styles, consider another option like Styling Hooks or rethink your approach. Overriding SDS styles will result in brittle code that may break in the future.

An example of adding markup and styles to the shdaow DOM:

// A vanilla JS example that makes all our SDS buttons decorated with puppies 🐶

// Import sds-button
import Button from '@salesforce-ux/sds-components/src/button/dist/button.js';

// New markup and styles
let tmpl = document.createElement('template');
tmpl.innerHTML = `
  <style>
    .puppy-decoration {
      margin-inline-start: var(--puppy-spacing-xx-small, var(--sds-g-spacing-xx-small));
    }
  </style>
  <span class="puppy-decoration">🐶</span>
`;

// Extend SdsButton and add new markup and styles
export default class PuppyButton extends SdsButton {
  constructor() {
    super();
  }

  connectedCallback() {
    super.connectedCallback();
    let superShadowRoot = this.shadowRoot.querySelector('[part=button]');
    superShadowRoot.appendChild(tmpl.content.cloneNode(true));
  }
}

window.customElements.define('puppy-button', PuppyButton);

When writing custom CSS, keep these rules in mind:

How to Style with Inheritance

The saying often goes with shadow DOM: no styles go in, no styles go out. But this isn't the full story. There are nuances. One of those nuances is inheritance crosses over the shadow boundary.

For example, add a property you can inherit from at a high-level scope:

body {
  color: #eee;
}

And in your shadow DOM, add an element that has a property that would inherit from your style:

<my-cmp>
  #shadow-root
    <p>I'm text that is in the shadow DOM</p>
</my-cmp>

The text in the shadow DOM would inherit a color value of #eee.

Inheritance is how CSS custom properties work with the shadow DOM. Keep in mind factors like user-agent stylesheets and conflicting CSS that will override your inheritance.

In the following example, we set the text color to blue in our CSS:

html {
  color: blue;
}

We expect our text to inherit their color from our CSS, but we run into an unexpected result: the text inside our <p> is blue, but the text inside the <button> is not. The discrepancy is due to the styles coming from the browser's user-agent stylesheets.

<my-cmp>
  #shadow-root
    <p>I'm blue</p>
    <button>I'm not blue</button>
</my-cmp>

How to Keep SDS Styles Intact

Picking up on SDS styles requires keeping the original DOM intact. This requirement looks for the correctly named part and DOM element to be present as defined by the component's SDS specification. If you break the original structure, your HTML is no longer binding with SDS.

An example output of a custom button that extends sds-button:

<!-- 🚫 Incorrect – part="button" was moved to the new div -->
<my-button>
  #shadow-root
    <div class="my-new-element" part="button">
      <button>
        <slot></slot>
      </button>
    </div>
</my-button>

<!-- ✅ Correct – sds-button expects part="button" to be on the button element -->
<my-button>
  #shadow-root
    <div class="my-new-element">
      <button part="button">
        <slot></slot>
      </button>
    </div>
</my-button>

Binding your new markup with SDS ensures you are consistent with SDS and receiving its benefits. Your component will automatically receive its base styling structure and is customizable with Styling Hooks when its CSS gets imported.

How to Make Styles Shareable

Attempting to make a component's aesthetics customizable is a difficult task. Even more challenging is sharing that component out to disparate systems and expecting it to be predictably customizable. SDS and Styling Hooks tackle that obstacle.

Follow these guidelines, and you will prime your components for sharing with the broader ecosystem:

You can pull in other systems' hard work in a shareable ecosystem and not worry about growing your CSS override debt.

<style>
  /* Each unique component is aware of these SDS fallbacks and customizable out of the box */
  :root {
    --sds-g-color-neutral-base-1: var(--my-neutral);
    --sds-g-color-neutral-base-contrast-1: var(--my-neutral-contrast);
    --sds-g-brand-base-1: var(--my-brand);
    --sds-g-brand-base-contrast-1: var(--my-brand-contrast);
  }
</style>

<section>
  <foo-card title="Save Your Changes?">
    <bar-text>Before exiting, would you like to save your progress?</bar-text>
    <buzz-button>Save</buzz-button>
    <buzz-button>Discard</buzz-button>
  </foo-card>
</section>

But keep in mind, sharing is only possible if all the pieces in play are using Styling Hooks correctly.

How to Think Web Components When Styling

Note: work in progress, still flushing this out based on feedback from real-world uses.

Styling web components within SDS requires a subtle shift in thinking. Old ways of going about business, such as using classes for almost everything, don't translate directly.

Use the following tips as overarching guides when working within SDS and web components:

More to come, and to repeat, this section is still a work in progress.