Compound Component Pattern
- Build custom components using the compound component pattern (like Radix UI).
- APIs should be declarative, composable, and expose clearly named parts.
- Favor composition via
children
over bundling multiple elements into one.
Semantic Markup
- Use semantic HTML elements (
table
,tr
,dl
,ul
,li
, etc.) for structure. - Do not use
div
s where a semantic element is appropriate (e.g., don’t usediv
for tables or lists).
Props
- Do not pass objects or arrays as props unless instructed.
- Keep props flat and simple; use primitive values.
// Bad
<Dialog triggerProps={{ className: 'my-class', hasTooltip: true, tooltipContent: 'permissions' }}>
<Permissions items={permissions} />
</Dialog>
// Bad
<Dialog triggerClassName="my-class" triggerHasTooltip triggerTooltipContent="permissions">
<Permissions items={permissions} />
</Dialog>
// Good
<Dialog.Root>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Dialog.Trigger className="my-class" />
</Tooltip.Trigger>
<Tooltip.Content>permissions</Tooltip.Content>
</Tooltip.Root>
<Dialog.Content>
<Permissions.Root>
{permissions.map(permission => (
<Permissions.Item key={permission.id}>{permission.name}</Permissions.Item>
))}
</Permissions.Root>
</Dialog.Content>
</Dialog.Root>
Improve the composition of the "Good" example only when reused, e.g this composes the above internally:
<PermissionsDialog.Root>
<PermissionsDialog.Trigger />
<PermissionsDialog.Content>
<Permissions.Root>
{permissions.map(permission => (
<Permissions.Item key={permission.id}>{permission.name}</Permissions.Item>
))}
</Permissions.Root>
</PermissionsDialog.Content>
</PermissionsDialog.Root>
Context
- Keep context private to the component.
- If global, expose a compound
Provider
part (e.g.,TooltipProvider
). - Do not expose
useContext()
or internal context APIs to consumers.
Minimal, Event-Driven APIs
- Minimize boolean flags.
- Prefer event handlers with
preventDefault
anddetail
for data. - Mimic native HTML behavior: components act unless prevented.
// Bad
<Collapsible.Root isCollapsible={!shouldNotOpen} />
// Good
<Collapsible.Root
onOpenChange={event => {
if (shouldNotOpen) event.preventDefault();
// if we need open state
const open = event.detail.open;
}}
/>
- Keep the owner tree flat.
- Write large JSX components and only abstract when necessary (e.g., for reuse).
- Avoid premature component extraction.
- Use Tailwind CSS for all styling by default.
- No styled-components, CSS modules, or global CSS unless specified.
- Each compound component part should render one element.
- Only render more when it simplifies the consumer API considerably.
- When access is required, move elements into their own compound parts for direct forwarding of props/attrs—avoid prop drilling from a parent component when possible.
- All parts must forward
ref
, native attributes (id
,aria-*
), and styling props (className
,style
, etc.). - Never restrict prop spreading.
Component Part Naming & Exporting
- All parts must be defined in a single file, unless parts need to be separated between client and server.
- Use individual named exports, renamed for namespace imports.
- Keep imports at the top of files after any "use client" or "use server" directives.
- If a component has only one part, export it directly (no renaming).
'use client';
import * as React from 'react';
/* -------------------------------------------------------------------------------------------------
* Tooltip
* -----------------------------------------------------------------------------------------------*/
interface TooltipProps {}
function Tooltip(prop: TooltipProps) {}
/* -------------------------------------------------------------------------------------------------
* TooltipTrigger
* -----------------------------------------------------------------------------------------------*/
interface TooltipTriggerProps {}
function TooltipTrigger(props: TooltipTriggerProps) {}
/* -------------------------------------------------------------------------------------------------
* TooltipContent
* -----------------------------------------------------------------------------------------------*/
interface TooltipContentProps {}
function TooltipContent(props: TooltipContentProps) {}
/* ---------------------------------------------------------------------------------------------- */
export {
Tooltip as Root,
TooltipTrigger as Trigger,
TooltipContent as Content,
}
File Formatting & Snippets
- Use
comb
andcomd
snippet comment blocks for structure. - Use kebab-case for filenames (e.g.,
dropdown-menu.tsx
). - Main component or export at the top; supporting functions/components below.
- Import third-party libraries first.
- Avoid optional props where possible; validate and narrow optional data before rendering the component.
- Do not allow optional props to leak into component logic.
- Do not pass
...data
objects to components. Be explicit about each prop. - Prefer explicit property access over destructuring, except for props to avoid forwarding invalid props.
// Good
const data = await getPageData()
return (
<PageContent
title={data.title}
description={data.description}
/>
)
// Bad
return <PageContent {...data} />
- Use
interface
for component props. - Use
type
only for unions or utility types.
interface ButtonProps extends React.ComponentProps<'button'> {}
- Fetch all data as high as possible in the React server tree.
- Only nest queries if there's a clear performance or architectural reason.
- Do not add "use client" to all components with client features. Only add it to parent components that compose client comps and are rendered in an RSC.
- Do not add "use client" to an entire compound component and all of its parts if only one part needs client features—abstract the part into its own file.
- Avoid Suspense by default; use only for real performance bottlenecks.
Summary: These rules ensure UI code is predictable, declarative, flat, explicit, semantic, and simple to debug.
By following these conventions, the codebase remains easy to change, refactor, or delete—enabling smooth transitions when making architectural decisions (such as switching databases or frameworks) and reducing technical debt over time.