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:
- CSS cascade, specificity, and inheritance ↗
- CSS custom properties ↗
- Web components (custom elements, shadow DOM, and HTML templates) ↗
- How subsystems will build on top of the CSS you author ↗
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;
- Use named
partattribute selectors to describe the regions/elements of your component. - The Part API is public, meaning it cannot change or be removed, so be selective on when to use the
partattribute. Typically, you use thepartattribute when you want the customer to have more control beyond Styling Hooks customizations. If you are unsure, please ask for feedback in your PR. - Attributes that describe the life-cycle of the component should be derived from the component's supported APIs.
- Encourage template extensibility through slots and named slots.
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:
- Information passed to the component does not need to be an attribute in order to run some form of check on it. If there is a requirement that a component only accepts a certain content type, we can run a check on the content passed into the slot.
- If we rely on attributes for displaying content, this depends on the custom element to work properly. In any instance where the custom element stops working, the content will fail to display and become inaccessible. In contrast, if the content is passed through a slot and the custom element fails, the content will still be available, visible, and accessible.
- We aim to align with the web platform. We should take that into consideration when thinking about our design patterns. Generally, built-in HTML elements allow you to pass in content via the light DOM.
- If, and with enough time ultimately when, a design of a component is updated to accept more than the original constraints imposed by it, the component will be more robust to changes.
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;
- A component's parts (i.e.,
part="button") defines its anatomy within its template. Think of it as the bones of the component, giving it shape before anything else is added. - Expose CSS custom property placeholders (custom property with no value) on the elements that want the customer to have the ability to customize.
- Fallbacks are only required if you want to ensure a property has a default value. This is usually the case for the structure of a component.
- Use reassignments (setting a new value on an existing custom property) when styling the lifecycle states of the component, such as
disabledor:hover. - If you do not want a customer to change a property of your component, do not use a CSS custom property. Just use a static value.
- Only use a class within a shadow host when
:host, attribute, orpartdoes not make sense. The style should be superficial and cosmetic. Use sparingly. - Do not author CSS on a
slotelement. Use the::slotted()psuedo selector to add CSS when an element is within the specific slot.
Formatting & Rules
These rules are enforced during development and at build time. Please familiarize yourself with our coding style.
-
Use 2 spaces for file indention (not tabs).
-
There should be a line between each rule.
-
Properties should be grouped in this order:
- custom properties
appearancebox-sizing(if override needed)- layout
positionfloatflexgrid- other displays
- sizing
widthmax-widthmin-widthheightmax-heightmin-height
- spacing
marginborderpadding
- masking
overflowclip
- typography
font-stylefont-weightfont-sizeline-heightfont-familylist-style- etc
- theming
background-colorbox-shadowcolortext-shadow
- interaction
cursorresize
- animations & transitions
-
Where possible, limit CSS files’ width to 80 characters. Reasons for this include
- the ability to have multiple files open side by side;
- viewing CSS on sites like GitHub, or in terminal windows;
- providing a comfortable line length for comments.
- There will be unavoidable exceptions to this rule—such as URLs, or gradient syntax—which shouldn’t be worried about.
-
All properties should be placed directly beneath their selector.
-
Single quotes for keywords and URLs.
-
New line break per selector, including comma separated selector declarations.
-
Try not to exceed 3 levels of specifity. There should be little to no reason to have more than 2 levels of specificity when authoring CSS for Shadow DOM due to encapsulation and smaller footprint of the component.
-
Use logical properties to handle bi-directional properties such as
margin,padding, e.g.,margin-inline-start. -
Use long-hand for bi-directional spacing properties like
marginandpadding, e.g.,margin-inline-start: 4px;vsmargin: 4px 4px 4px 4px;
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.