Skip to Content
DocsGuidelines

Component Guidelines

This section outlines the standards for building consistent, accessible, and maintainable components within the UI Kit — covering structure, styling, testing, and documentation best practices.

Structure

Each component should follow the structure below:

HvComp/ ├── index.ts # Entry file with public exports ├── HvComp.tsx # Component implementation and types ├── HvComp.stories.tsx # Storybook stories and documentation ├── HvComp.styles.ts # Styling utilities and class definitions └── HvComp.test.tsx # Unit tests
  • Components and their types must be exported in index.ts.
  • Use PascalCase for component names and prefix with Hv (e.g., HvButton, HvAccordion).
  • Boolean props should be opt-in — default value should be false.
  • Name event handler props using the on<Action> convention (e.g., onClick, onChange).

Styles

  • Use the createClasses utility to define component classes.
  • Class names generated by createClasses are exposed to users, so choose names thoughtfully:
    • Always apply classes.root to the root element.
    • When targeting non-root elements, make the purpose clear (e.g., classes.label, classes.iconContainer).
    • For conditional styles, use descriptive names that reflect the condition.
  • Avoid hard-coded styles. Always use the design theme (e.g., colors, spacing) for consistency.

Documentation

  • Create a corresponding HvComp.stories.tsx file using CSF (Component Story Format).
  • The primary example should be named Main, with controls configured appropriately.
  • Include only meaningful and distinct stories — avoid repetitive examples.

Other Conventions

Following these guidelines helps maintain a coherent developer experience across the library and ensures consistency, accessibility, and long-term maintainability.

Example

Here’s an example anatomy of a HvComp component:

// HvComp.styles.ts import { createClasses } from "@hitachivantara/uikit-react-utils"; export const { staticClasses, useClasses } = createClasses("HvComp", { /** Applied to the root element */ root: { // leverage the 👇 `theme.` object padding: theme.spacing("xs"), backgroundColor: theme.colors.backgroundColor, }, /** Applied to the root element when selected */ selected: {}, /** Applied to the root element when disabled */ disabled: { // 👇 leverage global `disabled` instead of adding a `buttonDisabled` "& $button": { cursor: "not-allowed", }, }, /** Applied to the button element */ button: {}, });
// HvComp.tsx import { useDefaultProps, type ExtractNames, } from "@hitachivantara/uikit-react-utils"; import { HvBaseProps } from "../types/generic"; import { staticClasses, useClasses } from "./MyComp.styles"; // export `staticClasses` 👇 as `compClasses` (camelCase, no `Hv`) export { staticClasses as compClasses }; // export the classes 👇 inferred from `useClasses` export type HvCompClasses = ExtractNames<typeof useClasses>; // extend the types to where `...others` 👇 is being passed export interface HvCompProps extends HvBaseProps<HTMLDivElement> { // 👇 name boolean props so its obvious they're boolean selected?: boolean; // 👇 add JSDoc to the props, especially if they're complex /** Disables the component visually and its controls */ disabled?: boolean; children?: ReactNode; // Use permissive 👇 `ReactNode` for props that are rendered as-is buttonContent?: ReactNode; // re-use types 👇 when possible onClick?: HvButtonProps["onClick"]; // 👇 call handlers `on<Action>` or `on<Element><Action>` for sub-elements onButtonClick?: HvButtonProps["onClick"]; // 👇 ❌ don't add redundant types (included in the parent interface) id?: string; } // 👇 Add a JSDoc block to the component explaining its purpose /** HvComp does some amazing stuff */ export const HvComp = forwardRef< // no-indent React.ComponentRef<"div">, HvCompProps >(function HvComp(props, ref) { const { children, // fix collisions 👇 by renaming to `<x>Prop` classes: classesProp, className, // 👇 make booleans be opt-in (default is false/undefined) selected, // 👇 don't set defaults (ie `false`) unnecessarily disabled, buttonContent, onClick, onButtonClick, // 👇 call remaining props `others` ...others // use the 👇 useDefaultProps utility } = useDefaultProps("HvComp", props); // use the useClasses utility 👇 const { classes, css, cx } = useClasses(classesProp); // internal state variables. const canSelect = !disabled && !selected; // reserve `useMemo` for expensive 👇 computations const hasElement = useMemo(() => expensiveSearch(children), [children]); // AVOID useEffect... useEffect(() => { // 👇 ❌ ...especially when dealing with event handlers onClick(); }, [onClick]); // rename handlers 👇 (`handle<Action>`) and 👇 type them accordingly const handleButtonClick: HvButtonProps["onClick"] = (evt) => { evt.preventDefault(); // use the ?. 👇 operator for optional event handlers & forward the event onButtonClick?.(evt); }; // keep `render<Stuff>` as a `ReactNode` if possible // if you 👇 need `(props) => ReactNode`, it should likely be a separate component const renderContent = ( <div> <span>{/* some content */}</span> </div> ); return ( <div // 👇 don't forget forwarding the ref ref={ref} // 👇 merge class names with `cx` provided by `useClasses` className={cx( // 👇 pass internal styles first, using `css` css(/* internal styles */), // 👇 ensure `classes.root` is on the root element classes.root, { // conditional classes 👇 are based on their *condition* [classes.selected]: selected, [classes.disabled]: disabled, }, // 👇 pass user-defined `className` last className, )} onClick={disabled ? undefined : onClick} // 👇 forward others according to the types {...others} > <HvButton className={classes.button} onClick={handleButtonClick}> {buttonContent} </HvButton> {selected && renderContent} {children} </div> ); });
Last updated on