9.4 KiB
9.4 KiB
Markdown Editor Theme Integration Guide
Overview
Integrate @uiw/react-md-editor with shadcn/ui theming system for consistent light/dark mode support.
Theme Switching
Using next-themes
'use client'
import { useTheme } from 'next-themes'
import MDEditor from '@uiw/react-md-editor'
export function ThemedMarkdownEditor({ value, onChange }) {
const { theme } = useTheme()
return (
<div data-color-mode={theme === 'dark' ? 'dark' : 'light'}>
<MDEditor
value={value}
onChange={onChange}
height={400}
/>
</div>
)
}
data-color-mode Attribute
The data-color-mode attribute controls the editor's theme:
light- Light modedark- Dark modeauto- System preference (default)
CSS Variable Mapping
Map shadcn/ui CSS variables to editor styles:
/* globals.css */
/* Editor Container */
.w-md-editor {
--md-editor-bg: hsl(var(--background));
--md-editor-color: hsl(var(--foreground));
--md-editor-border: hsl(var(--border));
}
/* Toolbar */
.w-md-editor-toolbar {
background-color: hsl(var(--muted) / 0.3);
border-bottom-color: hsl(var(--border));
}
.w-md-editor-toolbar button {
color: hsl(var(--foreground));
}
.w-md-editor-toolbar button:hover {
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
}
.w-md-editor-toolbar li.active button {
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
}
/* Editor Content */
.w-md-editor-content {
color: hsl(var(--foreground));
}
.w-md-editor-text-pre,
.w-md-editor-text-input {
color: hsl(var(--foreground));
caret-color: hsl(var(--foreground));
}
/* Preview */
.w-md-editor-preview {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
}
.wmde-markdown {
background-color: transparent;
color: hsl(var(--foreground));
}
/* Code Blocks */
.wmde-markdown pre {
background-color: hsl(var(--muted));
}
.wmde-markdown code {
background-color: hsl(var(--muted));
color: hsl(var(--primary));
}
/* Inline Code */
.wmde-markdown :not(pre) > code {
background-color: hsl(var(--muted));
color: hsl(var(--primary));
padding: 0.2em 0.4em;
border-radius: 0.25rem;
font-size: 0.875em;
}
/* Blockquotes */
.wmde-markdown blockquote {
border-left-color: hsl(var(--primary) / 0.5);
background-color: hsl(var(--muted) / 0.5);
color: hsl(var(--muted-foreground));
}
/* Links */
.wmde-markdown a {
color: hsl(var(--primary));
text-decoration: underline;
}
.wmde-markdown a:hover {
color: hsl(var(--primary) / 0.8);
}
/* Tables */
.wmde-markdown table {
border-color: hsl(var(--border));
}
.wmde-markdown th,
.wmde-markdown td {
border-color: hsl(var(--border));
}
.wmde-markdown th {
background-color: hsl(var(--muted));
}
.wmde-markdown tr:nth-child(even) {
background-color: hsl(var(--muted) / 0.3);
}
/* Horizontal Rule */
.wmde-markdown hr {
border-color: hsl(var(--border));
}
/* Headings */
.wmde-markdown h1,
.wmde-markdown h2,
.wmde-markdown h3,
.wmde-markdown h4,
.wmde-markdown h5,
.wmde-markdown h6 {
color: hsl(var(--foreground));
border-bottom: none;
}
Tailwind Class Approach
Use Tailwind utility classes for theming:
'use client'
import { useTheme } from 'next-themes'
import MDEditor from '@uiw/react-md-editor'
import { cn } from '@/lib/utils'
export function MarkdownEditor({ value, onChange, className }) {
const { theme } = useTheme()
return (
<div
data-color-mode={theme === 'dark' ? 'dark' : 'light'}
className={cn(
'rounded-md border border-input overflow-hidden',
className
)}
>
<MDEditor
value={value}
onChange={onChange}
height={400}
className="shadow-none"
/>
</div>
)
}
Typography Integration
Integrate with Tailwind Typography (prose):
/* Preview with prose styling */
.w-md-editor-preview {
@apply prose prose-sm dark:prose-invert max-w-none p-4;
}
/* Override prose defaults with theme colors */
.w-md-editor-preview.prose {
--tw-prose-body: hsl(var(--foreground));
--tw-prose-headings: hsl(var(--foreground));
--tw-prose-links: hsl(var(--primary));
--tw-prose-bold: hsl(var(--foreground));
--tw-prose-code: hsl(var(--primary));
--tw-prose-pre-bg: hsl(var(--muted));
--tw-prose-quotes: hsl(var(--muted-foreground));
--tw-prose-quote-borders: hsl(var(--primary) / 0.5);
}
.w-md-editor-preview.prose.dark {
--tw-prose-body: hsl(var(--foreground));
--tw-prose-headings: hsl(var(--foreground));
--tw-prose-links: hsl(var(--primary));
--tw-prose-bold: hsl(var(--foreground));
--tw-prose-code: hsl(var(--primary));
--tw-prose-pre-bg: hsl(var(--muted));
--tw-prose-quotes: hsl(var(--muted-foreground));
--tw-prose-quote-borders: hsl(var(--primary) / 0.5);
}
Complete Component with Theme
'use client'
import { useTheme } from 'next-themes'
import dynamic from 'next/dynamic'
import { ComponentProps, useEffect, useState } from 'react'
import rehypeSanitize from 'rehype-sanitize'
import { cn } from '@/lib/utils'
const MDEditor = dynamic(
() => import('@uiw/react-md-editor'),
{ ssr: false }
)
type MDEditorProps = ComponentProps<typeof MDEditor>
interface MarkdownEditorProps extends Omit<MDEditorProps, 'data-color-mode'> {
value: string
onChange?: (value?: string) => void
height?: number | string
className?: string
}
export function MarkdownEditor({
value,
onChange,
height = 400,
className,
...props
}: MarkdownEditorProps) {
const { theme, resolvedTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return (
<div
className={cn(
'rounded-md border border-input bg-background',
className
)}
style={{ height }}
>
<div className="flex items-center justify-center h-full text-muted-foreground">
Loading editor...
</div>
</div>
)
}
const colorMode = (resolvedTheme || theme) === 'dark' ? 'dark' : 'light'
return (
<div
data-color-mode={colorMode}
className={cn('markdown-editor-wrapper', className)}
>
<MDEditor
value={value}
onChange={onChange}
height={height}
previewOptions={{
rehypePlugins: [[rehypeSanitize]],
}}
{...props}
/>
</div>
)
}
Dark Mode Specifics
Handle System Preference
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
export function useEditorTheme() {
const { theme, systemTheme } = useTheme()
const [colorMode, setColorMode] = useState<'light' | 'dark'>('light')
useEffect(() => {
const effectiveTheme = theme === 'system' ? systemTheme : theme
setColorMode(effectiveTheme === 'dark' ? 'dark' : 'light')
}, [theme, systemTheme])
return colorMode
}
// Usage
const colorMode = useEditorTheme()
<div data-color-mode={colorMode}>
<MDEditor {...props} />
</div>
Handle Theme Transitions
/* Smooth theme transitions */
.w-md-editor,
.w-md-editor-toolbar,
.w-md-editor-content,
.w-md-editor-preview,
.wmde-markdown,
.wmde-markdown * {
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
Custom Syntax Highlighting
Integrate with shadcn/ui color scheme:
import { useTheme } from 'next-themes'
import MDEditor from '@uiw/react-md-editor'
export function MarkdownEditor({ value, onChange }) {
const { theme } = useTheme()
return (
<div data-color-mode={theme === 'dark' ? 'dark' : 'light'}>
<MDEditor
value={value}
onChange={onChange}
previewOptions={{
components: {
code: ({ node, inline, className, children, ...props }) => {
return inline ? (
<code className="bg-muted text-primary px-1.5 py-0.5 rounded text-sm" {...props}>
{children}
</code>
) : (
<code className={className} {...props}>
{children}
</code>
)
},
},
}}
/>
</div>
)
}
Testing Theme Integration
import { render, screen } from '@testing-library/react'
import { ThemeProvider } from 'next-themes'
import { MarkdownEditor } from './MarkdownEditor'
describe('MarkdownEditor Theme', () => {
it('applies light theme', () => {
render(
<ThemeProvider defaultTheme="light">
<MarkdownEditor value="Test" onChange={() => {}} />
</ThemeProvider>
)
const wrapper = screen.getByRole('textbox').closest('[data-color-mode]')
expect(wrapper).toHaveAttribute('data-color-mode', 'light')
})
it('applies dark theme', () => {
render(
<ThemeProvider defaultTheme="dark">
<MarkdownEditor value="Test" onChange={() => {}} />
</ThemeProvider>
)
const wrapper = screen.getByRole('textbox').closest('[data-color-mode]')
expect(wrapper).toHaveAttribute('data-color-mode', 'dark')
})
})
Troubleshooting
Issue: Theme not applying immediately Solution: Ensure editor is mounted after theme is resolved
Issue: Flash of wrong theme Solution: Add loading state while theme resolves
Issue: CSS variables not working Solution: Verify CSS is imported in correct order (globals.css before editor CSS)
Issue: Dark mode colors not matching shadcn/ui Solution: Use HSL values from CSS variables, not hardcoded colors