| name | ui-components |
| description | Core component library and design system patterns. Use when building UI, building new components, new page, using design tokens, or working with the component library. |
Core Components
Component Hierarchy
src/components/
├── ui/ # Design system primitives (Button, Badge, Spinner)
├── shared/ # Cross-feature, business-agnostic components
├── common/ # Common layout components (Modal, TableContainer)
├── layout/ # Layout wrappers (Header, Footer)
└── providers/ # Context providers
Component Location Rules
src/components/ui/ → Pure UI primitives (Button, Badge, Table)
src/components/shared/ → Cross-feature components (TokenIcon, AccountIdentity)
src/features/{name}/ → Feature-specific components
src/modals/ → Global modals (multi-trigger)
Modal
Import: @/components/common/Modal
Always use our custom Modal components. The shared wrapper applies Monarch typography, corner radius, background, blur, and z-index rules automatically.
Modal Systems
| System | Use Case | How It Works |
|---|
Local useState | Simple, single-trigger modals | Component controls own visibility |
useModalStore | Multi-trigger, nested, or chained modals | Zustand store, ModalRenderer in layout |
GlobalModalContext | Imperative generic modals | Call openModal(<Content />), renders in provider |
GlobalTransactionModals | Transaction progress | Reactive to useTransactionProcessStore, auto-renders |
Decision:
- Settings, confirmations, forms →
useModalStore or local state
- Transaction progress → Use
useTransactionTracking, modal handled automatically
- Dynamic content injection →
GlobalModalContext
import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/Modal';
import { Button } from '@/components/ui/button';
Standard Modal (settings, workflows)
<Modal
isOpen={isOpen}
onClose={onClose}
variant="standard"
zIndex="settings"
size="xl"
>
<ModalHeader
title="Modal Title"
description="Brief description of what this modal does"
/>
<ModalBody>
{/* Modal content */}
</ModalBody>
<ModalFooter>
<Button variant="secondary" onClick={onClose}>Cancel</Button>
<Button variant="cta" onClick={handleConfirm}>Confirm</Button>
</ModalFooter>
</Modal>
Compact Modal (filters, confirmations)
<Modal
isOpen={isOpen}
onClose={onClose}
variant="compact"
zIndex="base"
size="md"
>
<ModalHeader variant="compact" title="Filter Options" />
<ModalBody variant="compact">
{/* Tighter spacing applied automatically */}
</ModalBody>
<ModalFooter>
<Button variant="secondary" size="sm" onClick={onClose}>Cancel</Button>
<Button variant="cta" size="sm" onClick={handleApply}>Apply</Button>
</ModalFooter>
</Modal>
With Icon
<ModalHeader
title={`Supply ${symbol}`}
description="Supply to earn interest"
icon={<TokenIcon address={tokenAddress} chainId={chainId} symbol={symbol} width={20} height={20} />}
/>
Z-Index Layers
zIndex="base"
zIndex="process"
zIndex="selection"
zIndex="settings"
Modal State Patterns
Pattern 1: Local State (Default)
const [showModal, setShowModal] = useState(false);
{showModal && <MyModal isOpen={showModal} onClose={() => setShowModal(false)} />}
Pattern 2: Global State (Zustand)
const { open } = useModalStore();
<Button onClick={() => open('supply', { market, position })}>Supply</Button>
Use Pattern 2 when: multi-trigger (2+ places), props drilling pain, modal chaining, or nested modals.
⚠️ Nested modals must use Pattern 2. Radix-UI crashes with "Maximum update depth exceeded" when multiple Dialogs are mounted simultaneously (#3675). The modal store avoids this by only mounting visible modals.
SectionTag
Import: @/components/landing
A minimal label component for section headers with a technical, bracketed style.
import { SectionTag } from '@/components/landing';
<SectionTag>Account Access</SectionTag>
<SectionTag className="mb-4">Market Analysis</SectionTag>
Use cases:
- Landing page section labels
Button
Import: @/components/ui/button
import { Button } from '@/components/ui/button';
<Button variant="primary" size="md">New Position</Button>
<Button variant="surface" size="sm">Claim</Button>
<Button variant="default" size="md">Back to Markets</Button>
<Button variant="ghost" size="icon"><RefreshIcon /></Button>
Variants: primary | surface | default | ghost
Sizes: xs | sm | md | lg | icon
When wrapping Button in Tooltip, wrap it in a <span> to prevent ResizeObserver errors.
RefetchIcon
Import: @/components/ui/refetch-icon
Spinning reload icon with smooth animation completion. When isLoading becomes false, the icon completes its current rotation before stopping. Use isLoading with data refetching states as well.
import { RefetchIcon } from '@/components/ui/refetch-icon';
<Button variant="ghost" size="sm" onClick={onRefresh} disabled={isRefetching}>
<RefetchIcon isLoading={isRefetching} />
</Button>
<RefetchIcon isLoading={isLoading} className="h-4 w-4" />
Table
Import: @/components/ui/table
Never use raw HTML <table> tags.
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table';
import { TablePagination } from '@/components/common/TablePagination';
<Table>
<TableHeader>
<TableRow>
<TableHead className="text-left">Name</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody className="table-body-compact">
<TableRow>
<TableCell>John</TableCell>
<TableCell className="text-right">$100</TableCell>
</TableRow>
</TableBody>
</Table>
<TablePagination
currentPage={page}
totalPages={totalPages}
totalEntries={items.length}
pageSize={10}
onPageChange={setPage}
isLoading={loading}
/>
Rules:
- Use
table-body-compact on <TableBody> for activity tables
- Headers and cells must match alignment (
text-left, text-right, text-center)
- Amount columns: use flexbox with
justify-end
TableContainerWithHeader
Import: @/components/common/table-container-with-header
import { TableContainerWithHeader } from '@/components/common/table-container-with-header';
<TableContainerWithHeader title="Asset Activity">
<Table>{/* content */}</Table>
</TableContainerWithHeader>
<TableContainerWithHeader
title="Supplied Positions"
actions={
<>
<Button variant="ghost" size="sm" onClick={handleRefresh}>
<RefreshIcon className="h-3 w-3" />
</Button>
<Button variant="ghost" size="sm">
<SettingsIcon className="h-3 w-3" />
</Button>
</>
}
>
<Table>{/* content */}</Table>
</TableContainerWithHeader>
Props: title, actions?, children, className?
TablePagination
Import: @/components/common/TablePagination
import { TablePagination } from '@/components/common/TablePagination';
<TablePagination
currentPage={currentPage}
totalPages={totalPages}
totalEntries={data.length}
pageSize={pageSize}
onPageChange={setCurrentPage}
isLoading={isFetching}
showEntryCount={true} // Optional, default true
/>
Props: currentPage, totalPages, totalEntries, pageSize, onPageChange, isLoading?, showEntryCount?
Tooltip
Import: @/components/ui/tooltip
import { Tooltip } from '@/components/ui/tooltip';
<Tooltip content="Tooltip text">
<Button>Hover me</Button>
</Tooltip>
<Tooltip content="Top tooltip" placement="top" delay={500}>
<span>Hover</span>
</Tooltip>
Structured Content
import { TooltipContent } from '@/components/shared/tooltip-content';
<Tooltip
content={
<TooltipContent
icon={<Icon />}
title="Title"
detail="Detail text"
secondaryDetail="Secondary detail"
/>
}
>
<Button>Hover</Button>
</Tooltip>
Props: content, placement (top | bottom | left | right), delay (ms)
TooltipContent Props: icon, title, detail, secondaryDetail, actionIcon, actionHref
Switch
Import: @/components/ui/icon-switch
import { IconSwitch } from '@/components/ui/icon-switch';
<IconSwitch
size="xs"
selected={isEnabled}
onChange={setIsEnabled}
thumbIcon={null}
/>
<IconSwitch
size="sm"
selected={viewMode === 'user'}
onChange={toggleView}
thumbIcon={ViewIcon}
/>
Sizes: xs | sm | md | lg
Checkbox
Import: @/components/ui/checkbox
import { Checkbox } from '@/components/ui/checkbox';
<Checkbox
checked={isChecked}
onCheckedChange={setIsChecked}
label="Accept terms"
/>
<Checkbox
variant="highlighted"
checked={isSelected}
onCheckedChange={setIsSelected}
label="Enable advanced features"
/>
Variants: default | highlighted
Dropdown Menu
Import: @/components/ui/dropdown-menu
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuCheckboxItem,
} from '@/components/ui/dropdown-menu';
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>Options</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleAction}>Action</DropdownMenuItem>
<DropdownMenuItem startContent={<Icon />}>With Icon</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem checked={isChecked} onCheckedChange={setIsChecked}>
Toggle Item
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
Alignment: align="start" | "center" | "end"
Input
Numerical Inputs (Amounts)
import Input from '@/components/Input/Input';
<Input
decimals={18}
setValue={setAmount}
max={maxBalance}
setError={setError}
value={currentAmount}
onMaxClick={handleMaxClick}
/>
Text Inputs
<input
type="text"
placeholder="Enter text..."
value={inputValue}
onChange={(e) => handleChange(e.target.value)}
className="bg-hovered h-10 w-full rounded p-2 focus:border-primary focus:outline-none"
/>
Error States
className={`bg-hovered h-10 w-full rounded p-2 focus:border-primary focus:outline-none ${
error ? 'border border-red-500 focus:border-red-500' : ''
}`}
{error && <p className="text-sm text-red-500">{error}</p>}
AccountIdentity
Import: @/components/common/AccountIdentity
import { AccountIdentity } from '@/components/common/AccountIdentity';
<AccountIdentity address={address} variant="badge" />
<AccountIdentity address={address} variant="compact" chainId={1} />
<AccountIdentity address={address} variant="full" showAddress />
Variants: badge | compact | full
MarketIdentity
Import: @/components/MarketIdentity
import { MarketIdentity, MarketIdentityMode, MarketIdentityFocus } from '@/components/MarketIdentity';
<MarketIdentity market={market} chainId={chainId} mode={MarketIdentityMode.Normal} />
<MarketIdentity
market={market}
chainId={chainId}
mode={MarketIdentityMode.Focused}
focus={MarketIdentityFocus.Collateral}
/>
<MarketIdentity
market={market}
chainId={chainId}
mode={MarketIdentityMode.Minimum}
focus={MarketIdentityFocus.Loan}
showLltv
showOracle
/>
Modes: Normal | Focused | Minimum
MarketDetailsBlock
Import: @/components/common/MarketDetailsBlock
import { MarketDetailsBlock } from '@/components/common/MarketDetailsBlock';
<MarketDetailsBlock
market={market}
mode="supply"
defaultCollapsed
showRewards
/>
TransactionIdentity
Import: @/components/common/TransactionIdentity
import { TransactionIdentity } from '@/components/common/TransactionIdentity';
<TransactionIdentity txHash={txHash} chainId={chainId} />
<TransactionIdentity txHash={txHash} chainId={chainId} showFullHash />
CollateralIconsDisplay
Import: @/features/positions/components/collateral-icons-display
import { CollateralIconsDisplay } from '@/features/positions/components/collateral-icons-display';
<CollateralIconsDisplay
collaterals={position.collaterals}
chainId={position.chainId}
maxDisplay={8}
iconSize={20}
/>
Props: collaterals, chainId, maxDisplay? (default 8), iconSize? (default 20)
Toast
Import: @/hooks/useStyledToast
import { useStyledToast } from '@/hooks/useStyledToast';
const { success, error } = useStyledToast();
success('Success', 'Detail of the success');
error('Error', 'Detail of the error');
Styling Guidelines
Rounding
rounded - Modals, Cards, Tables, large containers
rounded-sm - Buttons, Inputs, Tooltips, Tags
Background
bg-surface - First layer components
bg-hovered - Hoverable elements or components on bg-surface
Typography
- Avoid bold weights for emphasis
- Use
text-primary / text-secondary for hierarchy
- Prefer
font-zen for vault UI surfaces