// Build React components in the Pond house style: compound component objects, CSS Modules, variants via data-attributes, props that extend native HTML elements. Use when creating, refactoring, or reviewing any component under apps/desktop/src/renderer/src/components/, ui/, or pages/. Triggers on /components or when the user asks to "make a component", "refactor this to match Settings", "follow the component convention", or asks how components should be structured in this codebase.
Build React components in the Pond house style: compound component objects, CSS Modules, variants via data-attributes, props that extend native HTML elements. Use when creating, refactoring, or reviewing any component under apps/desktop/src/renderer/src/components/, ui/, or pages/. Triggers on /components or when the user asks to "make a component", "refactor this to match Settings", "follow the component convention", or asks how components should be structured in this codebase.
/components: component patterns in the Pond house style
Quick start
The reference implementation lives in apps/desktop/src/renderer/src/components/settings/. Read it first. Every new component in this codebase should follow the same shape:
One folder per component: <name>/index.tsx + <name>/styles.module.css.
Export a single PascalCase object whose keys are the sub-components (Settings.Page, Settings.Header, Settings.Item, ...).
Every sub-component is a thin function whose props extend a native HTML element via React.ComponentPropsWithoutRef<"tag">.
Variants are data-* attributes, never boolean props. CSS targets them with &[data-x="y"].
Co-located CSS Modules. No inline styles. No styling props (className may still be passed in via ...props).
Use semantic HTML. <header> for headers, <h1>–<h3> by hierarchy, <p> for body, <div> only for layout primitives.
If a new component matches these six rules, it's correct. The rest of this file is the reasoning, the exact patterns to follow, and the things to avoid.
A consumer composes these like LEGO. No options, no variant props, no as props, no nested config objects.
<Settings.Page>
<Settings.Header>
<Settings.Title>Notifications</Settings.Title>
<Settings.Description>
Choose which background events surface as a toast.
</Settings.Description>
</Settings.Header>
Rules
1. One folder, two files
components/<name>/
index.tsx
styles.module.css
No types.ts, no <name>.tsx, no Component.tsx. The folder name is the import name.
If a component grows past ~250 lines, split sub-components into sibling files (item.tsx, header.tsx) and re-export them from index.tsx. Keep the public surface a single object.
2. Compound component object
Always export a single PascalCase object. Never export the sub-components individually.
Good:
exportconstSettings = {
Page,
Header,
Title,
};
Bad:
export { Page, Header, Title };
This keeps the namespace obvious at the call site (<Settings.Page> reads like a sentence) and lets you rename internals without churning every consumer.
3. Props extend the underlying HTML element
Every sub-component declares a props interface that extends the exact element it renders.
Even when the interface is empty, declare it. It documents which element this component is, and lets you add props later without changing the call signature.
Use React.ComponentPropsWithoutRef<"tag">, not HTMLAttributes<HTMLDivElement>. The former handles ref, key, and event types correctly for the specific tag.
4. Variants are data-* attributes, not boolean props
When a component has visual variants, expose them as a single string-union prop and forward it as a data-* attribute. Style with attribute selectors.
This lets a consumer pass id, aria-*, data-testid, event handlers, and even className (which will replace yours; accept that and merge with clsx only when necessary).
6. Semantic HTML by role
Pick the tag that matches the role, not the layout.
Role
Tag
Page heading
<h1>
Section heading
<h2>
Item heading
<h3>
Body / description
<p>
Header band
<header>
Generic layout box
<div>
Interactive control
<button> / <input> (from ui/)
The Settings file uses h1/h2/h3/p/header deliberately. Mirror this in any component that has the same hierarchy. Don't reach for <div> because "it's just text".
7. CSS Modules, co-located, kebab-case classes
One styles.module.css per component, in the same folder.
Import as import styles from "./styles.module.css";.
Class names are kebab-case (section-title, item-description).
Access via styles.foo for single-word names, styles["section-title"] for multi-word.
Don't use Tailwind, styled-components, cva, inline style={{ ... }}, or clsx on internal classes. The few style={{ ... }} blocks you'll find in pages (e.g. preferences.tsx) are page-level glue, not component primitives. Don't propagate them into new components.
8. CSS custom properties for tokens, locals on the root
Use design-system tokens for every color, never raw hex. Define component-local variables on the root class so callers can override them from the outside without writing component-specific CSS.
Locals live at the top of the root class, in declaration order. Token reads (var(--ds-gray-12)) live wherever they're needed.
Tokens: always use --ds-*
All theme tokens live in packages/ui/src/theme.css (re-exported as @pond/ui/theme.css). There is one system: --ds-*. Don't reach for ad-hoc colours, hex literals, or page-local hand-rolled scales.
Color scales. Backed by Radix Themes: --ds-gray-1…--ds-gray-12 plus alpha (--ds-gray-a1…--ds-gray-a12), and --ds-accent-1…--ds-accent-12 (sky-based) plus alpha. Same scale, same semantics in light and dark.
Semantic.--ds-tomato-{1..12,a3,a6} for danger / required / error. --ds-grass-{3,6,9,11,a6} for success.
Surface.--ds-background-primary for the app body. Use --ds-gray-1 for default surface, --ds-gray-2 for subtle panels.
Mono font.var(--ds-font-mono) for <code>, <kbd>, and any identifier-leaning string.
Brand.--ds-brand-{twitter, cosmos, arena, facebook, pinterest, dribbble} for source-badge backgrounds. Stay inside the badge; never escape into surrounding chrome.
Radix scale cheat-sheet for picking the right step:
The destructure form is the same line count, types correctly, and shows the default in the signature.
10. No JSDoc on primitives, prose where intent matters
Sub-components like Settings.Item don't need JSDoc; the name and the props say it all. Reserve doc comments for non-obvious behavior, like the notifications section explaining how toast categories map to the useToast wrapper.
/**
* Notifications section. Each switch maps to a `category` tag the
* shared `useToast()` wrapper checks before rendering. See
* `apps/desktop/src/renderer/src/ui/toast.tsx`. Untagged toasts
* (system errors, IPC failures) always show.
*/
Don't comment what the JSX already says. Do comment why a piece of state or an effect exists.
Component skeleton
Use this as the starting point for a new compound component. Replace the name and the sub-components.
These are the things to refuse in code review or rewrite when refactoring an older component.
Boolean prop walls.compact, large, dense, bordered, noPadding. Collapse them into one variant/size/densitydata-* attribute or split the component.
as props. If a consumer needs a different element, give them a different sub-component (Settings.Title vs Settings.SectionTitle), not polymorphism.
Default exports. Always named, always the compound object.
Single-component files exporting many primitives. Wrap them in the object.
Inline style={{ ... }} inside the component. Allowed only as page-level glue; never inside components/.
Tailwind classes, cva, tw\...``, or any CSS-in-JS. Use CSS Modules.
Raw hex colors. Always var(--ds-...) tokens.
--pond-* tokens. Removed. Everything is --ds-* now.
className overrides as the primary customization API. Variants first, ...props (which includes className) as escape hatch.
Wrapping a primitive in a <div> "just to add padding". Add a local --*-padding variable on the root and use it.
Hardcoded widths or breakpoints inside a sub-component. Use a data-* variant on the root and let the root drive layout via CSS variables (see Settings.Page's --settings-max-width).
Naming
Surface
Convention
Example
Folder
kebab-case
header-toolbar/
Compound object
PascalCase, singular
HeaderToolbar, Settings, Card
Sub-component
PascalCase
Page, ItemDetails
Props interface
<Component>Props
ItemDetailsProps
CSS class
kebab-case
item-description
Local CSS variable
--<component>-<name>
--settings-padding-inline-inset
Variant value
lower-case word
narrow, muted, compact
Checklist
Before you commit a new component, walk this list:
One folder, index.tsx + styles.module.css.
Single named export of a PascalCase compound object.
Every sub-component has a <Name>Props interface that extends React.ComponentPropsWithoutRef<"tag">.
Every sub-component spreads ...props onto its element.
No boolean variants. Use data-* attributes with a string union.
Every variant is reflected as a data-* attribute on the rendered element.
Semantic tags chosen by role (<header>, <h1>–<h3>, <p>, <button>).
No inline styles, no Tailwind, no CSS-in-JS.
All colors, radii, shadows, and brand surfaces via var(--ds-...) tokens. No raw hex. No --pond-* tokens.
Component-local CSS variables declared on the root class, used inside children.
No JSDoc on primitives; comments only where intent isn't obvious.
Reads like the Settings reference at the call site.
When NOT to apply this skill
The compound-component pattern is the default for presentational primitives under components/ and ui/. It is not the default for:
Page components under pages/. Pages are top-level, single-purpose, and may freely use hooks, effects, and inline style={{ ... }} for one-off glue.
Hooks and pool modules under pool/. They follow normal hook conventions.
Main-process and preload code under apps/desktop/src/main/ and apps/desktop/src/preload/. They have nothing to do with this skill.
Single-element wrappers that genuinely have no sub-parts (e.g. a one-off Spinner). A single named function export is fine; you do not need to invent sub-components to satisfy the pattern.