Automatic UI adaptation for React — Respects your users' preferences without a single line of theme code.
<MorphProvider>
<App />
</MorphProvider>That's it. Your app now adapts to:
- 🌙 Dark / light mode based on system preference and time of day
- 👁️ High contrast mode
- 🎨 Color blindness friendly palette
- ⚡ Reduced motion
- 🌍 User language detection
Before anything else, know this: Morph never forces itself on your UI. Two mechanisms let you draw a hard boundary around anything you don't want touched — styles, zone ordering, font-scale, behavioral tracking. Everything.
Put this attribute on any element and Morph stops at that boundary. The element and every descendant keep their original styles, order, and are not tracked by the behavioral observers.
<div data-morph-skip>
{/* Your brand logo, a third-party widget, a signed-off design system…
Morph will not modify its colors, reorder it, or scale its text. */}
<BrandLogo />
</div>When to use it:
- Brand assets that must render pixel-perfect (logos, signed designs)
- Third-party embeds you don't own (payment widgets, chart libraries)
<canvas>,<video>,<iframe>content you render yourself- Any subtree where you've already hand-tuned the dark/light variants
Pass safeMode on the provider and Morph will still detect theme,
contrast, language, reduced-motion, and system preference — but it will
never inject CSS, never modify styles, and never reorder DOM. Perfect
for testing, debugging, or gradually rolling out adaptation in production.
<MorphProvider safeMode>
<App />
</MorphProvider>Inside your app, useMorph() still returns all the detected values
plus safeMode: true, so you can wire your own theming with full signal
but zero side-effects:
const { theme, language, safeMode } = useMorph()
// theme is 'dark' or 'light' — apply it yourself, or not.Need an island of Morph inside a skipped subtree? Add
data-morph-force and adaptation resumes from that element down.
<div data-morph-skip>
<LegacyWidget />
<section data-morph-force>
{/* Morph applies again in here, even though the parent is skipped */}
<ModernCard />
</section>
</div>The resolution rule is "closest attribute wins": walking up from any
element, the first data-morph-force or data-morph-skip
encountered decides.
Every time you build an app, your designer creates multiple mockups — light theme, dark theme, high contrast variants. Your developer hardcodes every condition. And your user still gets a generic interface that doesn't match their real context.
That's a lot of work for a bad result.
Morph detects your app's existing theme, reads your styles, and automatically generates the right adaptation — without breaking what you already built.
Same app. Different users. Right interface every time.
npm install @morphuiapp/morphuiimport { MorphProvider } from '@morphuiapp/morphui'
export default function App() {
return (
<MorphProvider>
<YourApp />
</MorphProvider>
)
}Zero config. Morph detects everything from the browser and applies adaptation locally — no network call, no telemetry:
<MorphProvider>
<App />
</MorphProvider>Morph follows this logic automatically:
| App theme | Time of day | System preference | Action |
|---|---|---|---|
| Light | Day | None | ✅ Nothing — stay light |
| Light | Night | None | 🌙 Switch to dark |
| Dark | Day | None | ☀️ Switch to light |
| Dark | Night | None | ✅ Nothing — stay dark |
| Any | Any | Dark (manual) | 🌙 Always dark |
| Any | Any | Light (manual) | ☀️ Always light |
System preference always wins. Time-based logic only applies when the user hasn't manually set a preference on their device.
1. Configure Tailwind dark mode
// tailwind.config.js
module.exports = {
darkMode: 'class', // Required
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
}2. Use Morph variables as fallbacks (optional)
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: var(--morph-bg, #ffffff);
--foreground: var(--morph-text-primary, #171717);
--card: var(--morph-card-bg, #ffffff);
--border: var(--morph-border, #e5e5e5);
}
body {
background: var(--background);
color: var(--foreground);
}3. Wrap your app
import { MorphProvider } from '@morphuiapp/morphui'
export default function App() {
return (
<MorphProvider>
<YourApp />
</MorphProvider>
)
}Morph automatically toggles the .dark class on <html> — all your dark: Tailwind classes work instantly.
/* styles.css */
:root {
--background: #ffffff;
--foreground: #171717;
--primary: #2563eb;
--card: #f5f5f5;
--border: #e5e5e5;
}
body {
background: var(--morph-bg, var(--background));
color: var(--morph-text-primary, var(--foreground));
}
.card {
background: var(--morph-card-bg, var(--card));
border: 1px solid var(--morph-border, var(--border));
}
button {
background: var(--morph-primary, var(--primary));
color: var(--morph-primary-text, #ffffff);
}Morph reads your existing variables and generates adapted versions automatically.
Morph reads the computed styles of your DOM elements and injects style overrides. For best results, define your colors as CSS variables on :root.
// styles.scss
:root {
--color-bg: #ffffff;
--color-text: #171717;
--color-primary: #2563eb;
}
body {
background: var(--morph-bg, var(--color-bg));
color: var(--morph-text-primary, var(--color-text));
}
⚠️ Note: Apps using only hardcoded CSS colors (no CSS variables) will get partial adaptation. For full accuracy, migrate your key colors to CSS variables.
import { useMorph } from '@morphuiapp/morphui'
function MyComponent() {
const {
theme,
highContrast,
colorBlindMode,
language,
prefersReducedMotion,
adaptation, // 'none' | 'darken' | 'lighten'
appBrightness, // original theme of your app
} = useMorph()
return (
<div>
<p>Theme: {theme}</p>
<p>Adaptation: {adaptation}</p>
<p>Language: {language}</p>
</div>
)
}import { useTheme } from '@morphuiapp/morphui'
const { theme, prefersReducedMotion } = useTheme()
// theme → 'light' | 'dark'
// prefersReducedMotion → true | falseimport { useAccessibility } from '@morphuiapp/morphui'
const { highContrast, forcedColors, colorBlindMode } = useAccessibility()
// highContrast → 'normal' | 'high'
// forcedColors → true | false
// colorBlindMode → null | 'deuteranopia' | 'protanopia' | 'tritanopia'| Variable | Description |
|---|---|
--morph-bg |
Main background |
--morph-card-bg |
Card / surface background |
--morph-text-primary |
Primary text |
--morph-text-secondary |
Secondary / muted text |
--morph-primary |
Brand / accent color |
--morph-primary-text |
Text on primary color |
--morph-border |
Border color |
--morph-surface |
Alternative surface |
--morph-font-scale |
Font boost in high contrast (0px default, 2px HC) |
--morph-motion |
Motion multiplier (1 default, 0 reduced motion) |
Follow these guidelines to get the best theme adaptation from Morph.
/* Best — Morph reads and adapts these automatically */
:root {
--background: #ffffff;
--foreground: #171717;
--primary: #6366f1;
--card: #f5f5f5;
}
body { background: var(--background); color: var(--foreground); }{/* Good — Morph adapts standard Tailwind classes */}
<div className="bg-white text-gray-900 border-gray-200">
{/* Works but harder to adapt — arbitrary hex values */}
<div className="bg-[#f5f5f5] text-[#1c1c1e] border-[#dadce0]">{/* Good — Morph knows this div has a white background */}
<div className="bg-white rounded-lg p-6">
<p className="text-gray-700">Content</p>
</div>
{/* Bad — div inherits background, Morph can't distinguish it */}
<div className="rounded-lg p-6">
<p className="text-gray-700">Content</p>
</div>/* Bad — blocks Morph */
.my-card { background-color: #fff !important; }
/* Good — Morph can override it */
.my-card { background-color: #fff; }{/* Bad — inline styles are harder to override */}
<div style={{ background: '#ffffff', color: '#000000' }}>
{/* Good — classes are easy to override */}
<div className="bg-white text-gray-900">{/* PDF viewers, canvas, code editors — don't adapt these */}
<div data-morph-skip>
<PDFViewer document={doc} />
</div>
{/* Canvas and video are automatically excluded */}
<canvas /> {/* Never touched by Morph */}{/* Good — Morph detects and adapts the gradient */}
<section className="bg-linear-to-br from-indigo-50 via-white to-blue-50">
{/* Bad — gradient on a wrapper div without explicit class */}
<div style={{ background: 'linear-gradient(...)' }}>Morph automatically detects and adapts Ant Design, MUI, and Chakra UI components. No special config needed. Just wrap your app:
<MorphProvider>
<ConfigProvider> {/* antd */}
<App />
</ConfigProvider>
</MorphProvider>Make sure you have darkMode: 'class' in tailwind.config.js — this is required.
1. MorphProvider not at root level
// ❌ Wrong
function Page() {
return <MorphProvider><Content /></MorphProvider>
}
// ✅ Correct — wrap at the very top
function App() {
return <MorphProvider><Page /></MorphProvider>
}2. Colors hardcoded with !important
/* ❌ Blocks Morph */
.my-class { color: #000 !important; }
/* ✅ Use CSS variables */
.my-class { color: var(--morph-text-primary); }3. Inline styles
{/* ❌ Can't be overridden */}
<div style={{ color: '#000' }}>Text</div>
{/* ✅ Use CSS classes */}
<div className="text-foreground">Text</div>MorphProvider uses React hooks — it must be in a Client Component:
// app/providers.tsx
'use client';
import { MorphProvider } from '@morphuiapp/morphui';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<MorphProvider>
{children}
</MorphProvider>
);
}// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}For Vite / CRA, add this in index.html before React loads:
<script>
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
}
</script>Morph includes an automatic contrast fixer that checks every text element against its real background. If issues persist:
- Make sure elements have explicit backgrounds (not transparent)
- Update to the latest version:
npm update @morphuiapp/morphui
| Framework | Status |
|---|---|
| React 18+ | ✅ Supported |
| Next.js 13+ App Router | ✅ Supported |
| Next.js 12 Pages Router | ✅ Supported |
| Vite + React | ✅ Supported |
| Tailwind CSS v3 / v4 | ✅ Supported |
| shadcn/ui | ✅ Supported |
| Ant Design v5+ | ✅ Supported |
| Material UI (MUI) v5+ | ✅ Supported |
| Chakra UI v3 | ✅ Supported |
| CSS / CSS Variables | ✅ Supported |
| SCSS / SASS | ✅ Supported (best with CSS variables) |
| Plain CSS | ✅ Supported |
| Vue.js | 🔜 On the roadmap |
| React Native | 🔜 On the roadmap |
| Browser | Minimum version |
|---|---|
| Chrome | 76+ |
| Edge | 79+ |
| Firefox | 67+ |
| Safari | 12.1+ |
- Automatic light / dark theme detection
- Time-based adaptation (day / night)
- System preference — always takes priority
- High contrast mode
- Color blindness mode
- Reduced motion support
- Language detection
- Tailwind CSS v3 / v4 support
- shadcn/ui support
- Ant Design support
- Material UI (MUI) support
- Chakra UI v3 support
- CSS / CSS Variables / SCSS support
- Local HSL-based dark generation (no backend, no telemetry)
- Automatic WCAG contrast fixes
- Gradient adaptation (from-, via-, to-*)
- Inline style adaptation
- Portal / modal / dropdown detection
- Canvas / PDF / video exclusion
- Vue.js support
- React Native support
Want behavioral intelligence? Zone tracking, navigation-pattern reorder, heatmaps, AI palette generation, content morphing and the SaaS dashboard live in the proprietary V2 build (separate package). The OSS V1 here is a fully self-contained system-detection + theme-adaptation layer with zero network calls.
git clone https://github.com/morphuiapp/morphui
cd morph-ui
npm install
npm run devOpen an issue before submitting a pull request for major changes.
MIT © Cabraule
Built with ❤️ — Because every user deserves an interface made for them.