Skip to main content
Yoopta Editor provides a theme system that allows you to apply consistent visual styling across plugins. Themes are implemented as UI component overlays that can be applied to existing plugins.

What is a Theme?

A theme is a collection of styled React components that replace the default rendering of plugin elements. Instead of modifying plugin code, themes provide an elegant way to customize the visual appearance.

Available Themes

Yoopta Editor provides three built-in theme packages:
  • @yoopta/theme-base - Minimal base styling
  • @yoopta/theme-material - Material Design styled components
  • @yoopta/theme-shadcn - Shadcn UI styled components

Using a Theme

Installation

npm install @yoopta/theme-shadcn
# or
yarn add @yoopta/theme-shadcn

Applying a Theme

Use the applyTheme function to apply theme components to your plugins:
packages/themes/shadcn/src/applyTheme.ts
import { applyTheme } from '@yoopta/theme-shadcn';
import { Paragraph, HeadingOne, Accordion } from '@yoopta/plugins';

const themedPlugins = applyTheme([
  Paragraph,
  HeadingOne,
  Accordion,
]);

const editor = createYooptaEditor({
  plugins: themedPlugins,
  marks: MARKS,
});
The applyTheme function automatically maps theme components to their corresponding plugins. It returns new plugin instances with themed rendering.

How Themes Work

Themes use the plugin extend method to replace element render functions:
packages/themes/shadcn/src/applyTheme.ts
export function applyTheme(
  plugins: PluginWithUI[],
  extensions?: PluginExtensions,
): PluginWithUI[] {
  // Mapping of plugin types to their UI components
  const uiMap: Record<string, any> = {
    Accordion: AccordionUI,
    Paragraph: ParagraphUI,
    Blockquote: BlockquoteUI,
    // ... more mappings
  };

  return plugins.map((plugin) => {
    const pluginType = plugin.getPlugin.type;
    const ui = uiMap[pluginType];

    if (!ui) return plugin;  // No theme for this plugin

    // Apply theme UI
    return plugin.extend({
      elements: ui,
      ...extensions?.[pluginType],
    });
  });
}

Theme Extensions

You can provide additional configuration when applying a theme:
import { applyTheme } from '@yoopta/theme-shadcn';
import { Accordion, Paragraph, HeadingOne } from '@yoopta/plugins';

const themedPlugins = applyTheme(
  [Accordion, Paragraph, HeadingOne],
  {
    Accordion: {
      // Allow nested plugins in accordion content
      injectElementsFromPlugins: [Paragraph, HeadingOne],
      // Custom element overrides
      elements: {
        'accordion-list-item': {
          props: { 
            defaultExpanded: true 
          },
        },
      },
    },
  }
);

Extension Options

type PluginExtensions = Record<string, {
  injectElementsFromPlugins?: PluginWithUI[];
  events?: any;
  options?: any;
  elements?: any;
}>;
OptionDescription
injectElementsFromPluginsAllow elements from other plugins to be nested
eventsOverride or add event handlers
optionsOverride plugin options
elementsOverride specific element configurations

Creating Custom Themes

You can create your own theme package:

1. Define Styled Components

// my-theme/paragraph.tsx
import type { PluginElementRenderProps } from '@yoopta/editor';

export const ParagraphUI = {
  paragraph: {
    render: (props: PluginElementRenderProps) => (
      <p 
        className="my-theme-paragraph"
        {...props.attributes}
      >
        {props.children}
      </p>
    ),
  },
};

2. Create Theme Mappings

// my-theme/index.ts
import { ParagraphUI } from './paragraph';
import { HeadingOneUI, HeadingTwoUI } from './headings';
import { CalloutUI } from './callout';

export const MyThemeUI = {
  Paragraph: ParagraphUI,
  HeadingOne: HeadingOneUI,
  HeadingTwo: HeadingTwoUI,
  Callout: CalloutUI,
  // ... more components
};

3. Create Apply Theme Function

// my-theme/applyTheme.ts
import type { YooptaPlugin } from '@yoopta/editor';
import { MyThemeUI } from './index';

type PluginWithUI = YooptaPlugin<any, any>;

export function applyMyTheme(
  plugins: PluginWithUI[],
  extensions?: Record<string, any>,
): PluginWithUI[] {
  return plugins.map((plugin) => {
    const pluginType = plugin.getPlugin.type;
    const ui = MyThemeUI[pluginType];

    if (!ui) return plugin;

    const extension = extensions?.[pluginType];
    const elementsToApply = extension?.elements ?? ui;

    return plugin.extend({
      elements: elementsToApply,
      ...(extension && {
        injectElementsFromPlugins: extension.injectElementsFromPlugins,
        events: extension.events,
        options: extension.options,
      }),
    });
  });
}

4. Use Your Theme

import { applyMyTheme } from './my-theme';
import { Paragraph, HeadingOne } from '@yoopta/plugins';

const themedPlugins = applyMyTheme([Paragraph, HeadingOne]);

const editor = createYooptaEditor({
  plugins: themedPlugins,
  marks: MARKS,
});

Theme Anatomy

A complete theme component includes all element types for a plugin:
// Accordion theme UI
export const AccordionUI = {
  'accordion-list': {
    render: (props) => (
      <div className="accordion-list" {...props.attributes}>
        {props.children}
      </div>
    ),
  },
  'accordion-list-item': {
    render: (props) => {
      const { isExpanded } = props.element.props || {};
      return (
        <details 
          className="accordion-item"
          open={isExpanded}
          {...props.attributes}
        >
          {props.children}
        </details>
      );
    },
  },
  'accordion-list-item-heading': {
    render: (props) => (
      <summary className="accordion-heading" {...props.attributes}>
        {props.children}
      </summary>
    ),
  },
  'accordion-list-item-content': {
    render: (props) => (
      <div className="accordion-content" {...props.attributes}>
        {props.children}
      </div>
    ),
  },
};

Styling Considerations

CSS Classes

Use consistent class naming conventions:
// BEM-style naming
render: (props) => (
  <div 
    className="yoopta-accordion__item"
    data-expanded={props.element.props.isExpanded}
    {...props.attributes}
  >
    {props.children}
  </div>
)

CSS-in-JS

Use styled-components, emotion, or other CSS-in-JS libraries:
import styled from 'styled-components';

const StyledParagraph = styled.p`
  font-size: 16px;
  line-height: 1.5;
  margin: 1rem 0;
  color: ${props => props.theme.text};
`;

export const ParagraphUI = {
  paragraph: {
    render: (props) => (
      <StyledParagraph {...props.attributes}>
        {props.children}
      </StyledParagraph>
    ),
  },
};

Tailwind CSS

Use Tailwind utility classes:
export const ParagraphUI = {
  paragraph: {
    render: (props) => (
      <p 
        className="text-base leading-relaxed my-4 text-gray-900 dark:text-gray-100"
        {...props.attributes}
      >
        {props.children}
      </p>
    ),
  },
};

Theming Mark Components

While themes primarily focus on plugins, you can also style marks:
import { createYooptaMark } from '@yoopta/editor';

const ThemedBold = createYooptaMark({
  type: 'bold',
  hotkey: 'mod+b',
  render: ({ children }) => (
    <strong className="font-bold text-gray-900">
      {children}
    </strong>
  ),
});

const ThemedItalic = createYooptaMark({
  type: 'italic',
  hotkey: 'mod+i',
  render: ({ children }) => (
    <em className="italic text-gray-800">
      {children}
    </em>
  ),
});

const THEMED_MARKS = [ThemedBold, ThemedItalic, /* ... */];

Partial Theme Application

You can apply themes selectively:
import { applyTheme } from '@yoopta/theme-shadcn';
import { Paragraph, HeadingOne, Code } from '@yoopta/plugins';

// Apply theme to some plugins
const themedPlugins = applyTheme([Paragraph, HeadingOne]);

// Use default styling for others
const allPlugins = [...themedPlugins, Code];

const editor = createYooptaEditor({
  plugins: allPlugins,
  marks: MARKS,
});

Responsive Theming

Create responsive theme components:
export const HeadingOneUI = {
  'heading-one': {
    render: (props) => (
      <h1 
        className="
          text-3xl md:text-4xl lg:text-5xl
          font-bold
          my-4 md:my-6 lg:my-8
        "
        {...props.attributes}
      >
        {props.children}
      </h1>
    ),
  },
};

Dark Mode Support

Implement dark mode in your theme:
export const ParagraphUI = {
  paragraph: {
    render: (props) => (
      <p 
        className="
          text-gray-900 dark:text-gray-100
          bg-white dark:bg-gray-800
        "
        {...props.attributes}
      >
        {props.children}
      </p>
    ),
  },
};

Global Theme Styles

Provide global CSS for the editor:
/* my-theme/styles.css */
.yoopta-editor {
  font-family: 'Inter', sans-serif;
  font-size: 16px;
  line-height: 1.5;
  color: #1a1a1a;
}

.yoopta-editor h1 {
  font-size: 2.5rem;
  font-weight: 700;
  margin: 1.5rem 0;
}

.yoopta-editor p {
  margin: 1rem 0;
}

.yoopta-editor code {
  background: #f5f5f5;
  padding: 0.2rem 0.4rem;
  border-radius: 0.25rem;
  font-family: 'Fira Code', monospace;
}

Theme Best Practices

1. Maintain Consistency

Ensure all components follow the same design system:
// Define consistent spacing
const SPACING = {
  xs: '0.25rem',
  sm: '0.5rem',
  md: '1rem',
  lg: '1.5rem',
  xl: '2rem',
};

// Use across components
export const HeadingUI = {
  render: (props) => (
    <h1 style={{ margin: `${SPACING.lg} 0` }} {...props.attributes}>
      {props.children}
    </h1>
  ),
};

2. Preserve Accessibility

Ensure themed components remain accessible:
render: (props) => (
  <button
    className="theme-button"
    aria-label="Toggle accordion"
    {...props.attributes}
  >
    {props.children}
  </button>
)

3. Keep Semantic HTML

Don’t sacrifice semantics for styling:
// Good: Semantic HTML
render: (props) => <h1 {...props.attributes}>{props.children}</h1>

// Bad: Div with heading styles
render: (props) => <div className="h1-styled" {...props.attributes}>{props.children}</div>

4. Document Theme Usage

Provide clear documentation for your theme:
/**
 * My Custom Theme
 * 
 * A modern theme with:
 * - Clean typography
 * - Responsive spacing
 * - Dark mode support
 * 
 * @example
 * ```typescript
 * import { applyMyTheme } from '@my-org/yoopta-theme';
 * 
 * const plugins = applyMyTheme([Paragraph, HeadingOne]);
 * ```
 */
export function applyMyTheme(plugins: PluginWithUI[]): PluginWithUI[] {
  // Implementation
}

5. Test Across Plugins

Ensure your theme works well with all supported plugins:
// Test with all plugins
const ALL_PLUGINS = [
  Paragraph,
  HeadingOne,
  HeadingTwo,
  HeadingThree,
  Blockquote,
  Code,
  Image,
  // ... etc
];

const themedPlugins = applyMyTheme(ALL_PLUGINS);

Next Steps

Plugins

Learn about the plugin system

Elements

Understand element structures

Marks

Style text formatting

Shadcn Theme

Explore the Shadcn theme

Build docs developers (and LLMs) love