Best Practices

The following is a non-exhaustive list of conventions, patterns, and best practices we try to follow. As a contributor, we ask that you make a good faith effort to follow them as well. This ensures consistency and maintainability throughout the OLYSENSE Design System.

ODS components are built with accessibility in mind. Creating components that are fully accessible to users with varying capabilities across a multitude of circumstances is a challenge. Oftentimes, the solution to an accessibility (a11y) problem is not obvious, therefore, we may not get it right the first time.

There are, however, guidelines we can follow in our effort to make ODS an accessible foundation from which applications and websites can be built.

We take this commitment seriously, so please ensure your contributions have this goal in mind. If you need help with anything a11y-related, please reach out to the community for assistance. If you discover an accessibility concern within the library, please file a bug on the ODS Issue Tracker .

It’s important to remember that, although accessibility starts with foundational components, it doesn’t end with them. It everyone’s responsibility to encourage best practices and ensure we’re providing an optimal experience for all of our users.

The naming conventions we use for branches and pull requests use the folliwng format:

[story|bug] - [ticket-number] - [short-description]

Examples:

  • Story (Feature): story-12345-date-range-picker
  • Bug: bug-12345-primary-button-styles

Components should be composable, meaning you can easily reuse them with and within other components. This reduces the overall size of the library, expedites feature development, and maintains a consistent user experience.

All components have a host element, which is a reference to the <ods-*> element itself. Make sure to always set the host element’s display property to the appropriate value depending on your needs, as the default is inline per the custom element spec.

:host {
display: block;
}

Aside from display, avoid setting styles on the host element when possible. The reason for this is that styles applied to the host element are not encapsulated. Instead, create a base element that wraps the component’s internals and style that instead. This convention also makes it easier to use BEM in components, as the base element can serve as the “block” entity.

When authoring components, please try to follow the same structure and conventions found in other components. Classes, for example, generally follow this structure:

  • Static properties/methods
  • Private/public properties (that are not reactive)
  • @query decorators
  • @state decorators
  • @property decorators
  • Lifecycle methods (connectedCallback(), disconnectedCallback(), firstUpdated(), etc.)
  • Private methods
  • @watch decorators
  • Public methods
  • The render() method

Please avoid using the public keyword for class fields. It’s simply too verbose when combined with decorators, property names, and arguments. However, please do add private in front of any property or method that is intended to be private.

This might seem like a lot, but it’s fairly intuitive once you start working with the library. However, don’t let this structure prevent you from submitting a PR. Code can change and nobody will chastise you for “getting it wrong.” Encouraging consistency helps keep the library maintainable and easy for others to understand.

Boolean props should always default to false, otherwise there is no way for the user to unset them using only attributes.

/** Shows heading when set to true. */
@property({ type: Boolean, reflect: true }) disabled = false;

To keep the API as friendly and consistent as possible, use a property such as noHeader and a corresponding kebab-case attribute such as no-header.

/** Shows heading when set to true. */
@property({ type: Boolean, reflect: true, attribute: 'no-header' }) noHeader = false;

When naming boolean props that hide or disable things, prefix them with no-, e.g. no-spin-buttons and avoid using other verbs such as hide- and disable- for consistency.

When a component relies on the presence of slotted content to do something, don’t assume its initial state is permanent. Slotted content can be added or removed any time and components must be aware of this.

A good practice to manage this is as follows:

  • Add @slotchange={this.handleSlotChange} to the slots you want to watch.
  • Add a handleSlotChange method and use the hasSlot utility to update state variables for the the respective slot(s).
  • Never conditionally render <slot> elements in a component — always use hidden so the slot remains in the DOM and the slotchange event can be captured.

See the source of Card , Dialog , or Drawer for examples.

When providing fallback content inside of <slot> elements, avoid adding parts, e.g.:

<slot name="icon">
<sl-icon part="close-icon"></sl-icon>
</slot>

This creates confusion because the part will be documented, but it won’t work when the user slots in their own content. The recommended way to customize this example is for the user to slot in their own content and target its styles with CSS as needed.

Components must only emit custom events, and all custom events must start with ods- as a namespace. For compatibility with frameworks that utilize DOM templates, custom events must have lowercase, kebab-style names. For example, use ods-change instead of odsChange.

This convention avoids the problem of browsers lowercasing attributes, causing some frameworks to be unable to listen to them.

To expose custom properties as part of a component’s API, scope them to the :host block.

:host {
--color: var(--ods-sys-color-on-accent);
--background-color: var(--ods-sys-color-accent-muted);
}

Then use the following syntax for comments so they appear in the generated docs. Do not use the —ods-sys-, —ods-ref-, —ods-comp- prefixes. They aere reserved for design tokens that live in the global scope.

/**
* @cssproperty --color: The component's text color.
* @cssproperty --background-color: The component's background color.
*/
export default class OdsExample {
// ...
}

When designing a component’s API, standard properties are generally used to change the behavior of a component, whereas CSS custom properties (“CSS variables”) are used to change the appearance of a component. Remember that properties can’t respond to media queries, but CSS variables can.

There are some exceptions to this (e.g. when it significantly improves developer experience), but a good rule of thumbs is “will this need to change based on screen size?” - If so, you probably want to use a CSS variable.

There are two ways to enable customizations for components. One way is with CSS custom properties (“CSS variables”), the other is with CSS parts (“parts”).

CSS variables are scoped to the host element and can be reused throughout the component. A good example of a CSS variable would be —border-width, which might get reused throughout a component to ensure borders share the same width for all internal elements.

Parts let you target a specific element inside the component’s shadow DOM but, by design, you can’t target a part’s children or siblings. You can only customize the part itself. Use a part when you need to allow a single element inside the component to accept styles.

This convention can be relaxed when the developer experience is greatly improved by not following these suggestions.

Form controls should support submission and validation through the following conventions:

  • All form controls must use name, value, and disabled properties in the same manner as HTMLInputElement
  • All form controls must have a setCustomValidity() method so the user can set a custom validation message
  • All form controls must have a reportValidity() method that report their validity during form submission
  • All form controls must have an invalid property that reflects their validity
  • All form controls should mirror their native validation attributes such as required, pattern, minlength, maxlength, etc. when possible
  • All form controls must be tested to work with the standard <form> element

A Custom Elements Manifest is a JSON file used to describe Web Components. It helps tools (like IDEs, documentation generators, or design systems) understand and work with custom elements more effectively. The metadata in a file describes the following:

  • Custom elements (name, tag, attributes, properties, events, slots, etc.)
  • JavaScript modules and their exports
  • Relationships between components and modules
  • Documentation-related info (e.g., descriptions, JSDoc tags)

When documenting components and patterns you should utilize the following order when displaying sections of the documentation:

  1. Examples: Variants that we want to display for a component. Examples include source code for HTML (Web Components) and React.
  2. Importing: The ES6 import statment to use when importing the component into your application.
  3. Metadata: Slots, Properties, Event, Methods, Parts, Dependencies, etc.
  4. Usage and Content Guidelines: Dos and Donts and guidelines on the content that should be used within a component or pattern.

You best bet is to simply follow the structure of an existing components documation page.

The Metadata section uses the custom-elements.json file and JSDocs in a components source to generate those sections.

At times, after making updates to a components source, you must re-generate custom-elements.json file to see those changes reflected in the documation.

Terminal window
npx nx auto-generate-files @ods/components

Now that you have learned more about or best practices, visit the next step to learn how to submit design contributions for ODS.

Search