This document is the single source of truth for component prop design, naming conventions, prop ordering, and boolean-prop rules across the Credence Frontend codebase.
By following these conventions, we ensure a predictable developer experience, consistent public APIs, robust accessibility, and clean PR review cycles.
- Every shared component prop interface must be named
[ComponentName]Props(e.g.,ButtonProps,BadgeProps,RepoAvatarProps). - The prop interface must be exported from the component module.
- When a component wraps a native HTML element, extend the appropriate
HTMLAttributes<T>or element-specific attribute interface (e.g.ButtonHTMLAttributes<HTMLButtonElement>).
import { HTMLAttributes } from 'react'
export interface RepoAvatarProps extends HTMLAttributes<HTMLSpanElement> {
src?: string
name?: string
size?: 'sm' | 'md' | 'lg'
alt?: string
className?: string
}- Case: Always use
camelCasefor prop names (iconClassName,srPrefix,autoDismiss). - Event Callbacks: Callback props must start with
onfollowed by an imperative or past-tense action verb (e.g.,onClick,onClose,onToggle,onSelect). - Handler vs Prop Distinction: Use
handle[Action]for internal component handlers (e.g.,handleClick), andon[Action]for the public prop passed by consumers (e.g.,onClick). - Semantic Presets: Use semantic string unions rather than loose string types or magic numbers for visual variants (e.g.
size?: 'sm' | 'md' | 'lg').
- Positive Phrasing: Always phrase boolean props positively. Prefer
isOpen,isLoading,isEnabledover negative names likeisNotClosed,noBorder, orhideIcon. - Auxiliary Verb Prefixes: Custom boolean flags should use prefix verbs (
is,has,should,can,allow):isLoading: Async operation in progress.isConnected: Wallet or network connection status.hasError: Error state flag.shouldRestoreFocus: Focus restoration behavior flag.
- Default False: Optional boolean feature flags must default to
falseso omitting the prop keeps the flag disabled. - Explicit Destructuring Defaults: Always provide default values for optional boolean props in function parameter destructuring:
// Do:
export default function Button({ isLoading = false, fullWidth = false, children }: ButtonProps) {
/* ... */
}
// Don't:
export default function Button(props: ButtonProps) {
const isLoading = props.isLoading ?? false // Avoid manual fallback checks
}Order props consistently in both interface definitions and component parameter destructuring:
- Primary / Required Content Props:
children,value,name - Variants & Presets:
variant,size,severity - State & Feature Flags:
isLoading,disabled,fullWidth - Event Callbacks:
onClick,onClose,onToggle,onChange - Styling & Class Overrides:
className,iconClassName - Accessibility Props:
aria-label,ariaLabel,srPrefix - Rest HTML Attributes:
...props
export interface ConfirmDialogProps {
// 1. Primary content
title: string
message: string
// 2. Variants
variant?: 'danger' | 'warning' | 'info'
// 3. State flags
isOpen?: boolean
isLoading?: boolean
// 4. Callbacks
onConfirm: () => void
onCancel: () => void
// 5. Styling
className?: string
// 6. Accessibility
ariaLabel?: string
}- String Unions over Enums: Prefer TypeScript string union literals (
'sm' | 'md' | 'lg') over TypeScriptenumdeclarations for simpler bundling and serialization. - Centralized Presets: Land reusable size arrays, default fallbacks, and design token maps in
src/config/(e.g.src/config/avatar.tsorsrc/config/navigation.ts) and re-use them in components and tests.
// src/config/avatar.ts
export const REPO_AVATAR_SIZES = {
sm: 'sm',
md: 'md',
lg: 'lg',
} as const
export type RepoAvatarSize = keyof typeof REPO_AVATAR_SIZES
export const DEFAULT_REPO_AVATAR_SIZE: RepoAvatarSize = 'md'import { HTMLAttributes, useState, useEffect } from 'react'
import { REPO_AVATAR_SIZES, RepoAvatarSize, DEFAULT_REPO_AVATAR_SIZE } from '../config/avatar'
import './RepoAvatar.css'
export interface RepoAvatarProps extends HTMLAttributes<HTMLSpanElement> {
/** Repository or organization name */
name?: string
/** Image URL */
src?: string
/** Size preset */
size?: RepoAvatarSize
/** Image alt override */
alt?: string
/** Custom CSS classes */
className?: string
}
export default function RepoAvatar({
name,
src,
size = DEFAULT_REPO_AVATAR_SIZE,
alt,
className = '',
'aria-label': ariaLabel,
...props
}: RepoAvatarProps) {
const [hasError, setHasError] = useState(false)
useEffect(() => {
setHasError(false)
}, [src])
const normalizedSize: RepoAvatarSize =
size && size in REPO_AVATAR_SIZES ? size : DEFAULT_REPO_AVATAR_SIZE
const containerClasses = [
'credence-repo-avatar',
`credence-repo-avatar--${normalizedSize}`,
className,
]
.filter(Boolean)
.join(' ')
const accessibleLabel =
ariaLabel || alt || (name ? `${name} repository avatar` : 'Repository avatar')
return (
<span role="img" aria-label={accessibleLabel} className={containerClasses} {...props}>
{src && !hasError ? (
<img src={src} alt={alt || name} onError={() => setHasError(true)} />
) : (
<span>{name ? name.slice(0, 2).toUpperCase() : 'C'}</span>
)}
</span>
)
}