Conventions
The rules that keep 200 components looking like one person wrote them.
Exports & components
- Named exports only. No default exports for components.
memoon every UI component, with an explicitdisplayName— anonymous memo wrappers make React DevTools unreadable.forwardRefon anything wrapping a DOM element (button, input, …).useCallbackfor handlers passed to memoized children — otherwise the memo does nothing.- Module-level constants for static config (
NAV_TABS,NUMBER_FORMAT_OPTIONS), out of the component body for referential stability.
const PairRowInner = forwardRef<HTMLButtonElement, PairRowProps>(({ market, ...props }, ref) => (
<button ref={ref} data-active={props.active} {...props} />
));
export const PairRow = memo(PairRowInner);
PairRow.displayName = 'PairRow';Styling
- Tokens live in
src/index.cssunder@theme. Reference via Tailwind utilities or CSS vars — never hardcode a hex. - Component visuals live in CSS (
@layer components); React components are thin wrappers that apply the class and forward props. - State modifiers go on
data-*attributes, never conditional class strings.
// ✅ state is data, CSS reacts to it
<button data-active={isActive} className="pill-tab" />
// ❌ state smeared into a class string
<button className={`pill-tab ${isActive ? 'pill-tab-active' : ''}`} />The data-* rule is not stylistic. It means a designer can restyle the active state by editing
one CSS block, without opening a .tsx file — and it makes the DOM self-describing in tests
([data-active="true"]).
cn()(src/lib/cn.ts) joins class names. Noclsx, noclassnamesdependency.
Numbers
- Always
tabular-nums. Prices, sizes, PnL, percentages. - Mono for data — live prices, addresses, the order book, timestamps.
- Green = long / gain, red = short / loss. Never inverted, in any locale.
- Financial values respect
hideValues(masked as•••) so a screen can be shared without leaking position size.
PostHog session replay masks .tabular-nums, .font-mono, and [data-ph-mask]. Rendering a
balance without one of those classes leaks it into replay recordings.
Comments
Only when the why is non-obvious. Never restate the code.
// ❌ increments the nonce
nonce += 1;
// ✅ the engine rejects a reused nonce even for an identical payload, so
// this must increment before the retry, not after the response.
nonce += 1;i18n
All user-facing strings go through useT(). Nine languages live in src/i18n/
(en · fr · es · ko · zh · ru · th · vi · hi).
const t = useT();
<span>{t('order.form.submit')}</span>A hardcoded English string is a bug, not a shortcut — the topbar, order form, and wallet modal are all fully translated, so a raw string stands out immediately in a non-English locale.