Adding new components

When creating a new component in the Earth Design System, there are two places it can live depending on its function.

For components intended to be part of the core design system, i.e. generic components that aren't tied to specific business logic, they start their journey as an Experimental component.

If a component is being created out of a product need and contains business logic or product-specific functionality it will live as a Shared Widget

Experimental components

One of the goals of the Earth Design System is to avoid being forced to make breaking changes to our components. This challenge stems from the fact that understanding the full scope of a component's requirements, and how it will be used, often isn't clear until we've actually started using those components. New requirements can arise, and sometimes the only solution is to make a breaking change.

In an effort to prevent this, our team has introduced a "graduation path" for components. When we add a new component to the design system, we expose it to consumers via the @adaptavant/eds-core/experimental entrypoint to signal to users that these elements haven't undergone the same scrutiny as the stable ones.

Since all new components begin as experimental, we've updated our component templates to simplify the scaffolding of a new component in the experimental folder. This adjustment lets contributors concentrate on the component's implementation and API, without having to worry about small details like where to put files and other specifics.

Shared Widgets

Shared Widgets are UI components that are not part of the core design system, but lives along side them and leverage EDS components and patterns.

They often contain business logic, or are for product-specific applications and driven from product requirements.

Shared Widgets are exposed via the @adaptavant/eds-core/widgets entrypoint.

Before you start

Before you write a line of code, it's a good idea to consider how the component will be used and the props it will require. Looking at the existing components to find something similar to use as a reference or starting point is a good practice. Additionally, referencing other open source design systems and component libraries can provide valuable insights and inspiration. Should you have any questions or need guidance on the best approach, don't hesitate to reach out to a member of the design system team; we're here to assist and advise you.

How to add a component

Here is a quick walkthrough of how to add a new component (this assumes you've read the Contributing guide and have a basic familiarity with the awds repo).

From the root of the repo, run the following command to create a new Experimental core component:

pnpm new-component

To create a new Shared Widget, run the following command:

pnpm new-widget

You will be provided with the following prompt:

? What is the name of the component?

The casing of your response isn't important here, as we convert it to the appropriate casing (PascalCase for code, and camel-case for filenames). For this example, let's call it "MyComponent."

After choosing a component name, you should see a bunch of newly added and modified files.

Core package

packages/core/src/experimental/my-component/types.ts is the file I'd recommend starting with. This is where we define the component's external API by defining what props it takes. Let's take a look inside:

import { type StyleProps } from '../../shared/style-utils';

type Parts = 'root';

export type MyComponentProps = Partial<StyleProps<Parts>> & {
  children: React.ReactNode;
};

export type GetMyComponentStyles = () => Partial<
  Record<Parts, string>
>;

"Parts" is a string union of all the different parts of your component so that users can override styles for individual elements in a component if they need to. As a convention, the outermost element of all components is called "root". To style the root of a component, you can simply use the className or style props as the majority of the time, this is the element you would want to style.

If you want to style a different element, you can use classNames or styles (notices those are plural). These props accept an object with the different parts (apart from root). Here is an example of how you could override the label text of a button:

<Button variant="accentPrimary"  classNames={{ label: 'text-caution' }}>
  Click me!
</Button>

Notice we targeted "label" which in this case would be the <span> with the text inside of it.

"MyComponentProps" is the type for the props that the component accepts. It accepts a partial of StyleProps<Parts>, allowing users to override styles using the className, classNames, style and styles props.

In the example that gets scaffolded out for a new component, we also include "children," which accepts a React.ReactNode. This is pretty common, but not every component takes children, and sometimes you might want to be more strict with the type it accepts. For this example, let's add an optional isDisabled prop.

export type MyComponentProps = Partial<StyleProps<Parts>> & {
  children: React.ReactNode;

  /**
   * When true, the component will be styled to indicate that it is not
   * interactive and will be announced as disabled to screen-readers.
   */
  isDisabled?: boolean;
};

Notice that we use JSDoc comments above our types to provide inline documentation in editors that support it and to document the code.

e.g., if we were to use MyComponent, hovering over the isDisabled prop in your editor will show:

(property) isDisabled?: boolean | undefined
When true, the component will be styled to indicate that it is not interactive and will be announced as disabled to screen-readers.

"GetMyComponentStyles" is the type for the styling function. Feel free to add parameters to the function so that your components can be styled based on props the component receives or other states. It's recommended that these parameters are required so that TypeScript can warn if they've been left out.

Let's update GetMyComponentStyles to accept isDisabled as an argument:

export type GetMyComponentStyles = ({
  isDisabled,
}: {
  isDisabled: boolean;
}) => Partial<Record<Parts, string>>;

Now let's look at the component itself, which you can find at packages/core/src/experimental/my-component/my-component.tsx.

'use client';

import { Box } from '../../components/box';
import { getStyleProps } from '../../shared/style-utils';
import { getMyComponentStyles } from './styles';
import { type MyComponentProps } from './types';

/**
 * MyComponent
 *
 * @description
 */

export function MyComponent({
  children,
  className = '',
  classNames = {},
  style = {},
  styles = {},
  ...consumerProps
}: MyComponentProps) {
  const styleProps = getStyleProps({
    className,
    classNames,
    style,
    styles,
    tw: getMyComponentStyles(),
  });
  return (
    <Box {...consumerProps} {...styleProps('root')}>
      {children}
    </Box>
  );
}

We're importing the Box component, some functions used to style our components, as well as the types for the new component.

There is another JSDoc comment above the component ready for you to provide a description.

We use named functions for components, primarily because React DevTools uses the function name to determine the component's name. Using a named function (instead of an anonymous function) means we do not need to set a displayName.

You should also notice a type error when getMyComponentStyles is called. This is because we changed the type for GetMyComponentStyles to accept isDisabled. Lets update the component to fix it:

'use client';

import { Box } from '../../components/box';
import { getStyleProps } from '../../shared/style-utils';
import { getMyComponentStyles } from './styles';
import { type MyComponentProps } from './types';

/**
 * MyComponent
 *
 * @description
 */

export function MyComponent({
  children,
  className = '',
  classNames = {},
+ isDisabled = false,
  style = {},
  styles = {},
  ...consumerProps
}: MyComponentProps) {
  const styleProps = getStyleProps({
    className,
    classNames,
    style,
    styles,
-   tw: getMyComponentStyles(),
+   tw: getMyComponentStyles({ isDisabled }),
  });
  return (
    <Box {...consumerProps} {...styleProps('root')}>
      {children}
    </Box>
  );
}

The getStyleProps function is a helper that merges any styles provided by consumers with styles defined in the design system. It returns a function that accepts the name of the part being styled as its argument and returns an object with className and style properties.

We start by spreading consumer props, typically to the root element. This enables users to add extra properties if needed. However, they won't override the properties we specify afterward, to avoid accidentally breaking things.

We spread the style props at the end to ensure that any attempt to provide another style prop must be done via getStyleProps(). Using className or style will trigger a TypeScript error, indicating that the spread will always overwrite that property to safeguard against potential mistakes.

Along with the types and the component, we also generate a basic test and Storybook story.

The only other thing you're likely to need to change inside of the core package when adding a new component is translations.

Translations

Ideally all text in components should be provided by consumers. In cases where that isn't practical or when providing a default value would improve accessibility (such as for aria labels) we have a translations package with a small number of translations.

The core package exports a Translations type, which is an object type where the keys are the names of the components, and the values are the parts of the component that need translation. For newly added (AKA "experimental") components, the keys should be prefixed with "experimental_" since this type is exported from the stable part of the code.

For example the Button component has a translation for the visually hidden text used when the button is a loading state, so the the types

export type Translations = Readonly<{
  button: Record<'loadingLabel', string>;
  // add new types here e.g.:
  // experimental_myComponent: Record<'customLabel', string>;
}>;

Translations can be found in packages/core/src/shared/types.ts.

The translations themselves can be found inside of packages/translations/src/{{LANGUAGE}}.

We currently only support English translations although there is a placeholder Spanish translation with dummy values.

By updating the types for the translations in core, you should see a type error on the translations object.

export const translations: Translations = {
  button: {
    loadingLabel: 'Busy',
  },
  // add new translations here e.g.: 
  // experimental_myComponent: { customLabel: 'Custom label' },
};

If you need to support other languages, please let the Design System team know and we will do our best to accomodate, but since the translations are simple objects, you could potentially provide your own:

export default function App({ children }: { children: React.ReactNode }) {
  return (
    <Root
      brand={brand}
      colorScheme="system"
      translations={{
        button: {
          loadingLabel: 'Occupé', // French translation
        },
        // add translations for MyComponent e.g.: 
        // experimental_myComponent: { customLabel: 'Étiquette personnalisée' },
      }}
    >
      {children}
    </Root>
  );
}

Brand styles

You may require some styles to be brandable so they can be styled differently between brand. Using different tokens gets you some of the way, but we can customise things further by using BrandStyles. A good example of this is offering a different border radius between brands, for example our Setmore brand has round "pill" shaped buttons, but the "ServiceForge" brand has no border radius at all.

The types for this are in the same file as our translations types (packages/core/src/shared/types.ts).

export type BrandStyles = {
  buttonRadius: keyof GlobalTokens['radius'];
  // add new brandable items here, e.g.
  // labelWeight: keyof GlobalTokens['typography']['fontWeight'];
};

Once a value is added to this type, you can access it from another hook: useBrandStyles, which is available from packages/core/src/shared/context/config.tsx. You can use this value to apply conditional styles to your component.

Playroom snippets

apps/playroom/src/snippets.ts is where we add all our new Playroom snippets. This should be one of the last files you update once you have built the component and can test it as the API will often change while you're developing the component. You can go to Playroom in the deploy previews for the docs once you create a PR, or run it locally by running pnpm dev:playroom from the root.

Getting Your Component into the Design System

Once you're happy with your component, here's how you can get it into the design system:

  1. Create a PR: Make sure your PR includes:
    • A changeset to succinctly describe the updates.
    • A brief description of the component and how to use it.
    • Links to any related Jira tickets or Figma designs, providing context and visual references.
    • If a Storybook story hasn't been created, include screenshots showcasing the component in action.
  2. Review the Deploy Previews: Once the PR is opened, please verify that all deploy previews build successfully, and CI checks are passing. These preliminary checks ensure that the component is ready for review.
  3. Request a Review: Tag a member of the design system team to review your work. Their insights and approval are crucial for maintaining the quality and consistency of the design system.
  4. Await the Merge intomain: After your PR receives the necessary approvals, it will be merged into the main branch. Your component will then become available for use inside of the awds repo.
  5. Monitor the Automatic Processes: Post-merge, a new PR titled "Version Packages" will be automatically created or updated. Once this PR is approved and merged, a GitHub action will take over, creating a new release. The packages with changes will be published to GitHub packages.
  6. Update Your Code: With the new version released, you can now update any code using the design system to make use of the freshly added component.

We value collaboration and insight from all users of the design system. If you have any questions or need support during this process, we're here to assist.