All colors are defined in tailwind.config.js under colors.vibe:
| Name | Hex | Usage |
|---|---|---|
backgroundDark |
#0b0f14 |
Dark mode page backgrounds |
backgroundLight |
#EBE5D6 |
Light mode page backgrounds (warm cream, never pure white) |
cinnabarRed |
#E54D42 |
Leaderboard accent, destructive actions, errors, "downvote" states |
carrotOrange |
#F08D33 |
Default section headings (Latest Apps, Top Apps, etc.), labels |
saffronYellow |
#F5C34D |
Primary CTAs, bold text in dark mode, light theme toggle |
persianGreen |
#44A8A5 |
Community accent, links, focus states, input borders, success |
powderBlue |
#91D8DB |
Links in dark mode, dark theme toggle, secondary accents |
charcoalSlate |
#384955 |
Borders, subtle backgrounds, bold text in light mode |
retroPurple |
#8B5FC5 |
My Apps / Create & Organize accent - rich retro purple |
Orange is the default. Use it for any UI element not in a specific feature context.
| Context | Color | Variable | When to Use |
|---|---|---|---|
| Default / Generic | Carrot Orange | vibe-carrotOrange |
Homepage sections, Latest Apps, Top Apps, generic headings |
| My Apps / Profile | Retro Purple | vibe-retroPurple |
Profile page, app cards on profile, Create & Organize |
| Community / Explore | Persian Green | vibe-persianGreen |
Community page, app cards in explore, public browsing |
| Leaderboard / Voting | Cinnabar Red | vibe-cinnabarRed |
Leaderboard page, vote-focused UI, rankings |
When building a page or component, determine the context and apply its color to:
- Section headings:
text-vibe-[color] - Card borders:
border-vibe-[color]/30(normal),border-vibe-[color]/60(hover) - Card shadows:
shadow-vibe-[color]/10(normal),shadow-vibe-[color]/20(hover) - Card gradients:
from-vibe-[color]/15in light,from-vibe-[color]/10in dark - Buttons:
bg-vibe-[color] text-white - Navigation active states: Uses the feature's color
-
Readability first. When there's space, use it for comfortable reading. Never sacrifice legibility for compactness.
-
Use the space. On larger screens, scale UP - bigger fonts, more padding, wider content areas. Don't force content into narrow columns when there's room to breathe.
-
Proportional scaling. As screens get larger, everything should grow proportionally - fonts, spacing, containers. Don't cap content width too aggressively.
Mobile (< 640px): Comfortable minimums, efficient use of limited space
Tablet (640-1024px): Slightly larger, more breathing room
Desktop (1024-1280px): Generous sizing, comfortable reading
Large (1280px+): Even larger fonts, more padding, use the space
Ultra-wide (1920px+): Maximum comfort, sidebars appear, content stays generous
Key Rule: When a sidebar appears, the main content should still feel spacious - don't shrink fonts just to fit. Instead, let the layout breathe.
| Element | Minimum | Preferred | Max on Desktop |
|---|---|---|---|
| Body text | text-sm (14px) |
text-base (16px) |
text-lg (18px) |
| Card descriptions | text-sm (14px) |
text-base (16px) |
text-base |
| Labels/captions | text-xs (12px) |
text-sm (14px) |
text-sm |
| Headings (h3) | text-lg (18px) |
text-xl (20px) |
text-2xl |
| Section headings (h2) | text-2xl (24px) |
text-3xl (30px) |
text-4xl |
- Never go below
text-xs(12px) for any readable text - Body text should be
text-base(16px) or larger when space allows - On wide screens, scale UP not down - use the extra space for comfort
- Mobile text should match desktop - don't shrink text for mobile, shrink containers instead
- Line height matters - use
leading-relaxedfor paragraphs
// DO: Keep text readable at all sizes
className="text-base md:text-lg" // Starts at 16px, goes to 18px
// DON'T: Shrink text for mobile
className="text-xs sm:text-sm md:text-base" // Too small on mobile ❌
// DO: Comfortable card descriptions
className="text-gray-600 dark:text-white/80 leading-relaxed"
// DON'T: Cramped sidebar text
className="text-[10px]" // Never use arbitrary tiny sizes ❌Even in sidebars and compact layouts:
- App names: minimum
text-sm(14px), prefertext-base - Descriptions: minimum
text-sm, can truncate withline-clamp-2 - Metadata (votes, dates):
text-smis acceptable - When space is tight: truncate content, don't shrink font
Every component and page MUST be tested in both modes. Use these patterns:
// Text colors - ALWAYS include dark variant
className="text-gray-900 dark:text-white" // Primary text
className="text-gray-600 dark:text-gray-400" // Secondary text
className="text-gray-500 dark:text-gray-500" // Muted text
// Backgrounds - NEVER use pure white in light mode
className="bg-vibe-backgroundLight dark:bg-vibe-backgroundDark" // Page backgrounds
className="bg-white/60 dark:bg-gray-900/60" // Card/input backgrounds
// Borders
className="border-vibe-charcoalSlate/20 dark:border-gray-700" // Subtle borders
className="border-vibe-carrotOrange/50 dark:border-gray-600" // Hover states- System UI headings (Overview, Tags, Status, Notes, etc.):
text-vibe-carrotOrange - Page titles:
text-gray-900 dark:text-white - Form labels:
text-vibe-carrotOrange(uppercase, tracking-wide) - Markdown H1:
text-vibe-cinnabarRed dark:text-vibe-saffronYellow - Markdown H2:
text-vibe-carrotOrange(same both modes) - Markdown H3:
text-vibe-persianGreen dark:text-vibe-powderBlue
Bold text (**text** in markdown) should stand out:
- Light mode:
text-vibe-charcoalSlate(dark slate) - Dark mode:
text-vibe-saffronYellow(warm yellow)
className="[&_strong]:text-vibe-charcoalSlate dark:[&_strong]:text-vibe-saffronYellow"// Standard links
className="text-vibe-persianGreen dark:text-vibe-powderBlue hover:underline"
// Navigation active state
className="text-vibe-carrotOrange"// Primary CTA (yellow, high visibility)
className="bg-vibe-saffronYellow text-vibe-backgroundDark hover:bg-vibe-carrotOrange"
// Secondary/outline
className="border border-vibe-charcoalSlate/30 text-gray-900 dark:text-white hover:bg-vibe-carrotOrange/10"
// Destructive
className="bg-vibe-cinnabarRed/15 text-vibe-cinnabarRed border border-vibe-cinnabarRed/30"const inputClasses = `
w-full px-4 py-3 rounded-xl
border-2 border-vibe-charcoalSlate/20 dark:border-gray-700
bg-white/60 dark:bg-gray-900/60
text-gray-900 dark:text-white
placeholder-gray-400 dark:placeholder-gray-500
focus:border-vibe-persianGreen focus:bg-white dark:focus:bg-gray-900
outline-none transition-all
`// Error container
className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 rounded-xl"
// Error text
className="text-red-500 dark:text-red-400"Cards should be styled based on their context color. The pattern:
// Feature card with context color (replace [color] with: retroPurple, persianGreen, cinnabarRed, or carrotOrange)
<div className="
group block overflow-hidden rounded-2xl
border border-vibe-[color]/30 hover:border-vibe-[color]/60
bg-gradient-to-b from-vibe-[color]/15 via-vibe-backgroundLight to-vibe-backgroundLight
dark:from-vibe-[color]/10 dark:via-vibe-backgroundDark dark:to-vibe-backgroundDark
shadow-lg shadow-vibe-[color]/10 hover:shadow-xl hover:shadow-vibe-[color]/20
transition-all duration-300 hover:-translate-y-1
">
{/* Card headline in context color */}
<h3 className="text-xl font-semibold text-vibe-[color]">Title</h3>
{/* Body text - always readable */}
<p className="text-gray-900 dark:text-white/80">Description</p>
{/* Button in context color */}
<button className="bg-vibe-[color] text-white hover:bg-vibe-[color]/80">
Action
</button>
</div>| Page/Section | Color Variable | Border | Shadow | Heading |
|---|---|---|---|---|
| Homepage (Latest Apps) | carrotOrange |
/30 → /60 |
/10 → /20 |
Orange |
| Profile / My Apps | retroPurple |
/30 → /60 |
/10 → /20 |
Purple |
| Community / Explore | persianGreen |
/30 → /60 |
/10 → /20 |
Green |
| Leaderboard | cinnabarRed |
/30 → /60 |
/10 → /20 |
Red |
Before submitting any UI changes, verify:
- Light mode readable: All text has sufficient contrast against
backgroundLight - Dark mode readable: All text has sufficient contrast against
backgroundDark - No pure white backgrounds: Use
bg-vibe-backgroundLightor semi-transparent whites - Inputs styled for both modes: Check placeholder, text, border, and focus states
- Errors visible in both modes: Red tones should work on both backgrounds
- Links distinguishable: Persian green (light) / Powder blue (dark)
- Headings use palette colors: Carrot orange for system labels
- Navigation visibility: Check fixed nav against various page backgrounds
Use consistent icons across the application. The canonical icon set is defined in src/components/Navigation.tsx:
| Feature | Icon | Description |
|---|---|---|
| My Apps / Create & Organize | Boxes/storage icon | Multiple stacked boxes |
| Community | People group icon | Group of 3 people |
| Leaderboard | Bar chart icon | Ascending bars |
| User | Person silhouette | Single person |
| Visit App | External link | Arrow pointing out of box |
| Star | Star outline/filled | 5-point star |
| Vote up | Arrow up | Upward pointing arrow |
| Vote down (remove) | Arrow down | Downward pointing arrow |
| Public/visible | Eye open | Eye with pupil |
| Private/hidden | Eye closed | Eye with slash |
Rules:
- Always use
stroke="currentColor"for icons (not fill) unless intentionally filled - Use
strokeWidth={2}as default - Size icons consistently:
h-4 w-4for buttons,h-5 w-5for navigation - When adding icons to buttons, match size and styling of existing buttons
CRITICAL RULE: Never use the HTML title attribute for tooltips. Always use custom CSS tooltips for a consistent, styled experience.
- Native tooltips have inconsistent styling across browsers
- They appear with delay and cannot be styled
- They don't work on touch devices
- They break the visual consistency of the app
All interactive elements that need hover hints must use this pattern:
// Container with group class
<div className="relative group">
{/* Interactive element - NO title attribute */}
<button className="..." aria-label="Description for accessibility">
<Icon />
<span className="hidden sm:inline">Visible Label</span>
</button>
{/* Custom tooltip - appears on hover */}
<span className="absolute z-50 -bottom-9 left-1/2 -translate-x-1/2 px-2 py-1 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
Tooltip text
{/* Arrow pointing up */}
<span className="absolute -top-1 left-1/2 -translate-x-1/2 w-2 h-2 bg-gray-900 dark:bg-gray-700 rotate-45" />
</span>
</div>| Position | Classes | Use Case |
|---|---|---|
| Below (default) | -bottom-9 left-1/2 -translate-x-1/2 + arrow -top-1 |
Navigation buttons, icon-only buttons |
| Above | -top-8 left-1/2 -translate-x-1/2 + arrow at bottom |
Buttons near bottom of viewport |
| Left-aligned | -bottom-9 left-0 |
Buttons near right edge |
| Right-aligned | -bottom-9 right-0 |
Buttons near left edge |
When multiple tooltips exist in the same container, use named groups:
<div className="flex gap-2">
<div className="relative group/vote">
<button>Vote</button>
<span className="... opacity-0 group-hover/vote:opacity-100">Upvote</span>
</div>
<div className="relative group/star">
<button>Star</button>
<span className="... opacity-0 group-hover/star:opacity-100">Add to favorites</span>
</div>
</div>For navigation items (buttons, links with icons):
- Tooltip appears below the element
- Include arrow pointing up to the button
- Use
aria-labelfor screen readers (instead oftitle) - Hide tooltip on mobile if label is visible
// NavLink with custom tooltip
function NavLink({ to, icon, label, tooltip }) {
return (
<div className="relative group">
<Link to={to} aria-label={tooltip || label}>
<span>{icon}</span>
<span className="hidden sm:inline">{label}</span>
</Link>
{/* Only show tooltip when label is hidden (mobile) */}
<span className="absolute z-50 -bottom-9 left-1/2 -translate-x-1/2 px-2 py-1 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 sm:group-hover:opacity-0 transition-opacity pointer-events-none">
{label}
<span className="absolute -top-1 left-1/2 -translate-x-1/2 w-2 h-2 bg-gray-900 dark:bg-gray-700 rotate-45" />
</span>
</div>
)
}For action buttons in cards:
<div className="relative group/[unique-name]">
<button
className="flex items-center gap-1 px-2 py-1 rounded-md transition-all text-gray-500 hover:text-[accent-color] hover:bg-[accent-color]/10"
aria-label="Action description"
>
<svg className="h-3.5 w-3.5" ... />
<span className="font-medium">{count}</span>
</button>
<span className="absolute -top-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded opacity-0 group-hover/[unique-name]:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-50 shadow-lg">
Tooltip text
</span>
</div>- Background:
bg-gray-900 dark:bg-gray-700 - Text:
text-white text-xs - Padding:
px-2 py-1 - Border radius:
rounded - Z-index:
z-50 - Transition:
opacity-0 group-hover:opacity-100 transition-opacity - Arrow:
w-2 h-2 rotate-45matching background color - Pointer events:
pointer-events-none(prevents tooltip from blocking clicks) - Parent containers: Must have
overflow-visible(notoverflow-hidden)
- Color definitions:
tailwind.config.js - Theme context:
src/contexts/ThemeContext.tsx - Theme toggle:
src/components/ThemeToggle.tsx - Icon definitions:
src/components/Navigation.tsx