---
title: API design principles for UI components
description: Best practices for designing developer interface components APIs, useful for UI components coding and system design interviews
seo_title: API Design Principles | Front End Interview Playbook
seo_description: Best practices for designing developer interface components APIs, useful for UI components coding and system design interviews
social_title: API design principles for UI components | Front End Interview Playbook
---

User Interface component libraries like [Bootstrap](https://getbootstrap.com/) and [Material UI](https://mui.com/) help developers build UI faster by providing commonly used components like buttons, tabs, modals, etc so that developers do not have to reinvent the wheel by building these components from scratch whenever they start on a new project.

Often during front end interviews, you will be asked to build UI components and design an API to initialize them. Designing good component APIs is bread and butter for Front End Engineers. This page covers some of the top tips and best practices for designing UI component APIs. Some of these tips may be framework-specific but can be generalized for other component-based UI frameworks.

## Initialization

There are multiple ways to initialize a UI component:

### jQuery-style

Before modern JavaScript UI libraries/frameworks like [React](https://react.dev/), [Angular](https://angular.io/), and [Vue](https://vuejs.org/) came into the picture, [jQuery](https://jquery.com/) (and [jQuery UI](https://jqueryui.com/)) was the most popular way to build UI. jQuery UI popularized the idea of initializing UI components via "constructors" which involved two arguments:

1. **Root element**: A root DOM element to render the contents
1. **Customization options**: Optional, additional, customization options usually in the form of a plain JavaScript object

Using jQuery UI, one can turn a DOM element into a [slider](https://api.jqueryui.com/slider/) (among many other UI components) with a single line of code:

```html
<div id="gfe-slider"></div>
<script>
  $('#gfe-slider').slider();
</script>
```

**jQuery refresher**: jQuery UI's `slider()` method (constructor) takes in a JavaScript object which serves as customization options. Doing `$('#slider')` selects the `<div id="slider">` element and returns a jQuery object that contains convenient methods to "do something" with the element such as `addClass`, `removeClass`, etc and other DOM manipulation methods. Within jQuery methods, the selected element can be accessed via the `this` keyword. jQuery APIs are built around this "select an element and do something with it" approach, hence the `slider()` method does not need an argument for the root DOM element.

The slider can be customized by passing in a plain JavaScript object of options:

```html
<div id="gfe-slider"></div>
<script>
  $('#gfe-slider').slider({
    animate: true,
    max: 50,
    min: 10,
    // See other options here: https://api.jqueryui.com/slider/
  });
</script>
```

### Vanilla JavaScript style

There's no vanilla JavaScript style for initializing components since vanilla JavaScript is not a standard or framework. But if you have read enough of GreatFrontEnd's solutions for our vanilla JavaScript [UI coding questions](/questions/formats/ui-coding), you'll see that the API we recommend is similar to jQuery's, the constructor takes in a root element and options:

```js
function slider(rootEl, options) {
  // Do something with rootEl and options.
}
```

### React (Or similar component-based libraries)

React forces you to write UI as components where the logic and structure is contained within. React components are JavaScript functions that return markup, a description of how to render itself.

React components can take in `props`, which are essentially customization options for a component.

```js
function Slider({ min, max }) {
  // Use the props to render a customized component.
  return <div>...</div>;
}

<Slider max={50} min={10} />;
```

Components do not take in a root element. To render the element into the page, a separate API is used.

```jsx
import { createRoot } from 'react-dom/client';
import Slider from './Slider';

const domNode = document.getElementById('#gfe-slider');
// React will manage the DOM within this element.
const root = createRoot(domNode);
// Display the Slider component within the element.
root.render(<Slider max={50} min={10} />);
```

You will usually not need to call `createRoot()` yourself if the entire page is a React app because there will only be one `createRoot()` call at the root/page-level.

## Customizing appearance

Even though UI components in UI libraries provide default styling, developers will usually want to customize them with their company/product's branding and theme colors.

Hence most UI components (especially those built as 3rd-party libraries) will allow for customization of the appearance, via a few methods:

### Class injection

The idea here is simple, components accept a prop/option to allow the developer to provide their own classes and these classes are added to the actual DOM elements. This approach is not very robust because if the component also adds its own styling via classes, there could be conflicting properties within the component's classes and developer-provided classes.

#### React

```jsx
import clsx from 'clsx';

function Slider({ className, value }) {
  return (
    <div className={clsx('gfe-slider', className)}>
      <input type="range" value={value} />
    </div>
  );
}

<Slider className="my-custom-slider" value={50} />;
```

```css
/* UI library default stylesheet */
.gfe-slider {
  height: 12px;
}
```

```css
/* Developer's custom stylesheet */
.my-custom-slider {
  color: red;
}
```

Through class injection, developers can change the text `color` of the component to be `red`.

If there are many DOM elements within the component to be targeted and one single `className` prop is not sufficient, you can also have multiple differently-named props for `className`s of different elements:

```jsx
import { useId } from 'react';
import clsx from 'clsx';

function Slider({ label, value, className, classNameLabel, classNameTrack }) {
  const id = useId();
  return (
    <div className={clsx('gfe-slider', className)}>
      <label className={clsx('gfe-slider-label', classNameLabel)} for={id}>
        {label}
      </label>
      <input
        className={clsx('gfe-slider-range', classNameTrack)}
        id={id}
        type="range"
        value={value}
      />
    </div>
  );
}
```

#### jQuery

In jQuery, classes can also be passed as a field on the options.

```js
$('#gfe-slider').slider({
  // In reality, jQuery UI takes in a `classes` field instead
  // since there are multiple elements.
  class: 'my-custom-slider',
});
```

In reality, all of jQuery UI's component initializers take in the `classes` field to allow adding additional classes to individual elements. The following example is taken from [jQuery UI Slider](https://api.jqueryui.com/slider/#option-classes):

```js
$('#gfe-slider').slider({
  classes: {
    'ui-slider': 'highlight',
    'ui-slider-handle': 'ui-corner-all',
    'ui-slider-range': 'ui-corner-all ui-widget-header',
  },
});
```

#### Downside: Non-deterministic styling

Class injection has an unobvious downside — the final visual result is non-deterministic and may not be what is expected. Take the following code for example:

```jsx
import clsx from 'clsx';

function Slider({ className, value }) {
  return (
    <div className={clsx('gfe-slider', className)}>
      <input type="range" value={value} />
    </div>
  );
}

<Slider className="my-custom-slider" value={50} />;
```

```css
/* UI library default stylesheet */
.gfe-slider {
  height: 12px;
  color: black;
}
```

```css
/* Developer's custom stylesheet */
.my-custom-slider {
  color: red; /* .gfe-slider also defines a value for color. */
}
```

In the example above, both `.gfe-slider` and `.my-custom-slider` classes specify the `color` and because these two selectors have the same specificity, the winning style is actually the class that appears later on the HTML page.

If the loading order of the stylesheet is not guaranteed (e.g. if stylesheets are lazily loaded), the visual result will not be deterministic. The results is that developers start using hacks like `!important` or `.my-custom-slider.my-custom-slider` to let their selectors win the specificity war. With all these hacks, the CSS code starts becoming unmaintainable.

In jQuery UI, if a custom class is added, the existing default value is not used. This removes the "winning style" ambiguity but the user must now reimplement all the necessary styles present in the original class. This approach can also be applied to React components to resolve the ambiguity.

Despite its flaws, class injection is still a very popular option.

### CSS selector hooks

Technically speaking, developers can achieve customization if they read the source code of the component and define their custom styling by using the same classes. However, doing this is dangerous as relying on a component's internals and there's no guarantee that the class names won't change in the future.

If UI library authors can make these classes/attributes part of their API, which comes with these guarantees:

1. The list of selectors is published for external reference
1. Existing published selectors will not be changed. If they are changed, it will be a breaking change and a version bump is needed as per semver

Then it's an acceptable practice and developers can "hook" onto them (target them) by using these selectors in their stylesheets.

An example of hooking into a component's selectors:

```jsx
import { useId } from 'react';
import clsx from 'clsx';

function Slider({ label, value }) {
  const id = useId();
  return (
    <div className="gfe-slider">
      <label className="gfe-slider-label" for={id}>
        {label}
      </label>
      <input className="gfe-slider-range" id={id} type="range" value={value} />
    </div>
  );
}
```

```css
/* UI library default stylesheet */
.gfe-slider {
  font-size: 12px;
}

/* No other classes are defined in this stylesheet,
gfe-slider-label and gfe-slider-range are added
to the component just for developers to gain access
to the underlying elements. */
```

```css
/* Developer's custom stylesheet */
.gfe-slider {
  font-size: 16px; /* Conflicts with the default .gfe-slider */
  padding: 10px 20px;
}

.gfe-slider-label {
  color: red;
}

.gfe-slider-range {
  height: 20px;
}
```

This approach saves developers the hassle of passing in classes into the component as they only have to write CSS to customize the styling. [Reach UI](https://reach.tech/styling), a headless UI component library for React, uses element selectors. Each component has a `data-reach-*` attribute on the underlying DOM element.

```css
[data-reach-menu-item] {
  color: blue;
}
```

However, this approach still suffers from the non-deterministic styling issue as per "class injection" and doesn't easily allow per-instance styling. If per-instance styling is desired, this approach can be combined with the class injection approach.

### Theme object

Instead of taking in classes, the component takes in an object of key/values for styling. This is useful if there is only a strict subset of properties to customize, or if you want to restrict styling to only a few properties.

```jsx
const defaultTheme = { color: 'black', height: 12 };

function Slider({ value, label, theme }) {
  // Combine with default.
  const mergedTheme = { ...defaultTheme, ...theme };

  return (
    <div className="gfe-slider">
      <label
        for={id}
        style={{
          color: mergedTheme.color,
        }}>
        {label}
      </label>
      <input
        id={id}
        type="range"
        value={value}
        style={{
          height: mergedTheme.height,
        }}
      />
    </div>
  );
}

<Slider theme={{ color: 'red', height: 24 }} {...props} />;
```

However, since no classes with conflicting styles are used, and inline styles have higher specificity than classes, there's no specificity conflict and the inline styles will win. However, the number of options that need to be supported can grow really quickly. Inline styles are also present in the DOM per component instance, which can be bad for performance if this component is rendered hundreds/thousands of times within a page.

The theme object is just a way to restrict the styling to certain properties and optionally, an accepted set of values, the values do not need be used as inline styles and instead can be combined with other styling approaches.

### CSS preprocessor compilation

UI libraries are usually written with CSS preprocessors like [Sass](https://sass-lang.com/) and [Less](https://lesscss.org/). [Bootstrap](https://getbootstrap.com/) is written with Sass and they provide a way to [customize the Sass variables](https://getbootstrap.com/docs/5.3/customize/sass/) used so that developers can generate a custom UI library stylesheet.

This approach is great because it doesn't rely on overriding CSS selectors to achieve customization. There's also less amount of resulting CSS and no redundant overridden styles. The downside is that a compilation step is needed.

### CSS variables / custom properties

[CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) (or more formally known as CSS custom properties) are entities defined by CSS authors that contain specific values to be reused throughout a document. The `var()` function, it accepts fallback values if the given variable is not set.

```jsx
function Slider({ value, label }) {
  return (
    <div className="gfe-slider">
      <label for={id}>{label}</label>
      <input id={id} type="range" value={value} />
    </div>
  );
}
```

```css
/* UI library default stylesheet */
.gfe-slider {
  /* Fallback of 12px if not set. */
  font-size: var(--gfe-slider-font-size, 12px);
}
```

```css
/* Developer's custom stylesheet */
:root {
  --gfe-slider-font-size: 15px;
}
```

The developer can define a value for `--gfe-slider-font-size` globally via the `:root` selector and set the font size for the `.gfe-slider` class to be 15px. The benefit of this approach is that it doesn't require JavaScript, however, per-component customization will be more troublesome (but still possible).

### Render props

In React, render props are function props that a component uses to know what to render. It is useful for separating behavior from presentation. Many behavioral/headless UI libraries like [Radix](https://www.radix-ui.com/), [Headless UI](https://headlessui.com/), and [Reach UI](https://reach.tech/menu-button) make heavy use of render props.

{/* TODO: Section on manipulation of component after initialization */}

## Internationalization (i18n)

Does your UI work for multiple languages? How easy is it to add support for more languages?

### Avoid hardcoding of labels in a certain language

Some UI components have label strings within them (e.g. image carousel has labels for prev/next buttons). It'd be good to allow customization of these label strings by making them part of component props/options.

### Right-to-left languages

Some languages (e.g. Arabic, Hebrew) are read from right-to-left and the UI has to be flipped horizontally. The component can take in a `direction` prop/option and change the order of how elements are rendered. For example, the prev and next buttons will be on the right and left respectively in an RTL language.

Use [CSS logical properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties) to futureproof your styles and let your layout work for different [writing modes](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Writing_Modes).

{/* TODO: Give examples of how to implement RTL. */}
