API design principles for UI components
User Interface component libraries like Bootstrap and Material UI 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
jQuery-style
Before modern JavaScript UI libraries/frameworks like React, Angular, and Vue came into the picture, jQuery (and jQuery UI) was the most popular way to build UI. jQuery UI popularized the idea of initializing UI components via "constructors" which involved two arguments:
- Root Element: A root DOM element to render the contents.
- 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 (among many other UI components) with a single line of code:
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:
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, you'll see that the API we recommend is similar to jQuery's, the constructor takes in a root element and options:
React
React forces you to write UI as components which contain its own logic and appearance. 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 of a component's options.
Components do not take in a root element. To render the element into the page, a separate API is used.
You will usually not need to call createRoot()
yourself if the entire page a React app because there will only be one createRoot
call for the root/page-level component.
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 all UI components 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
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:
jQuery
In jQuery, classes can also be passed as a field on the options.
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:
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:
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. This is when developers start using hacks like !important
or .my-custom-slider.my-custom-slider
to let their selectors win the specificity war and 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 possible 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 future.
If UI library authors can make these classes/attributes part of their API, which comes with these guarantees:
- The list of selectors is published for external reference.
- 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:
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, a headless UI component library for React, uses element selectors. Each component has a data-reach-*
attribute on the underlying DOM element.
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.
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 and Less. Bootstrap is written with Sass and they provide a way to customize the Sass variables 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 (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.
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, Headless UI, and Reach UI make heavy use of render props.
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 to futureproof your styles and let your layout work for different writing modes.