Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.heygaia.io/llms.txt

Use this file to discover all available pages before exploring further.

Source of truth is DESIGN.md at the repo root. This page renders it visually. Keep them in sync when tokens change.

Philosophy

GAIA’s UI is dark-first, flat, and single-accent. Every decision flows from five constraints:
  • Dark-first. Primary experience is dark mode (#111111 background). Light mode is supported via CSS variables but is secondary.
  • Flat depth. Depth comes from layered backgrounds (zinc-800zinc-900) only. Never borders, rings, or outlines.
  • Single accent. One primary action color: #00bbff. Everything else is zinc-scale neutrals.
  • Borderless cards. Data cards use background-only separation — no border-, ring-, or outline- in the card tree.
  • Subtle motion. Animations serve entrance, exit, and state changes only. Never decorative. Keep durations ≤ 300ms.

Colors

Brand Tokens

Primary

Selection

Layout rule: Use --color-primary for CTAs, user chat bubbles, selection highlights, and links. Use --color-primary-bg / --color-secondary-bg for the main app canvas and sidebar. Never use these on dark card surfaces — use zinc directly there.

Semantic Variables (Shadcn / Radix)

Use on layout surfaces and standard components. Switch automatically between light and dark.
VariableLightDark
--backgroundhsl(0 0% 100%)hsl(224 71% 4%)
--foregroundhsl(222.2 47.4% 11.2%)hsl(213 31% 91%)
--mutedhsl(210 40% 96.1%)hsl(223 47% 11%)
--muted-foregroundhsl(215.4 16.3% 46.9%)hsl(215.4 16.3% 56.9%)
--accenthsl(210 40% 96.1%)hsl(216 34% 17%)
--borderhsl(214.3 31.8% 91.4%)hsl(216 34% 17%)
--ringhsl(215 20.2% 65.1%)hsl(216 34% 17%)
--destructivehsl(0 100% 50%)hsl(0 63% 31%)

Zinc Scale — Dark Card Surfaces

Use zinc directly on dark card surfaces — not the CSS variables above.

Surfaces

Text

Status Colors

Always use /10 opacity background paired with matching foreground text. Never solid.

Status

Priority

bg-emerald-400/10 text-emerald-400  // success
bg-amber-400/10  text-amber-400     // warning
bg-red-400/10    text-red-400       // error
bg-blue-400/10   text-blue-400      // info
bg-zinc-700/50   text-zinc-400      // pending
Never use solid color backgrounds for status badges — always /10 opacity background paired with matching text color.

Typography

Three font families. Never set font-family inline — use the Tailwind token class.
TokenFamilyWeightsUse
font-sansInterAllAll UI — body, labels, buttons, inputs
font-serifPP Editorial New200, 400Editorial headings, landing hero text
font-monoAnonymous Pro400, 700Code blocks, <code>, technical content

Specimens

Inter — font-sans
The quick brown fox jumps
GAIA proactively manages your email, calendar, tasks, and workflows — so you don’t have to.
Light 300Regular 400Medium 500Semibold 600Bold 700
PP Editorial New — font-serif
The future of personal AI
is already here.
Ultralight 200Ultralight ItalicRegular 400Regular Italic
Anonymous Pro — font-mono
const gaia = await agent.run(“Summarize my inbox”);

Heading scale

Defined globally via @layer base — use semantic HTML tags, styles apply automatically.
TagClassesSize
<h1>text-3xl font-bold30px
<h2>text-2xl font-bold24px
<h3>text-xl font-bold20px
<h4>text-lg font-bold18px
<h5>text-base font-bold16px
<h6>text-sm font-bold14px

Text patterns

// Uppercase section labels — settings, card headers, form groups
<p className="text-xs font-medium uppercase tracking-wider text-zinc-500">
  Section Title
</p>

// Truncation — always truncate in constrained containers
<span className="truncate">...</span>
<p className="line-clamp-2">...</p>
<p className="line-clamp-1 max-w-[200px]">...</p>
Inline code gets border-radius: 10px and padding: 4px globally. Use font-mono or .monospace class.

Spacing

ValueUse
gap-1 / gap-1.5Icon + label pairs
gap-2Standard row items
gap-3Section spacing inside cards
space-y-2Vertical list of items inside a card
p-3Inner card item padding
p-4Outer card padding
px-3 / px-4Horizontal padding on inputs, buttons

Border Radius

ContextClassSize
Dark cards — outerrounded-2xl16px
Dark cards — inner itemsrounded-2xl or rounded-xl12px
Imagesrounded-3xl24px
Buttons, inputsrounded-md6px
Badges, pillsrounded-full
Context menusrounded-xl12px
Never use rounded-lg on card containers — that’s the Shadcn base radius, visually too small. Cards always use rounded-2xl.

Depth & Elevation

Depth primarily from background layering and blur, not shadow.
ContextValue
Buttons, inputsshadow-xs
Dialogs, sheetsshadow-lg
Dark cards (solid)No shadow — flat design
Dark cards (glass)bg-zinc-800/40 backdrop-blur-xl
Hover on dark surfaceshover:bg-white/5
LevelClassUse
Moderatebackdrop-blur-lgGlass cards
Standardbackdrop-blur-xlPanels overlaying content, floating cards
Maximumbackdrop-blur-2xlSearch overlays, full-screen modals

Dark Card System

All data cards, tool sections, and info panels use this contract. Two-tone zinc depth, no borders.

Template

"use client";

const statusClasses = {
  success: "bg-emerald-400/10 text-emerald-400",
  error:   "bg-red-400/10 text-red-400",
  warning: "bg-amber-400/10 text-amber-400",
  info:    "bg-blue-400/10 text-blue-400",
  pending: "bg-zinc-700/50 text-zinc-400",
} as const;

export default function MyCard({ title, items, badge }) {
  return (
    <div className="rounded-2xl bg-zinc-800 p-4 w-fit min-w-[400px]">
      <div className="flex items-center justify-between mb-3">
        <p className="text-sm font-semibold text-zinc-100">{title}</p>
        {badge && (
          <span className="rounded-full bg-zinc-700/50 px-2 py-0.5 text-xs text-zinc-400">
            {badge}
          </span>
        )}
      </div>
      <div className="space-y-2">
        {items.map((item) => (
          <div key={item.id} className="rounded-2xl bg-zinc-900 p-3">
            <div className="flex items-center justify-between gap-2">
              <span className="text-sm font-medium text-zinc-200">{item.label}</span>
              <span className={`rounded-full px-2 py-0.5 text-xs ${statusClasses[item.status]}`}>
                {item.value}
              </span>
            </div>
            {item.meta && <p className="text-xs text-zinc-500 mt-1">{item.meta}</p>}
          </div>
        ))}
      </div>
    </div>
  );
}

Live preview

Recent Activity4 items
Deploy pipelineSuccess

2 minutes ago

Memory syncWarning

12 minutes ago

Email batchInfo

1 hour ago

Layer reference

LayerClasses
Outer containerrounded-2xl bg-zinc-800 p-4 w-fit min-w-[400px]
Outer (accordion)rounded-2xl bg-zinc-800 p-3 py-0
Inner itemrounded-2xl bg-zinc-900 p-3
Inner item compactrounded-xl bg-zinc-900 p-3
Glass variantrounded-2xl bg-zinc-800/40 p-4 backdrop-blur-xl
Section headertext-sm font-semibold text-zinc-100 mb-3
Item titletext-sm font-medium text-zinc-200
Item title (prominent)text-sm font-medium text-zinc-100
Body texttext-xs text-zinc-400
Meta / timestamptext-xs text-zinc-500
Item spacingspace-y-2
Status badgerounded-full px-2 py-0.5 text-xs + status color
Section divider<Divider className="bg-zinc-700/50" /> (HeroUI)
Hoverable list itemp-4 transition-all hover:bg-white/5
Constraints: Never border-, ring-, outline- anywhere in the card tree. rounded-2xl on outer containers always. zinc-800 outer → zinc-900 inner is the entire separation mechanism. Status colors always use /10 opacity backgrounds.

Icons

All icons come from @icons (@theexperiencecompany/gaia-icons). Never raw SVGs.
import { CheckmarkCircle02Icon, Alert01Icon, Copy01Icon } from "@icons";
Icons accept className, height, width, and size props.
ContextValue
Inline (badges, text)height={17}
Action buttonssize={16}
Prominent / decorativesize={24}
// In a button
<Button variant="ghost" size="icon">
  <Copy01Icon className="h-4 w-4" />
</Button>

// In a chip / badge
<Alert01Icon className="text-warning-500" height={17} />

// With hover animation
<SomeIcon className="transition-all duration-200 group-hover:scale-110" />
Never use Unicode/text symbols in JSX: no , , , , ×, or similar. Always use icon components from @icons.

Animations

Available classes

ClassDurationUse
animate-spinInfiniteLoading spinner
animate-pulseInfiniteSkeleton placeholder
animate-accordion-down0.2s ease-outAccordion open
animate-accordion-up0.2s ease-outAccordion close
animate-scale-in0.4s bounceElement entrance
animate-scale-in-blur0.5s bounceBlurred entrance
animate-shimmer2s linearShimmer effect
animate-shake0.7sError shake

Transitions

Default: transition-all duration-200. Use this everywhere unless a specific property needs targeting.
ScenarioClasses
All propertiestransition-all duration-200
Color onlytransition-colors duration-200
Button pressactive:scale-95 transition-all! duration-300

Easing

NameValueUse
DefaulteaseMost transitions
Exit / entranceease-outEntrances, exits
Bouncecubic-bezier(0.34, 1.56, 0.64, 1)scale-in, scale-in-blur

Framer Motion

Import from motion/react — not framer-motion. AnimatePresence is required for exit animations. Keep durations ≤ 300ms for micro-interactions, ≤ 500ms for entrances.
import { AnimatePresence, m } from "motion/react";

<AnimatePresence mode="wait">
  {visible && (
    <m.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
      {content}
    </m.div>
  )}
</AnimatePresence>

Toast / Notifications

Sileo — already mounted globally. Call the toast function directly. Never add <Toaster> or import from sonner / react-hot-toast. Toast style: dark fill (#262626), white title, white/75 description, top-right position. Action button colors apply automatically by type: error → red, warning → amber, success → green, info → blue.
import { toast } from "@/lib/toast";

toast.success("File saved");
toast.error("Something went wrong");
toast.warning("Storage almost full");
toast.info("New message received");

Component Library

Style preset: new-york · Base color: zinc · CSS variables: on · Located at src/components/ui/
ComponentKey details
Buttondefault destructive outline secondary ghost link · sizes: default sm lg icon
Inputh-9 rounded-md shadow-xs · focus ring · aria-invalid for errors
TextareaSame as Input · min-h-16 · auto-height via field-sizing-content
DialogZoom + fade · use for confirmations, forms requiring focus
SheetFade-in slide panel · use for side panels, settings drawers
PopoverAnchored overlay · use for inline pickers, contextual options
TooltipHover label only — no interactive content
DropdownMenu / ContextMenuAction lists
AccordionAnimated expand/collapse
Avatarrounded-full, image + fallback
Skeletonanimate-pulse rounded-md
ScrollAreaRadix scrollable with edge shadows
SidebarCollapsible · Cmd/Ctrl+B toggle · cookie-persisted

Styling Tools

// cn() — always use for conditional class merging, never string concatenation
import { cn } from "@/lib/utils";
<div className={cn("base-class", condition && "conditional-class", className)} />

// cva — for components with multiple visual variants
import { cva } from "class-variance-authority";
const cardVariants = cva("rounded-2xl p-4", {
  variants: {
    depth: {
      outer: "bg-zinc-800",
      inner: "bg-zinc-900",
    },
  },
});

Forms & Validation

Field pattern

<FormField
  control={form.control}
  name="fieldName"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Label</FormLabel>
      <FormControl>
        <Input placeholder="..." {...field} />
      </FormControl>
      <FormMessage />  {/* auto-shows error */}
    </FormItem>
  )}
/>

Input states

Error state is driven by aria-invalid={!!error} — styling applies automatically via the Input component.
StateVisual
Defaultborder-input bg-transparent
Focusring-ring/50 ring-[3px] border-ring
Errorring-destructive/20 border-destructive (via aria-invalid)
Disabledopacity-50 cursor-not-allowed
Loadingcursor-wait (set disabled on the input)

Loading & Empty States

Loading

PatternWhen
<Skeleton className="h-4 w-32 rounded-md" />Known content shape, replacing text/images
animate-pulse on the containerUnknown shape, shimmer a region
animate-spin on an iconInline action in progress
Full <LoadingIndicator />Whole chat response pending
Skeleton inherits: bg-accent animate-pulse rounded-md. Match the skeleton shape to the content it replaces.

Empty states

No shared component — build inline:
<div className="flex flex-col items-center gap-2 py-8 text-center">
  <SomeIcon className="text-zinc-600" size={24} />
  <p className="text-sm text-zinc-400">No items yet</p>
  <p className="text-xs text-zinc-500">Optional sub-text</p>
</div>

Interactive States

StateClasses
Hover (standard)hover:bg-accent · hover:bg-primary/90 · hover:opacity-80
Hover (dark surface)hover:bg-white/5
Focus visiblefocus-visible:ring-ring/50 focus-visible:ring-[3px] focus-visible:border-ring
Active / pressactive:scale-95
Disableddisabled:opacity-50 disabled:pointer-events-none disabled:cursor-not-allowed
Erroraria-invalid:ring-destructive/20 aria-invalid:border-destructive
Hover revealopacity-0 transition-all group-hover:opacity-100 (parent needs group)

Dark / Light Mode

Class-based: .dark on <html>. Tailwind dark: modifier works everywhere.
  • Layout surfacesbg-background text-foreground (auto-switches via CSS vars)
  • Dark cardsbg-zinc-800 / bg-zinc-900 (always dark — no dark: needed)
  • Explicit overrides → only when CSS variables don’t cover it
  • Brand cyan (#00bbff) is the same in both modes

Responsiveness

BreakpointValueImpact
Mobilemax-width: 600pxFull-width layouts, larger tap targets
Tabletmax-width: 990pxNavbar becomes full-width strip
md:768pxText size shifts (text-basetext-sm)
Core layout does not use lg:, xl:, or 2xl: breakpoints.

Scrollbars

Global scrollbar is already styled (8px, pill-shaped, zinc-700 thumb). Use .no-scrollbar to suppress chrome on scroll areas where it would be distracting.

Rules

Do
  • rounded-2xl on all outer card containers
  • zinc-800 outer → zinc-900 inner for card depth
  • /10 opacity backgrounds for all status colors
  • Import icons from @icons
  • cn() for all conditional class merging
  • transition-all duration-200 as the default transition
  • AnimatePresence for exit animations
  • Import from motion/react
  • import { toast } from "@/lib/toast"
Don’t
  • border-, ring-, or outline- anywhere in a card tree
  • rounded-lg on card containers
  • Solid backgrounds for status badges
  • Unicode symbols in JSX (, , ) — use icons
  • Add <Toaster> — already mounted globally
  • Import from sonner or react-hot-toast
  • CSS variables on dark card surfaces — use zinc directly
  • Set font-family inline — use Tailwind token classes
  • Import from framer-motion — use motion/react