DEV Community

Cover image for Create a custom theme editor with HyperTheme
Angelo Libero for Hyperting Srl

Posted on • Updated on

Create a custom theme editor with HyperTheme

In this tutorial you'll learn how to create a custom theme editor and a simple Color Panel Editor.

In this tutorial you'll use:

Prerequisites

You need HyperTheme Editor installed and working on your Chakra UI Project.

If you still not have installed HyperTheme, follow the installation instructions here.

HyperThemeEditor component

In the Installation page we have added the HyperThemeEditor component that gives us a plug&play editor on the page through a Button.

The source code of HyperThemeEditor itself it's very simple as you can see here:

import React, { FC } from 'react'
import {
  ThemeEditor,
  ThemeEditorButton,
  ThemeEditorButtonProps,
  ThemeEditorDrawer,
} from '@hypertheme-editor/chakra-ui-core'
import { ThemeEditorColors } from '@hypertheme-editor/chakra-ui-colors'
import { ThemeEditorFontSizes } from '@hypertheme-editor/chakra-ui-font-sizes'
import { CgColorPicker } from 'react-icons/cg'
import { ImFontSize } from 'react-icons/im'

export type DefaultThemeEditorProps = ThemeEditorButtonProps

export const HyperThemeEditor: FC<DefaultThemeEditorProps> = (props) => {
  return (
    <ThemeEditor>
      <ThemeEditorButton {...props} />
      <ThemeEditorDrawer>
        <ThemeEditorColors icon={CgColorPicker} title="Colors" />
        <ThemeEditorFontSizes icon={ImFontSize} title="Font Sizes" />
      </ThemeEditorDrawer>
    </ThemeEditor>
  )
}
Enter fullscreen mode Exit fullscreen mode

Read more about HyperThemeEditor component here.

Creating a Custom ThemeEditor

Based on the source code of HyperThemeEditor we can create a custom ThemeEditor.

To do so, create a new MyThemeEditor component with this content:

import React, { FC } from 'react'
import {
  ThemeEditor,
  ThemeEditorButton,
  ThemeEditorButtonProps,
  ThemeEditorColors,
  ThemeEditorFontSizes,
} from '@hypertheme-editor/chakra-ui'
import { CgColorPicker } from 'react-icons/cg'
import { BiText } from 'react-icons/bi'

export const MyThemeEditor = (props) => {
  return (
    <ThemeEditor>
      <ThemeEditorButton {...props} />
      <ThemeEditorDrawer>
        <ThemeEditorColors icon={CgColorPicker} title="Colors" />
        <ThemeEditorFontSizes icon={BiText} title="Typography" />
      </ThemeEditorDrawer>
    </ThemeEditor>
  )
}
Enter fullscreen mode Exit fullscreen mode

As you can see, you can change the order of the panels, the icons and the labels.

HyperTheme uses react-icons for rendering the icons on the editor, here you can search for the icon you need.

Read more about: ThemeEditor, ThemeEditorDrawer and ThemeEditorButton.

Creating a custom Panel

Now that we have a custom ThemeEditor component we can start creating our custom editor panel.

Create a new MyColorEditor component:

import React from 'react'
import { Box } from '@chakra-ui/react'

export default function MyCustomPanel(props) {
  return <Box>Testing Custom Panel</Box>
}
Enter fullscreen mode Exit fullscreen mode

Add it as a new Panel to our MyThemeEditor component:

import React, { FC } from 'react'
import {
  ThemeEditor,
  ThemeEditorButton,
  ThemeEditorButtonProps,
  ThemeEditorColors,
  ThemeEditorFontSizes,
} from '@hypertheme-editor/chakra-ui'
import { CgColorPicker } from 'react-icons/cg'
import { BiText } from 'react-icons/bi'
import MyCustomPanel from './MyCustomPanel'

export default function MyThemeEditor(props) {
  return (
    <ThemeEditor>
      <ThemeEditorButton {...props} />
      <ThemeEditorDrawer>
        {/* Add the MyCustomPanel to our theme editor */}
        <MyCustomPanel icon={CgColorPicker} title="My Panel" />
        <ThemeEditorColors icon={CgColorPicker} title="Colors" />
        <ThemeEditorFontSizes icon={BiText} title="Typography" />
      </ThemeEditorDrawer>
    </ThemeEditor>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now you have a custom panel inside the ThemeEditorDrawer component, your theme editor should look like the example below:

function MyCustomPanel(props) {
  return <Box>Testing Custom Panel</Box>
}

function MyThemeEditor(props) {
  return (
    <ThemeEditor>
      <ThemeEditorButton {...props} />
      <ThemeEditorDrawer>
        {/* Add the MyCustomPanel to our theme editor */}
        <MyCustomPanel icon={CgColorPicker} title="My Panel" />
        <ThemeEditorColors icon={CgColorPicker} title="Colors" />
        <ThemeEditorFontSizes icon={BiText} title="Typography" />
      </ThemeEditorDrawer>
    </ThemeEditor>
  )
}

render(<MyThemeEditor />)
Enter fullscreen mode Exit fullscreen mode

useThemeEditor hook

Now that we have a theme editor with a custom editor panel, it's time to start retrieving and live edit the theme.

HyperTheme Editor provides the useThemeEditor hook to:

  • maintaint the current theme state
  • live edit the current theme
  • manage changes history

Check out more on the documentation.

Retrieving and show the current theme

In this section we're creating a ThemeColorBox that shows a color for the theme.

Create a new ThemeColorBox component with this content:

import React from 'react'
import { Box } from '@chakra-ui/react'
import { useThemeEditor } from '@hypertheme-editor/chakra-ui'

export default function ThemeColorBox({ token, paletteIndex = 500, ...props }) {
  const { theme } = useThemeEditor()

  const color = useMemo(() => {
    // in Chakra UI colors could objects
    // with an index (100, 200, 300, etc) or a single color
    if (theme && theme.colors[token]) {
      // if the color is a string, return it
      if (typeof theme.colors[token] === 'string') {
        return theme.colors[token]
        // if it's an object return the current paletteIndex for that color
      } else if (theme.colors[token][paletteIndex]) {
        return theme.colors[token][paletteIndex]
      }
    }

    return 'gray'
  }, [theme, token, paletteIndex])

  return <Box w="40px" h="40px" borderRadius="md" bgColor={color} {...props} />
}
Enter fullscreen mode Exit fullscreen mode

Let's check our new component with some different theme colors, try to change the colors from the editor to check that is working correctly:

<HStack>
  <HyperThemeEditor />
  <ThemeColorBox token="blue" paletteIndex={500} />
  <ThemeColorBox token="red" paletteIndex={500} />
  <ThemeColorBox token="green" paletteIndex={500} />
  <ThemeColorBox token="orange" paletteIndex={500} />
  <ThemeColorBox token="yellow" paletteIndex={500} />
  <ThemeColorBox token="purple" paletteIndex={500} />
</HStack>
Enter fullscreen mode Exit fullscreen mode

Live edit the current theme

It's time to live edit the current theme.

Let's create a SimpleColorEditor component that shows the current color and set a new one through an Input:

import React from 'react'
import { useThemeEditor } from '@hypertheme-editor/chakra-ui'
import { Input } from '@chakra-ui/react'
import { useDebouncyEffect } from 'use-debouncy'
import { colord } from 'colord'

export default function SimpleColorEditor({ token, paletteIndex, ...props }) {
  const { theme, setTheme } = useThemeEditor()
  const [inputValue, setInputValue] = useState<string>(theme.colors[token][paletteIndex] || '')

  const handleOnChange = useCallback((event) => {
    setInputValue(event.target.value)
  }, [])

  // use a debounced effect so the UI is not blocked
  // while the value are changed through the Input
  useDebouncyEffect(
    () => {
      // check that the input color is valid
      if (colord(inputValue).isValid()) {
        // the color token could be a simple color or a palette object,
        // so we have to check it.
        // We also check that the input value differ from
        // the one from the theme, this is necessary
        // for undo/redo functionality to work correctly
        if (typeof theme.colors[token] === 'string' && theme.colors[token] !== inputValue) {
          // live edit the current theme
          setTheme({
            ...theme,
            colors: {
              ...theme.colors,
              [token]: inputValue,
            },
          })
        } else if (
          theme.colors[token][paletteIndex] &&
          theme.colors[token][paletteIndex] !== inputValue
        ) {
          // live edit the current theme
          setTheme({
            ...theme,
            colors: {
              ...theme.colors,
              [token]: {
                ...theme.colors.token,
                [paletteIndex]: inputValue,
              },
            },
          })
        }
      }
    },
    500,
    [inputValue]
  )

  // update internal state if another panel change this value
  useEffect(() => {
    if (
      theme.colors[token] &&
      theme.colors[token][paletteIndex] &&
      theme.colors[token][paletteIndex] !== inputValue
    ) {
      setInputValue(theme.colors[token][paletteIndex])
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [theme.colors, token, paletteIndex])

  return (
    <HStack>
      {/* add the ThemeColorBox we've created before */}
      <ThemeColorBox token={token} paletteIndex={paletteIndex} />
      <Input onChange={handleOnChange} value={inputValue} />
    </HStack>
  )
}
Enter fullscreen mode Exit fullscreen mode

Let's try our new SimpleColorEditor component.

If you change a value (using HEX notation) you should see the new color in current theme.

Compare the edited colors with the value provided by HyperThemeEditor.

<VStack>
  <HyperThemeEditor />
  <SimpleColorEditor token="blue" paletteIndex={500} />
  <SimpleColorEditor token="red" paletteIndex={500} />
  <SimpleColorEditor token="green" paletteIndex={500} />
  <SimpleColorEditor token="orange" paletteIndex={500} />
  <SimpleColorEditor token="yellow" paletteIndex={500} />
  <SimpleColorEditor token="purple" paletteIndex={500} />
</VStack>
Enter fullscreen mode Exit fullscreen mode

Final mounting

Finally, we can mount our SimpleColorEditor into our custom theme editor.

Edit the MyPanel component like this:

import React from 'react'
import { Box } from '@chakra-ui/react'

export default function MyCustomPanel(props) {
  return (
    <VStack>
      <SimpleColorEditor token="blue" paletteIndex={500} />
      <SimpleColorEditor token="red" paletteIndex={500} />
      <SimpleColorEditor token="green" paletteIndex={500} />
      <SimpleColorEditor token="orange" paletteIndex={500} />
      <SimpleColorEditor token="yellow" paletteIndex={500} />
      <SimpleColorEditor token="purple" paletteIndex={500} />
    </VStack>
  )
}
Enter fullscreen mode Exit fullscreen mode

The final result should be like this:

function MyCustomPanel(props) {
  return (
    <VStack>
      <SimpleColorEditor token="blue" paletteIndex={500} />
      <SimpleColorEditor token="red" paletteIndex={500} />
      <SimpleColorEditor token="green" paletteIndex={500} />
      <SimpleColorEditor token="orange" paletteIndex={500} />
      <SimpleColorEditor token="yellow" paletteIndex={500} />
      <SimpleColorEditor token="purple" paletteIndex={500} />
    </VStack>
  )
}

function MyThemeEditor(props) {
  return (
    <ThemeEditor>
      <ThemeEditorButton {...props} />
      <ThemeEditorDrawer>
        {/* Add the MyCustomPanel to our theme editor */}
        <MyCustomPanel icon={CgColorPicker} title="My Panel" />
        <ThemeEditorColors icon={CgColorPicker} title="Colors" />
        <ThemeEditorFontSizes icon={BiText} title="Typography" />
      </ThemeEditorDrawer>
    </ThemeEditor>
  )
}

render(<MyThemeEditor />)
Enter fullscreen mode Exit fullscreen mode

Congratulations! You have created your first working custom panel.

Discussion (2)

Collapse
anudeepreddy profile image
Anudeep Reddy

Are the links alright?

Collapse
angelo_libero profile image
Angelo Libero Author

Sorry for that! now the problem is fixed 👍