Every v9 component package follows this exact layout:
packages/react-components/react-<name>/library/src/
├── components/<Name>/
│ ├── <Name>.tsx # ForwardRefComponent
│ ├── <Name>.types.ts # Props, State, Slots types
│ ├── <Name>.test.tsx # Unit tests (adjacent)
│ ├── use<Name>.ts or .tsx # State management hook
│ ├── use<Name>Styles.styles.ts # Griffel styling
│ ├── render<Name>.tsx # JSX rendering
│ └── index.ts # Component barrel export
├── contexts/ # Optional: context definitions
├── utils/ # Optional: shared utilities
├── testing/
│ └── isConformant.ts # Conformance tests
├── <Name>.ts # Root barrel per component
└── index.ts # Package export
Components use three core hooks:
-
use<Name>(props, ref)— Processes props and slots into normalized state. Use.tsif pure logic,.tsxif the hook body contains JSX. -
use<Name>Styles(state)— Creates Griffel CSS-in-JS styling using design tokens. Always ends in.styles.ts. -
render<Name>(state)— Pure JSX rendering from state. Always.tsx.
| Bug type | Fix location |
|---|---|
| State / behavior | use<Name>.ts |
| Styling | use<Name>Styles.styles.ts |
| Rendering / JSX | render<Name>.tsx |
| Types / props | <Name>.types.ts |
All v9 components use slots for extensibility:
// Types
type ButtonSlots = {
root: Slot<'button'>;
icon?: Slot<'span'>;
};
// Hook — create slots
const state: ButtonState = {
root: slot.always(props.root, { elementType: 'button' }),
icon: slot.optional(props.icon, { elementType: 'span' }),
};
// Render — use assertSlots for type safety
export const renderButton_unstable = (state: ButtonState) => {
assertSlots<ButtonSlots>(state);
return (
<state.root>
{state.icon && <state.icon />}
{state.root.children}
</state.root>
);
};Use makeStyles with design tokens — never hardcode values:
import { makeStyles } from '@griffel/react';
import { tokens } from '@fluentui/react-theme';
export const useButtonStyles = makeStyles({
root: {
color: tokens.colorNeutralForeground1,
backgroundColor: tokens.colorNeutralBackground1,
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
':hover': {
backgroundColor: tokens.colorNeutralBackground1Hover,
},
},
});Always use mergeClasses() and preserve user className as the last argument:
state.root.className = mergeClasses(
classes.root,
props.size === 'small' && classes.small,
state.root.className, // Always last
);// Component.types.ts
export type ComponentProps = ComponentPropsWithRef<'div'> & {
appearance?: 'primary' | 'secondary';
size?: 'small' | 'medium' | 'large';
};
export type ComponentState = Required<Pick<ComponentProps, 'appearance' | 'size'>> & {
components: ComponentSlots;
root: SlotProps<'div'>;
};
// Main component — always ForwardRefComponent, never React.FC
export const Component: ForwardRefComponent<ComponentProps> = React.forwardRef((props, ref) => {
const state = useComponent_unstable(props, ref);
useComponentStyles_unstable(state);
return renderComponent_unstable(state);
});