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
├── stories/src/<Name>/
│ ├── <Name>Accessibility.md # Optional: concise accessibility guidance
│ └── <Name>AccessibilitySpec.mdx # Optional: full component accessibility spec
├── <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);
});Refer to <Name>Accessibility.md or <Name>BestPractices.md for high-importance accessibility requirements.
Check <Name>AccessibilitySpec.mdx for the full description of accessibility behaviors, including keyboard interaction, assistive tech behaviors, high contrast and reduced motion styles, content restrictions, and guidance on extending the control without breaking accessibility.
Most packages only have one <Name>AccessibilitySpec.mdx covering all components in the package -- for example, MenuAccessibilitySpec.mdx covers Menu, MenuList, MenuItem, MenuItemCheckbox, MenuItemLink, MenuItemRadio, MenuItemSwitch, etc.