Build production-grade UI for OpenBB Workspace - a professional financial data platform. Every interface must be functional, visually refined, and feel intentionally designed for power users working with financial data.
Design Philosophy
Context: OpenBB Workspace
- Audience: Quants, analysts, traders, financial professionals
- Purpose: Data visualization, analysis, decision-making
- Tone: Professional yet distinctive. Premium feel. Data-dense but not cluttered.
- Aesthetic: Dark-first, information-rich, precise typography, purposeful whitespace
Before Designing, Consider:
- Purpose: What financial workflow does this serve? What decisions does it enable?
- Data density: Financial users want information, not empty space. Balance density with clarity.
- Hierarchy: In data-heavy interfaces, visual hierarchy is critical. What's the primary insight?
- Consistency: OpenBB has an established design system. Innovate within it, don't fight it.
Design Principles for Financial Interfaces
- Every pixel earns its place - Remove decoration that doesn't aid comprehension
- Data is the hero - UI chrome should recede, data should pop
- Precision over personality - Clean lines, exact spacing, no sloppiness
- Dark mode is default - Easier on eyes during long analysis sessions
- Performance is UX - Financial users notice lag. Optimize render paths.
AVOID Generic AI Aesthetics
- Hardcoded hex colors — use semantic tokens or Tailwind primitives
- Random font choices — use
var(--font-family)or system typography classes - Cookie-cutter card layouts with excessive padding
- Animations that slow down data consumption
- "Friendly" rounded everything — financial tools should feel sharp and precise
- Inconsistent spacing — use the Tailwind scale
Before You Build - MANDATORY
- Search
src/components/ds/first - design system components - Search
src/components/ui/second - additional UI primitives - Only create new if nothing similar exists
# Check for existing components
ls src/components/ds/atoms/
ls src/components/ds/molecules/
ls src/components/ds/dialogs/
ls src/components/ui/
Available Components
Design System (~/components/ds/)
Atoms:
Button- primary actions, variants: primary, secondary, outlined, ghost, danger, warningInput- text input with label supportSelect- dropdown selectionEnhancedSelect- rich select with icons, subtitles, tooltipsCheckbox- boolean toggle with labelSwitch- on/off toggleRadioGroup- single selection from optionsLabel- form labelsTextArea- multiline text inputAvatar- user/entity avatarsTag- labels/badges (colors: grey, success, warning, danger, brand, ruby, purple, yellow, dark-blue, burgundy)DropdownMenu- contextual menusPopover- floating contentCopyButton- copy to clipboardHintLabel- helper text/hintsFontFamilyInput- font selectionSpacingInput- spacing value input
Molecules:
Form- form wrapper with validationTabs- tabbed navigation (variants: default, filled, filled_secondary)ColorPicker- color selectionSettingsMenu- collapsible settings panelExportProgress- export status indicatorExpandableSection- collapsible content section
Dialogs:
Dialog- base modalBaseDialog- reusable dialog patternConfirmDialog- confirmation prompts
UI Primitives (~/components/ui/)
Calendar- date display/selectionDatePicker- date inputSingleDatePicker- single date selectionDrawer- slide-out panelSlider- range inputScrollArea- custom scrollbarProgress- progress indicatorResizable- resizable panelsContextMenu- right-click menuEdgeHoverZone- edge interaction zonesinput-otp- OTP input
Color System — Two-Layer Architecture
The color system has two layers defined across src/components/ds/colors.ts (primitives) and src/styles/tokens.css (semantic tokens). Tailwind exposes both via tailwind.config.ts.
Layer 0: Primitive Colors (static, theme-independent)
Primitives are raw color values. They do NOT change between light/dark mode. Use them when you need a fixed color regardless of theme (e.g., forced-light contexts like login pages).
base-0 (#FFFFFF), base-100 (#0C0C0E)
main-50 (#33BBFF), main-100 (#0088CC), main-200 (#006699)
dark-50..dark-900 (dark neutrals)
light-50..light-900 (light neutrals)
error-50, error-100, error-200
success-50, success-100, success-200
warning-50, warning-100, warning-200
informative-50, informative-100, informative-200
extra-{grey,pink,turquoise,burgundy,coral,yellow,orange,red,purple,green,blue}-{50,100,200}
Tailwind usage: bg-light-100, text-dark-500, border-error-100, text-main-100
Layer 1: Semantic Tokens (theme-aware, auto-switch light/dark)
Semantic tokens resolve to different primitives based on the active theme. Use these by default — they handle dark mode automatically, eliminating the need for dark: prefixes.
Defined in src/styles/tokens.css, exposed as Tailwind classes in tailwind.config.ts.
General / Structure
bg-general-bg-primary — main background (white / dark-900)
bg-general-bg-primary-hover — hover state
bg-general-bg-primary-disabled
bg-general-bg-secondary — secondary bg (light-100 / dark-800)
bg-general-bg-secondary-hover
bg-general-bg-secondary-disabled
border-general-border-primary — primary borders
border-general-border-secondary — subtle borders
border-general-border-disabled
text-general-label — primary label text
text-general-label-hover — hover label text
text-general-label-disabled — disabled label text
Surface
bg-surface-page — page background (light-50 / base-100)
bg-surface-header — header areas
bg-surface-divider — dividers/separators (use for hr, border dividers)
bg-surface-layer — overlay/backdrop (semi-transparent)
bg-surface-card — card backgrounds
Text
text-ds-text-heading — headings, primary text (darkest)
text-ds-text-subtitle — subtitles, secondary emphasis
text-ds-text-body — body text, icons
text-ds-text-caption — captions, placeholders, labels (lightest)
Buttons
bg-btn-primary-bg, hover, disabled / text-btn-primary-label, disabled
bg-btn-secondary-bg, hover, disabled / border-btn-secondary-border
border-btn-outlined-border, hover, disabled / text-btn-outlined-label, disabled
bg-btn-ghost-bg-hover / text-btn-ghost-label, disabled
bg-btn-destructive-bg, hover, disabled / text-btn-destructive-label, disabled
bg-btn-warning-bg, hover, disabled / text-btn-warning-label, disabled
Alerts
text-alert-success, text-alert-warning, text-alert-error, text-alert-informative
Links
text-link-color — themed link color (main-100 light / main-50 dark)
Inputs
bg-input-field-bg, bg-input-field-bg-hover, bg-input-field-bg-disabled
Dropdowns
bg-dropdown-bg, border-dropdown-border
Tabs
bg-tab-bg-primary, bg-tab-bg-secondary, bg-tab-group-bg
border-tab-border, text-tab-action-active
Tags
bg-tag-{color}-bg / text-tag-{color}-label
Colors: pink, blue, green, red, yellow, orange, coral, burgundy, grey, turquoise, purple
Tables
bg-table-header-bg, bg-table-cell-bg, bg-table-cell-bg-hover
Other
bg-tooltip-bg, bg-toggle-bg, bg-toggle-bg-disabled
bg-scrollbar-track, bg-scrollbar-handle, bg-scrollbar-handle-hover
Brand Variables (white-label theming)
--brand-main /* Primary accent — backward compat, prefer semantic tokens */
--brand-lighter /* Lighter variant */
--brand-darker /* Darker variant */
Tailwind: text-brand-main, bg-brand-lighter
Runtime override: src/utils/colorUtils.ts updates --color-main-* and --brand-* vars.
Styling Rules
Semantic Tokens First, dark: Rarely Needed
Default approach — use semantic tokens. They handle light/dark automatically:
// GOOD: semantic tokens — no dark: prefix needed
<div className="bg-general-bg-primary text-general-label border-general-border-primary" />
<p className="text-ds-text-body" />
<button className="bg-btn-primary-bg text-btn-primary-label hover:bg-btn-primary-bg-hover" />
<hr className="border-surface-divider" />
Only use dark: when the component needs different structural behavior per theme (rare), or when using primitives in a theme-aware context:
// OK: SpecialTooltip uses primitives because it's a non-DS component
<div className="bg-light-50 dark:bg-dark-500" />
Forced-Light Contexts (login, register, onboarding)
Some pages render always-light (white card on dark bg). Semantic tokens break here because they resolve to dark-mode values when root is dark. Use static primitive classes instead:
// BAD: semantic tokens change with theme — invisible text in dark mode
className="!text-ds-text-heading !border-general-border-secondary"
// GOOD: primitives are always the same color
className="!text-light-850 !border-light-100 !placeholder-light-500"
Primitive equivalents of common light-mode semantic tokens:
| Semantic token | Light-mode primitive |
|---|---|
general-border-secondary |
light-100 |
ds-text-heading |
light-850 |
ds-text-subtitle |
light-750 |
ds-text-body |
light-600 |
ds-text-caption |
light-500 |
general-bg-primary-hover |
light-100 |
Class Names
import { cn } from "~/components/ds/utils";
// Always use cn() for conditional classes
<div className={cn(
"base-classes",
{ "conditional-class": condition }
)} />
Typography Classes
body-xs-regular, body-xs-medium, body-xs-semibold
body-sm-regular, body-sm-medium, body-sm-semibold
body-base-regular, body-base-medium, body-base-semibold
body-lg-regular, body-lg-medium, body-lg-semibold
title-xs, title-sm, title-base, title-lg, title-xl
subtitle-xs, subtitle-sm, subtitle-base
No Inline Styles
Use Tailwind only. Never use style={{}}.
Existing CSS Utilities (from index.css)
Prefer these established patterns over creating new ones:
obb-btn, obb-btn-blue, obb-btn-secondary, obb-btn-tertiary, obb-btn-ghost, obb-btn-outlined
obb-icon-btn, obb-icon-btn-v2, obb-small-navbar-btn
obb-dropdown-container, obb-dropdown-item
obb-navigation-item, obb-navigation-item-active
obb-modal, obb-modal-overlay, obb-modal-close, obb-modal-title
obb-label, obb-hyper-link, obb-divider, obb-divider-vertical
obb-card, obb-code, obb-floating-actions
obb-minimal-input, obb-small-input, obb-new-input
obb-parameter, obb-tag, obb-page-container
Icons
import Icon from "~/components/Icon";
<Icon id="icon-name" className="w-4 h-4" />
- Default size:
w-4 h-4 - Check
Icon.types.tsfor available IDs - Use
classNameto override size - Icons use
currentColorfor theming
Adding New Icons
When user pastes SVG content and wants to add a new icon, Claude handles everything automatically:
-
Check for duplicates:
- Read
src/components/Icon.types.tsand check if icon ID already exists - Search for similar names (e.g., if adding "settings-02", check for "settings", "settings-01")
- If duplicate/similar exists, ask user if they want to proceed or use existing
- Read
-
Optimize with SVGO:
# Save pasted SVG to temp file, optimize, output to stdout echo '<svg>...</svg>' | bunx svgo --input - --output - --multipass -
Convert to symbol format:
- Remove
<?xml>declaration and<!DOCTYPE>if present - Remove
xmlnsandxmlns:xlinkattributes (sprite already has them) - Replace
<svg ...>with<symbol id="icon-id" ...> - Replace
</svg>with</symbol> - Keep
viewBoxattribute (addviewBox="0 0 24 24"if missing) - Remove
widthandheightattributes - Replace hardcoded colors with
currentColorfor theming
- Remove
-
Insert into sprite (at the BEGINNING):
- Edit
public/assets/icons/sprite.svg - Insert the
<symbol>right after<defs>tag (beginning of icons)
- Edit
-
Regenerate types:
bun run update-icons -
Confirm to user:
// New icon available: <Icon id="new-icon-id" />
Example transformation:
<!-- Input SVG (pasted by user) -->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path stroke="#000" d="M12 2L2 7l10 5"/>
</svg>
<!-- Output symbol (after SVGO + conversion) -->
<symbol id="my-icon" viewBox="0 0 24 24" fill="none">
<path stroke="currentColor" d="M12 2L2 7l10 5"/>
</symbol>
Motion & Interaction
- Restrained animations - Financial users prioritize speed over delight
- Functional motion only - Loading states, transitions between views, feedback
- No gratuitous effects - Skip page load animations, hover wobbles, etc.
- Instant feedback - Buttons, inputs should respond immediately
- Use CSS transitions - Prefer
transitionover JS animations for performance
// Good: subtle, functional
className="transition-colors duration-150"
// Avoid: slow, distracting
className="animate-bounce duration-1000"
Data Display Patterns
- Tables: Use proper alignment (numbers right-aligned, text left-aligned)
- Numbers: Monospace font, consistent decimal places, thousand separators
- Charts: Let TradingView/charting library handle chart rendering
- Loading: Skeleton loaders that match data shape, not spinners
- Empty states: Actionable, not just "No data"
Component Patterns
// Always use forwardRef for ref forwarding
const MyComponent = forwardRef<HTMLDivElement, MyComponentProps>((props, ref) => {
// ...
});
// Use CVA for variant management
import { cva, type VariantProps } from "class-variance-authority";
const buttonVariants = cva("base-classes", {
variants: {
variant: { default: "...", destructive: "..." },
size: { default: "...", sm: "...", lg: "..." },
},
defaultVariants: { variant: "default", size: "default" },
});
// Use Radix primitives as base
import * as DialogPrimitive from "@radix-ui/react-dialog";
// Always use ~ imports
import { Button } from "~/components/ds/atoms/Button";
Quick Audit Checklist
When reviewing UI:
- Hierarchy clear? - Can user identify primary/secondary actions?
- Spacing consistent? - Using Tailwind spacing scale?
- Typography appropriate? - Using defined typography classes?
- Colors correct? - Using semantic tokens (not hardcoded hex)?
- Dark mode works? - Semantic tokens handle it, or
dark:for primitives? - Forced-light contexts? - Using primitives, not semantic tokens?
- Loading states? - Handling async operations?
- Empty states? - What shows when no data?
- Error states? - How are errors displayed?
Import Convention
// ALWAYS use ~ imports
import { Button } from "~/components/ds/atoms/Button";
import { Dialog } from "~/components/ds/dialogs/Dialog";
import Icon from "~/components/Icon";
import { cn } from "~/components/ds/utils";
// NEVER use relative imports
import { Button } from "../../../components/ds/atoms/Button"; // BAD
Key Files Reference
src/components/ds/- design systemsrc/components/ds/colors.ts- primitive color palettesrc/styles/tokens.css- semantic tokens (light/dark definitions)tailwind.config.ts- Tailwind color config (exposes both layers)src/utils/colorUtils.ts- runtime brand color applicationsrc/components/ds/utils/cn.ts- class utilitysrc/components/Icon.tsx- icon componentsrc/components/Icon.types.ts- icon type definitionspublic/assets/icons/sprite.svg- icon sprite