Theming
Customize Gecko UI colors and design tokens using CSS variables
Theming
Gecko UI uses CSS custom properties (variables) for theming with OKLCH color values. You can override theme colors using any valid CSS color format — oklch(), hex, rgb(), hsl(), or any other format you prefer.
CSS Variables
Theme colors are defined as --color-* CSS custom properties.
Tailwind Utilities
When the stylesheet is imported from your Tailwind CSS v4 file, every color below is also a Tailwind utility, with no extra setup:
@import "tailwindcss";
@layer components {
@import "@geckoui/geckoui/styles.css";
}<div className="bg-surface-secondary text-text-muted border border-border-secondary">
<span className="text-error">Required</span>
<div className="bg-primary-600/50 hover:bg-surface-hover dark:bg-surface-tertiary" />
</div>The utilities point at the variables (bg-surface-primary is
background-color: var(--color-surface-primary)), so they follow dark mode and any
override below. Opacity modifiers and variants work as with Tailwind's own colors.
Importing styles.css only from JavaScript styles the components but does not add these
utilities, since your Tailwind never reads the file.
Primary Colors
Primary color scale from 50 (lightest) to 950 (darkest):
:root {
--color-primary-50: oklch(0.9705 0.0142 254.6);
--color-primary-100: oklch(0.9319 0.0316 255.59);
--color-primary-200: oklch(0.8823 0.0571 254.13);
--color-primary-300: oklch(0.8091 0.0956 251.81);
--color-primary-400: oklch(0.7137 0.1434 254.62);
--color-primary-500: oklch(0.6231 0.188 259.81);
--color-primary-600: oklch(0.5461 0.2152 262.88);
--color-primary-700: oklch(0.4882 0.2172 264.38);
--color-primary-800: oklch(0.4244 0.1809 265.64);
--color-primary-900: oklch(0.3791 0.1378 265.52);
--color-primary-950: oklch(0.2823 0.0874 267.94);
}Surface Colors
Background colors for various UI surfaces:
:root {
--color-surface-primary: oklch(1 0 none); /* Main background */
--color-surface-secondary: oklch(0.9851 0 none); /* Secondary background */
--color-surface-tertiary: oklch(0.9702 0 none); /* Tertiary background */
--color-surface-hover: oklch(0.9702 0 none); /* Hover state */
--color-surface-hover-strong: oklch(0.9401 0 none); /* Strong hover state */
--color-surface-active: oklch(0.8699 0 none); /* Active/pressed state */
--color-surface-disabled: oklch(0.9401 0 none); /* Disabled state */
--color-surface-overlay: oklch(0 0 none); /* Backdrop overlay */
--color-surface-autofill: oklch(0.9702 0 none); /* Browser autofill */
--color-surface-emphasis: oklch(0.4461 0.0263 256.8);
}Text Colors
Text colors for different hierarchy levels:
:root {
--color-text-primary: oklch(0.2046 0 none); /* Primary text */
--color-text-secondary: oklch(0.3715 0 none); /* Secondary text */
--color-text-tertiary: oklch(0.5555 0 none); /* Tertiary text */
--color-text-disabled: oklch(0.7155 0 none); /* Disabled text */
--color-text-placeholder: oklch(0.7155 0 none); /* Placeholder text */
--color-text-inverse: oklch(0.9851 0 none); /* Inverse text (on dark) */
--color-text-muted: oklch(0.5555 0 none); /* Muted text */
--color-text-on-primary: oklch(1 0 none); /* Text on primary color */
}Border Colors
Border colors for various states:
:root {
--color-border-primary: oklch(0.9401 0 none); /* Default border */
--color-border-secondary: oklch(0.8699 0 none); /* Secondary border */
--color-border-focus: oklch(0.7155 0 none); /* Focus state */
--color-border-hover: oklch(0.8699 0 none); /* Hover state */
--color-border-disabled: oklch(0.9401 0 none); /* Disabled state */
--color-border-invalid: oklch(0.6368 0.2078 25.33); /* A field in error */
--color-border-invalid-hover: oklch(0.5168 0.2178 25.33); /* ...under the pointer */
}Every field that can be invalid takes its red from --color-border-invalid, so one line
retints all of them:
:root {
--color-border-invalid: rebeccapurple;
}--color-border-invalid-hover is the step the border takes under the pointer: darker in
light mode, lighter in dark, the way the other borders behave against each ground.
Semantic Colors
One token per meaning, shared by every component that reports status. Overriding one updates Badge, Alert and Toast together.
:root {
--color-success: oklch(0.6271 0.1699 149.21);
--color-error: oklch(0.6368 0.2078 25.33);
--color-warning: oklch(0.6685 0.1626 58.32);
--color-info: oklch(0.6231 0.188 259.81);
}| Variable | Used by |
|---|---|
--color-success | Badge, Alert, Toast |
--color-error | Badge, Alert, Toast, InputError, the required marker on Label |
--color-warning | Badge, Alert, Toast |
--color-info | Badge, Alert, Toast |
These are lifted in dark mode, since the light values sit too dark on a dark surface.
Override them inside .dark if you change them:
.dark {
--color-success: oklch(0.72 0.17 149.21);
}Scrollbar Colors
Custom scrollbar styling (these use the --gecko-ui- prefix as they are not Tailwind utilities):
:root {
--gecko-ui-scrollbar-track: oklch(0 0 none);
--gecko-ui-scrollbar-thumb: oklch(0.8717 0.0093 258.34);
--gecko-ui-scrollbar-thumb-hover: oklch(0.7137 0.0192 261.32);
}Dark Mode
Gecko UI includes built-in dark mode support. Apply the .dark class to enable dark theme:
.dark {
--color-surface-primary: oklch(0.2046 0 none);
--color-surface-secondary: oklch(0.2435 0 none);
--color-surface-tertiary: oklch(0.2972 0 none);
--color-surface-hover: oklch(0.2972 0 none);
--color-surface-hover-strong: oklch(0.3715 0 none);
--color-surface-active: oklch(0.5555 0 none);
--color-surface-disabled: oklch(0.2972 0 none);
--color-surface-overlay: oklch(0 0 none);
--color-surface-autofill: oklch(0.2435 0 none);
--color-surface-emphasis: oklch(0.7748 0.0054 247.89);
--color-text-primary: oklch(0.9851 0 none);
--color-text-secondary: oklch(0.8699 0 none);
--color-text-tertiary: oklch(0.7155 0 none);
--color-text-disabled: oklch(0.5555 0 none);
--color-text-placeholder: oklch(0.5555 0 none);
--color-text-inverse: oklch(0.2046 0 none);
--color-text-muted: oklch(0.7155 0 none);
--color-border-primary: oklch(0.4676 0 none);
--color-border-secondary: oklch(0.4676 0 none);
--color-border-focus: oklch(0.5555 0 none);
--color-border-hover: oklch(0.7155 0 none);
--color-border-disabled: oklch(0.2972 0 none);
--color-border-invalid: oklch(0.71 0.19 25.33);
--color-border-invalid-hover: oklch(0.83 0.17 25.33);
--gecko-ui-scrollbar-track: oklch(0 0 none);
--gecko-ui-scrollbar-thumb: oklch(0.4461 0.0263 256.8);
--gecko-ui-scrollbar-thumb-hover: oklch(0.551 0.0234 264.36);
}Implementing Dark Mode
Add the dark class to your root element:
import { useState } from 'react';
function App() {
const [isDark, setIsDark] = useState(false);
return (
<div className={isDark ? 'dark' : ''}>
<button onClick={() => setIsDark(!isDark)}>
Toggle Dark Mode
</button>
{/* Your app content */}
</div>
);
}Or apply it to the <html> element:
'use client';
import { useEffect, useState } from 'react';
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
useEffect(() {
document.documentElement.classList.toggle('dark', theme === 'dark');
}, [theme]);
return <>{children}</>;
}Customizing Colors
Changing Primary Color
Override the primary color scale to match your brand. You can use any CSS color format:
:root {
/* Using oklch */
--color-primary-50: oklch(0.98 0.02 330);
--color-primary-100: oklch(0.95 0.04 330);
--color-primary-200: oklch(0.90 0.08 330);
--color-primary-300: oklch(0.82 0.14 330);
--color-primary-400: oklch(0.72 0.19 330);
--color-primary-500: oklch(0.65 0.24 330);
--color-primary-600: oklch(0.55 0.22 330);
--color-primary-700: oklch(0.48 0.19 330);
--color-primary-800: oklch(0.40 0.16 330);
--color-primary-900: oklch(0.35 0.12 330);
--color-primary-950: oklch(0.25 0.08 330);
}:root {
/* Using hex — works too! */
--color-primary-500: #8b5cf6;
--color-primary-600: #7c3aed;
--color-primary-700: #6d28d9;
}Customizing Specific Colors
Override individual tokens:
:root {
/* Make borders more prominent */
--color-border-primary: oklch(0.8 0 none);
/* Adjust text hierarchy */
--color-text-secondary: oklch(0.4 0 none);
/* Custom hover states */
--color-surface-hover: oklch(0.95 0 none);
}Using Color Variables in Custom CSS
Since variables contain full color values, use them directly with var():
/* Direct usage */
color: var(--color-text-primary);
background: var(--color-surface-primary);
/* With opacity using color-mix() */
color: color-mix(in oklch, var(--color-text-primary) 50%, transparent);
background: color-mix(in oklch, var(--color-surface-primary) 80%, transparent);
/* In Tailwind arbitrary values */
className="text-[var(--color-text-primary)]"
className="bg-[var(--color-surface-hover)]"Component-Specific Styling
Components use their own class names for targeted styling:
/* Style all inputs */
.GeckoUIInput {
--color-border-primary: oklch(0.75 0 none);
}
/* Style all buttons */
.GeckoUIButton {
border-radius: 0.5rem;
}
/* Style specific button variant */
.GeckoUIButton[data-variant="filled"] {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}Component Variables
Some components expose their own variables for size, shape and spacing, so you can restyle them without overriding a class. They fall back to the tokens above, so theming still flows through.
| Component | Prefix | Documented at |
|---|---|---|
| Toast | --gecko-toast-* | Toast |
| Badge | --gecko-badge-* | Badge |
| Alert | --gecko-alert-* | Alert |
For example, widening every toast and rounding it more:
:root {
--gecko-toast-width: 28rem;
--gecko-toast-radius: 1.5rem;
}Scope a variable to one variant or colour to change just that case:
.GeckoUIAlert[data-color="error"] {
--gecko-alert-bg: var(--color-error);
--gecko-alert-accent: white;
}While an overlay holds the scroll lock, --gecko-scrollbar-width is set on the root
element, so anything you pin to the right edge can compensate:
.my-fixed-panel {
right: calc(1rem + var(--gecko-scrollbar-width, 0px));
}Stacking Order
Overlays sit in three tiers:
| Tier | Components | z-index |
|---|---|---|
| Inline dropdowns | Select menu, Breadcrumb list, TagInput list, TimeInput picker | 10 |
| Floating panels | Tooltip, Menu, Popover, DateInput and DateRangeInput calendars | 50 |
| Overlays | Drawer 1000, Dialog 2000, Toast 3000 |
Each is a variable, so a tier can be moved without overriding anything:
:root {
--gecko-toast-z: 5000;
}--gecko-select-menu-z, --gecko-breadcrumb-z, --gecko-tag-input-z, --gecko-time-input-z, --gecko-tooltip-z, --gecko-menu-z, --gecko-popover-z, --gecko-date-input-z, --gecko-date-range-input-z, --gecko-toast-z.
Keep your own chrome below 1000. A sticky header at a higher z-index sits over drawers, dialogs and toasts — a toast you cannot reach is the usual way this shows up.
Customizing Icons
Gecko UI uses CSS mask-image with inline SVGs for icons. This approach allows icons to inherit color from parent elements and be styled with CSS.
To find an icon's class name, use your browser's DevTools to inspect the element. All icon classes follow a consistent naming pattern (e.g., .GeckoUI-icon__check, .GeckoUIAlert__icon[data-variant="error"]).
Overriding an Icon
To replace an icon, override the mask-image property with your custom SVG:
.GeckoUI-icon__check {
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg>');
-webkit-mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg>');
}Overriding Alert Icons
Alert icons are variant-specific. Override them by targeting the variant modifier:
.GeckoUIAlert__icon[data-variant="error"] {
@apply bg-red-600;
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 15v-2h2v2h-2zm0-4V7h2v6h-2z"/></svg>');
-webkit-mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 15v-2h2v2h-2zm0-4V7h2v6h-2z"/></svg>');
}Icon Styling Properties
Icons use these CSS properties which you can also customize:
.GeckoUI-icon__check {
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
-webkit-mask-position: center;
-webkit-mask-size: contain;
-webkit-mask-repeat: no-repeat;
}Using External Icon Libraries
You can also use icons from external libraries by converting them to data URLs:
/* Using a Lucide icon */
.GeckoUI-icon__arrow-down {
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>');
-webkit-mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>');
}Best Practices
- Use Any Color Format: Override with
oklch(),hex,rgb(),hsl()— whatever you prefer - Maintain Consistency: Keep color scales consistent with 50-950 range
- Test Contrast: Ensure text colors meet WCAG contrast requirements
- Dark Mode Parity: When customizing light mode, also update dark mode tokens
- Component Overrides: Use component-specific classes (
.GeckoUIButton) rather than global tokens when possible
Example: Complete Custom Theme
/* custom-theme.css */
:root {
/* Brand primary color (Purple) */
--color-primary-50: oklch(0.98 0.02 300);
--color-primary-100: oklch(0.95 0.05 300);
--color-primary-200: oklch(0.90 0.09 300);
--color-primary-300: oklch(0.84 0.14 300);
--color-primary-400: oklch(0.74 0.19 300);
--color-primary-500: oklch(0.65 0.24 300);
--color-primary-600: oklch(0.56 0.23 300);
--color-primary-700: oklch(0.49 0.20 300);
--color-primary-800: oklch(0.42 0.17 300);
--color-primary-900: oklch(0.36 0.13 300);
--color-primary-950: oklch(0.26 0.08 300);
}Import after the base styles:
import '@geckoui/geckoui/styles.css';
import './custom-theme.css';Next Steps
- Installation - Setup guide