Authoring Guidelines

This document is living, what might have been the guidance 3, 6, 9 months ago might not be the same today. Please review this document quarterly to ensure any guidance has not changed.

Getting Started

The Salesforce Design System aims to build foundational low-level web components that are optimized for native shadow. With this goal in mind, we need to author our HTML and CSS that align with the methodologies of web components. For the purpose of authoring code, we want to focus on encapsulation, interobability, and composition. With these in mind, we can build secure, flexible, and performant web components for the Salesforce ecosystem.

Pre-requisites

This documentation assumes you have an understanding of standards-based web technologies and the Salesforce platform. Additionally, we recommend having a solid grasp of these concepts:

Understanding these topics beforehand is extremely beneficial when reading our documentation.

Definition of Ready

Please review and understand our Definition of Ready ↗ policy before proceeding with the creation of a component specification and authoring of source code.

Definition of Done

Please review and understand our Definition of Done ↗ policy before proceeding with the creation of a component specification and authoring of source code.

Authoring HTML

Best Practices

TLDR;

Attribute selectors

In the context of Web Components, we want to author CSS selectors using attribute selectors. In particular, the part attribute.

<sds-foo-component>
  #shadow-root
  <div part="container">
    <h2 part="title">Foo</h2>
    <p>...</p>
  </div>
</sds-foo-component>

Using the part attribute offers interoperability between the shadow DOM and the light DOM. If your CSS and HTML reside in the same scoped DOM tree, you can target the element with the part attribute using an attribute selector:

/* CSS and HTML both live in the light DOM */

[part='container'] {
  border: 1px solid black;
}

If your CSS and HTML reside in different scoped DOM trees, the attribute selector will not work. For example, if your CSS lives in the light DOM, and the HTML you want to style lives in a shadow DOM, the shadow boundary blocks off the attribute selector.

But we can use the Part API to access the div because we permitted it by using a named part attribute. part allows the customer to add CSS on top of the foundational CSS we will author in the SDS component. In addition, we will expose Styling Hooks to provide supported customizations to our foundational components.

/* CSS lives in light DOM, HTML lives in shadow DOM */

sds-foo-component::part(container) {
  background: white;
}

Important Authoring Detail

Using the Part API is for our customers and subsystems, you should not author CSS in SDS using the Part API syntax.

API derived selectors

A feature-rich component will provide visual feedback based on a user's interaction or particular state, such as an error state. We still want to utilize attribute selectors to manage the life-cycle of the component.

For example, let's say a user clicks on the component to toggle the state of its button element. The event is fired and adds the state of the component as an attribute:

<sds-button clicked>
  #shadow-root
  <button>Clicked</button>
</sds-button>

The attribute can be accessed via the custom element, or in our case as the owner of the component, the :host:

:host([clicked]) {
  --sds-c-button-color-background: red;
}

An added benefit is the forced accessibility by having our APIs rely on ARIA standard attributes, i.e., aria-pressed.

<sds-button>
  #shadow-root
  <button aria-pressed="true">Clicked</button>
</sds-button>

That same attribute selector can be accessed since we own the component and style it to provide visual feedback to our users.

[aria-pressed='true'] {
  --sds-c-button-color-background: red;
}

Slots and named slots

Slots and named slots are a great way to provide placeholders in your component. They act as a way for our customers to compose different variations of our component without touching the internals of the component.

The default slot is where content would go if the customer simply wrapped the custom element around their content.

<sds-button>My Button</sds-button>

If we look at the internals of the component:

<host>
  #shadowroot
  <button>
    <slot name="start"></slot>
    <slot></slot>
    <slot name="end"></slot>
  </button>
</host>

The custom would expect the text of "My Button" to display in the default, unnamed slot.

Since we don't want to limit our customers ability to compose different experiences, by default, we offer additional placeholders that take in consider variations in the buttons design. A slot with the name start would display content before the default slot if targeted.

<sds-button>
  <sds-icon slot="start">
  My Button
</sds-button>

The customer could then have an icon before the text of its button. Alternatively, the customer could target the end slot and place content after the text of the button. Or both, the control is now in the hands of the customer.

Important Authoring Detail

Do not add CSS that targets a slot, when empty, the CSS still applies to the slot and cannot be removed. Use ::slotted() instead.

Don't 🚫

slot {
  padding: 1rem;
}

Do ✅

::slotted(div) {
  padding: 1rem;
}

Attribute vs Slot

In the scenario where a component can accept a text node (no element nodes) that will be displayed within the component, should the user pass in their text node through an attribute or slot?

We recommend authors use slots over attributes in this scenario. Applied more broadly, use slots for light DOM content. Use attributes for configuration and data.

<!-- Attribute Approach -->
<sds-chip label="26 Points"></sds-chip> 🚫

<!-- Slot Approach -->
<sds-chip>26 Points</sds-chip>

A few key points to support this recommendation:

For a deeper dive into this topic, view the related Github issue.

Common mistakes to avoid

When going from global cascade to component cascade, we are faced with new challenges when authoring CSS. By design, moving to web components shifts the cascade to your component and only effects the structure of the component you own, unless through the use of supported APIs such as CSS custom properties and the Part API.

The following aims to highlight the pitfalls of moving from a global cascade to the component's encapsulated cascade.

Custom Elements

SDS Web components encourage ownership and interopability, this is achieved by creating a custom element that is scoped to a systems namespace. SDS will deliver custom elements with the sds namespace, e.g., sds-button. When a web component is registered, the new element appears in the lightDOM and wraps the internals of the component’s HTML. This is important to remember when authoring your HTML and CSS, the custom element prevents using descendant selectors to target the immediate children of an element.

<div class="context">
  <sds-button>
    <span>My Button</span>
  </sds-button>
</div>

Without the custom element, <span>My Button</span> would be the direct descendants of <div class="context" />, and targeting descendant or adjacent selector matching would fail.

.context > span {
  background-color: red; /* won't work */
}

Shadow DOM

One of the most important aspects of authoring web components is encapsulation. This helps keep the markup structure, style, and behavior hidden and separate from other code on the page so parts don’t clash and the code is kept clean. The Shadow DOM API serves this purpose by attaching a hidden DOM to an element.

<host>
  #shadowroot
  <div class="my-component">...</div>
</host>

Any HTML that is inside the Shadow Root attached to the custom element is now blocked by a Shadow Boundary.

This is great! but can be a problem if a customer relied on your component and had targeted CSS to changed the visual output of your nested component:

<div class="parent">
  <h2>My Context</h2>
  <sds-foo-component>
    #shadow-root
    <h2>My Component</h2>
  </sds-foo-component>
</div>

With no Shadow DOM and relying on the global cascade, the following CSS would work:

.parent h2 {
  font-size: 2rem;
}

With the Shadow DOM, the encapsulation would block this selector match. The .parent class is unaware of the DOM inside the Shadow DOM of sds-foo-component. The h2 inside of the Shadow DOM will then fall back to the user agent declarations for an h2.

If styles are declared at the earlier in the HTML document, selector matching will work only on elements found outside of Shadow DOM, referred to as the Light DOM.

By design, and the benefit of encapsulation, the CSS declarations inside a Shadow Boundary will not cross-contaminate other matching selectors.

/* Global CSS */
h2 {
  font-size: 2.4rem; /* matches .parent h2 */
}
/* Component CSS */
h2 {
  font-size: 2rem; /* matches sds-foo-component h2 */
}

Important Authoring Detail

Selector matching cannot cross shadow boundaries. For example, if you are using a component inside your Shadow DOM that you do not own, and it has its own Shadow DOM then you cannot style its DOM. The only way around this restriction is to use Styling Hooks (CSS custom properties) or the Part API.

Authoring CSS

Best Practices

TLDR;

Formatting & Rules

These rules are enforced during development and at build time. Please familiarize yourself with our coding style.

CSS Selector, Values and Fallbacks

When authoring CSS for you component, you need to consider how much customization and at which level will the customization come from. These levels of customization are defined by the Global Styling Hooks RFC.

Selectors

We want to author CSS selectors using attribute selectors. In particular, the part attribute.

Using the part attribute offers interopability between the shadow DOM and the light DOM. In light DOM the selector can be targeted via the attribute selector:

[part='container'] {
  border: 1px solid black;
}

Attribute selector provide the same performance and flexibility of classes, but can derive attribute declarively from the component's public APIs.

[part='button'][pressed] {
  background: red;
}

Using static values

It makes sense some of the time to not allow for customizations on your component property, for example, always wanting your component's display property to be block. If its the opinion of you, as the author, to lock down customization on a property then you would author it as a static value.

[part='container'] {
  display: block;
}

Using runtime values and setting sound defaults

Adversely, more often then not you want to allow for the customer to provide property customizations. This is where Styling hooks (CSS Custom Properties) come into play. The value of these custom properties either take a pre-defined value from a parent scope or are used as a placeholder (no initial value).

selector {
  color: var(--sds-c-foo-text-color);
}

If --sds-c-foo-text-color has not been defined previous in the document, then this custom property computes to the initial value provided by the browser. In the case of color, the value would be inherit.

You can provide a sound default for the property by provide a static value as the last fallback in the var() function.

selector {
  color: var(--sds-c-foo-text-color, black);
}

Common fallback values are typically set for properties like structure and brand colors.

If no default is necessary, negate the fallback from your var() function.

Local customizations

Properties that enable scoped changes to a component based on context. i.e. “Within this experience, I want a bigger CTA to capture my audiences attention...”

For detailed specifications on component styling hooks, see Component Styling Hooks RFC.

Important Authoring Detail

This should always be defined first, before the shared and global styling hooks.

selector {
  property: var(--component);
}

Shared customizations

selector {
  property: var(--component, --shared);
}

Shared hooks are properties that propagate into more than one component or element that share semantically similar meanings. i.e., “All my form elements share these properties...”

Shared hooks are prefixed with -s-; this indicates that a Styling Hook is shared between many components within a scope that shares styles. For example, if my scope is inputs, you can expect the Styling Hook to start with --sds-s-input-.

The scope does not have to be a set of components, but it could be HTML elements like text headings, ranging from h1-h6. The defining quality of a shared hook is that the elements must share semantically similar meanings.

Important Authoring Detail

This should always be defined after the component scope but before the global scope.

For detailed specifications on shared styling hooks, see Shared Styling Hooks.

Global customizations

selector {
  property: var(--component, var(--shared, --global));
}

Properties that holistically change the customer’s application. i.e., “My brand looks like....”. These properties come with the default values provided by SDS but when overridden would propagate throughout the customer’s application.

The global scope properties are prefixed with -g-, this is an indicator that the Styling Hook can be used on anything; a standard HTML element, in a component, in a selector, whatever — it has no rules.

Important Authoring Detail

This should always be the last property in your custom property fallback cascade.

For detailed specifications on global styling hooks, see Global Styling Hooks.

No customizations

selector {
  property: value;
}

If you want to allow for no customizations on a property of your component, simply negate any CSS custom properties from the value declaration.

Property Reassignments

If you are extending an existing selector in your component, whether it be to add a state or a variant, you should take advantage of reassigning the custom property that is already declared on the base selector wanting to extend, for example:

[part='base'] {
  background: var(--sds-c-foo-color-background);
}

Here the part=base is exposing --sds-c-foo-color-background. We want to use this custom property, that is originally mapped to the background property to change the color of our background when my component's API reflects a different state of my component.

[part='base'][selected] {
  --sds-c-foo-color-background: red;
}

Instead of redeclaring the background property, I just inherited that property value from --sds-c-foo-color-background and reassigned a new value.