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.
- Always apply
- 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
- Write unit tests using
@testing-library/react
. - Use semantic HTML elements whenever possible for better accessibility and performance.
- Ensure components meet WCAG requirements and follow ARIA guidelines.
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>
);
});