Skip to content

Coding Conventions

Eliza Khachatryan edited this page Aug 13, 2026 · 6 revisions

tags: [conventions]

This is a living document defining our best practices and reasoning for authoring Calcite Components.

General Guidelines

Generally adhere to and follow these best practices for authoring components.

Component Responsibilities

Calcite Components broadly targets two groups of projects inside Esri:

Components should present the minimum possible implementation to be usable by both sites and apps and leave as much as possible to users.

It is generally agreed on that components should not:

  • Make network requests. Authentication and the exact environment of the request is difficult to manage and better left to the specific application or site.
  • Manage routing or manipulate the URL. Managing the URL is the domain of the specific site or app.
  • Implement any feature which can easily be achieved with simple CSS and HTML. E.x. it was decided that <calcite-switch> should not support text or position properties because those could be easily duplicated with CSS (ref)
  • Implement any component which might replace a browser feature, without adding functionality that goes above and beyond what browser defaults would provide.

However components are allowed to:

  • Use or implement localStorage if there is a specific use case.
  • Communicate with other components if a specific use case exists.

Discussed In:

Events

All public events should be documented with JSDoc.

Event Names

Event names should be treated like global variables since they can collide with any other event names and global variables. As such follow these guidelines when naming events.

  • Name events list Component + Event name for example the change event on <calcite-tabs> should be named calciteTabsChange.
  • Always prefix event names with calcite and never use an event name used by existing DOM standards https://developer.mozilla.org/en-US/docs/Web/Events.
  • For example:
    • Bad: change
    • Good: calciteTabChange
  • If an existing event can be listened to, don't create a new custom event. For example, there is no need to create a calciteButtonClick event because a standard click event will still be fired from the element.
  • For consistency, use calcite<ComponentName>Change for value change events.

Discussed In:

Private/Internal Events

If you need to use events to pass information inside your components for example to communicate between parents and children make sure you call event.stopPropagation(); and event.preventDefault(); to prevent the event from reaching outside the component.

Also, make sure to add the @internal JSDoc tag to hide an event from the generated doc or @private to hide it from both the doc and generated type declarations.

Event Details

Only attach additional data to your event if that data cannot be determined from the state of the component. This is because events also get a reference to the component that fired the event. For example you do not need to pass anything exposed as a @Prop() in the event details.

@Listen("calciteCustomEvent") customEventHandler(
  event: CustomEvent
) {
  console.log(event.target.prop); // event.target is the component that fired the event.
}

<calcite-tab-nav> is also an example of this. The event.details.tab item contains the index of the selected tab or the tab name which cannot be easily determined from the state of <calcite-tab-nav> in some cases so it makes sense to include in the event.

Native event cancellation

When a component handles events for its own interaction (e.g., moving between list items, closing an open menu), if the event is tied to default browser behavior (e.g., space key scrolling the page), Event.preventDefault() must be called to avoid mixed behavior.

class SomeInputTypeComponent {
  handleKeyDown(event: KeyboardEvent): void {
    if (event.key === "Escape") {
      /* clear text/close popover */
      event.preventDefault(); // let browser or other components know that the event has been handled
    }
    // ...
  }
}

For composite components or components that support children (either light or shadow DOM), they may need to check if an event has been canceled (Event.defaultPrevented) before handling it.

class CompositeOrParentComponent {
  handleKeyDown(event: KeyboardEvent): void {
    if (
      event.key === "Escape" &&
      !event.defaultPrevented // check if child component has already handled this
    ) {
      /* close */
      event.preventDefault(); // let browser or other components know that the event has been handled
    }
    // ...
  }
}

Interaction events

Pointer events should be used in favor of mouse events to maximize device compatibility.

Event listeners

There are a few ways to add event listeners within our components:

  1. @Listen decorator
    • automatically cleaned up by component lifecycle
    • does not provide event type information
    • event name is not type checked
  2. JSX event listener props
    • automatically cleaned up by component lifecycle
    • cannot specify event listener options (some events may have a matching capture prop)
    • provides event type information
    • event name is type checked
  3. addListener
    • not removed by the component lifecycle, so the listener needs to be explicitly removed to prevent memory leaks
    • provides total flexibility regarding event listener options
    • provides event type information
    • event name is not type checked

1 and 2 should be used whenever possible (which one you use will depend on convenience). 3 should only be used whenever 1 and 2 are not possible or ideal.

Event handling guidelines: Programmatic vs User-initiated actions event story

status

Public interaction events

Public interaction events communicate state changes caused by user interaction.

Setting a component property or calling a programmatic API must update component state, but should not emit public interaction events unless that API explicitly documents an event contract.

Associated state changes

A single user action can cause multiple observable state changes. Emit each public event whose corresponding state changed as part of that action.

Do not emit related events when the action does not change the state represented by that event.

For example, closing the selected tab closes that tab and changes selection. Emit the close event and the tab-selection events. Closing an unselected tab closes that tab but leaves selection unchanged. Emit the close event, but not selection events.

Lifecycle events

Lifecycle events that signal the start or completion of an asynchronous component transition may emit for both user-initiated and programmatic changes.

For example, consumers may rely on before-open, open, before-close, and close events to coordinate work with an open or close transition.

Internal events

Internal events may be emitted as implementation details to coordinate component parts. They are not a public event contract.

For example, calcite-tab-title emits calciteInternalTabsActivate to coordinate selection with calcite-tab-nav. The internal event may be emitted for programmatic activation because the components still need to synchronize state. The public calciteTabsActivate event emits only for user-triggered activation.

Properties

Private/internal properties should be annotated accordingly to avoid exposing them in the doc and/or API. You can do this by using the @private/@internal JSDoc tags.

Property values

Property values should be meaningful. Avoid using values such as "default" for default values. Instead, use a more descriptive value.

For instance: placement: "start" | "end" = "start"; is preferred to placement: "default" | "end" = "default";

Reflecting to attributes

It is recommended to reflect properties that fit the following criteria:

  • are static or will not be updated frequently during the component lifespan (e.g., a number that represents a range min or max would be reflected, but a number that represents a value that will constantly be updated by the user would not)
  • value represents non-rich data or booleans/numbers/strings that are not used as content (e.g., a string that represents a mode would be reflected, but a string that represents a placeholder, title or summary would not)
  • are public and belong to a public component
  • required for internal styling or would make internal styling easier

Doing so will give developers more flexibility when querying the DOM. This is important in framework environments where we can't safely assume components will have their attributes set vs properties.

Focus support

Components with focusable content, must implement the following pattern:

interface FocusableComponent {
  setFocus(focusId?: FocusId): Promise<void>; // focusId should be supported if there is more than one supported focus target
}

type FocusId = string;

Note: Implementations can use the focusElement helper to handle focusing both native and calcite components.

Examples:

CSS Class Names

Because most components utilize shadow DOM, there is far less concern over naming collisions in a global CSS namespace. In addition, it's better for file transfer times and easier to write if class names are shorter. For these reasons, full BEM is not necessary. Instead, we can omit the "Block", and use the host instead. Consider the following BEM markup:

<div class="card">
  <h3 class="card__title card__title--large">Title</h3>
  <p class="card__text">Text</p>
</div>

In a component using shadow DOM, this should instead be written as:

<Host>
  <h3 class="title title--large">Title</h3>
  <p class="text">Text</p>
</Host>

Notice .card does not appear anywhere. We would then apply styles to the host element itself:

:host {
  // card styles here
}

.title {
}
.title--large {
}
.text {
}

Modifier classes on the "block" (host element) can often be written by reflecting the prop and selecting it directly via an attribute selector:

:host([kind="success"]) {
}

This builds a nice symmetry between the styling and the public API of a component.

CSS lookup object

To enhance maintainability and facilitate easier refactoring, it's beneficial to use a CSS lookup object for storing class names instead of hardcoding them within components. This approach not only helps prevent errors, such as typos in class names but also simplifies global updates to styling by centralizing class names in one location. Additionally, it proves useful in testing scenarios involving shadow DOM selectors.

Here’s how you can implement a CSS lookup object:

// resources.ts
export const CSS = {
  card: "card",
  title: "title",
  titleLarge: "title--large",
  text: "text",
};

Usage in a component:

<div class={CSS.card}>
  <h3 class={{ [CSS.title]: true, [CSS.titleLarge]: this.largeTitle }}>Title</h3>
  <p class={CSS.text}>Text</p>
</div>

For dynamic class generation, useful for mutually exclusive classes (e.g., scale), the object can also include functions:

// resources.ts
export const CSS = {
  card: "card",
  title: "title",
  titleLarge: "title--large",
  text: "text",
  titleScale: (scale: Scale) => `title--scale-${scale}` as const,
};

Using dynamic classes:

<div class={CSS.card}>
  <h3
    class={{
      [CSS.title]: true,
      [CSS.titleLarge]: this.largeTitle,
      [CSS.titleScale(this.scale)]: true,
    }}
  >
    Title
  </h3>
  <p class={CSS.text}>Text</p>
</div>

Note: Ensure that class-generating functions are strongly typed to avoid runtime errors.

Unique IDs for Components

Many times it is necessary for components to have a id="something" attribute for things like <label> and various aria-* properties. To safely generate a unique id for a component but to also allow a user supplied id attribute to work follow the following pattern:

import { guid } from "../../utils/guid";

@Component({
  tag: "calcite-example",
  styleUrl: "example.scss",
  shadow: true,
})
export class Example {
  // ...

  guid: string = `calcite-example-${guid()}`;

  render() {
    const id = this.el.id || this.guid;
    return <Host id={id}></Host>;
  }

  // ...
}

This will create a unique id attribute like id="calcite-example-51af-0941-54ae-22c14d441beb" which should have a VERY low collision change since guid() generates IDs with window.crypto.getRandomValues. If a user supplies an id this will respect the users id.

Cleaning up resources

Ensure all components clean up their resources.

Timeouts

When using setTimeout(), make sure that you clear the timeout using clearTimeout() in cases where the same timeout may be called again before the first timeout has finished or if the handler is no longer needed. For example, the handler may no longer need to be called if the component was disconnected from the DOM.

Example:

menuFocusTimeout: number;

focusMenu(): void => {
  clearTimeout(this.menuFocusTimeout);
  this.menuFocusTimeout = window.setTimeout(() => focusElement(this.menuEl), 100);
}

Layering

Avoid setting z-index ad hoc and instead use a contextual z-index layer from the Tailwind z-index extension. This will ensure proper layering across components.

Utils

There are utilities for common workflows in src/utils.

Global attributes

The globalAttributes util was specifically made to access the lang global attribute when set on a Calcite component. However, it can be extended to allow additional global attributes by adding to the allowedGlobalAttributes array. The util is used in calcite-pagination, which you can use as a reference.

Usage steps

  1. Import the interface and watch/unwatch methods

    import {
      GlobalAttrComponent,
      watchGlobalAttributes,
      unwatchGlobalAttributes,
    } from "../../utils/globalAttributes";
  2. Implement the interface

    export class ComponentName implements GlobalAttrComponent {
  3. Add globalAttributes state

    @State() globalAttributes = {};
  4. Add connect/disconnect callbacks

    connectedCallback(): void {
        watchGlobalAttributes(this, ["lang"]);
    }
    
    disconnectedCallback(): void {
        unwatchGlobalAttributes(this);
    }
  5. Use the state to access lang (or another global attribute that may be allowed in the future).

    const lang =
      this.globalAttributes["lang"] || document.documentElement.lang || "en";

BigDecimal

BigDecimal is a number util that helps with arbitrary precision arithmetic. The util is adopted from a Stack Overflow answer with some small changes. There are some usage examples in number.spec.ts.

Custom child element support

In order to support certain architectures, parent components might need to handle custom elements that wrap their expected child items within shadow DOM that would prevent discovery when querying the DOM.

For such cases, the following pattern will enable developers to create custom child/item components and have them work seamlessly with parent components.

Parent component

  • Must provide a customItemSelectors property to allow querying for custom elements in addition to their expected children.
  • An interface for the class (used by custom item classes) and element (used by parent component APIs) must be created in the parent's interfaces.d.ts file, where the necessary child APIs must be extracted.
Example
parent/interfaces.d.ts
type ChildComponentLike = Pick<
  Components.CalciteChild,
  "required" | "props" | "from" | "parent"
>;
type ChildComponentLikeElement = ChilcComponentLike & HTMLElement;
parent/parent.tsx
  @Prop() selectedItem: HTMLChildComponentElement | ChildComponentLikeElement;
custom-item/custom-item.tsx
export class CustomItem implements ChildComponentLike {
  private childComponentEl: HTMLChildComponentLikeElement;

  @Prop() required: boolean;
  @Prop() props: string;
  @Prop() from: number;

  @Method() async parent(): Promise<string> {
    await this.childComponentEl.parent();
  }

  render(): VNode {
    return (
      <Host>
        <child-component
          required={this.required}
          props={this.props}
          from={this.from}
          ref={(el) => (this.childComponentEl = el)}
        />
      </Host>
    );
  }
}

Custom child component

  • Must implement the element interface expected by the parent (e.g., ChildComponentLike).

Notes

  • This pattern should be applied sparingly and on a case-by-case basis.
  • We can refine this pattern as we go on, but additional modifications needed to handle the custom items workflow will be considered out-of-scope and thus not supported.
  • Until we have documentation covering creating custom elements, customItemSelectors must be made internal and any ChildComponentLike types must be excluded from the doc.
  • Please refer to https://github.com/Esri/calcite-design-system/pull/7608/ as an example on how this pattern is applied.

Clone this wiki locally