Create a custom theme editor with HyperTheme
Tags: react, javascript, showdev, webdev
Published: 2021-09-22 15:53:07


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:

- [`ThemeEditor` component](https://hyperthe.me/documentation/components/ThemeEditor)
- [`ThemeEditorButton` component](https://hyperthe.me/documentation/components/ThemeEditorButton)
- [`ThemeEditorDrawer` component](https://hyperthe.me/documentation/components/ThemeEditorDrawer)
- [`useThemeEditor` hook](https://hyperthe.me/documentation/hooks/useThemeEditor)

## 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](https://hyperthe.me/documentation/getting-started/community).

## HyperThemeEditor component

In the [Installation page](https://hyperthe.me/documentation/getting-started/community) we have added the [`HyperThemeEditor` component](https://hyperthe.me/documentation/components/HyperThemeEditor) that gives us a plug&play editor on the page through a `Button`.

The source code of [`HyperThemeEditor`](https://hyperthe.me/documentation/components/HyperThemeEditor) itself it's very simple as you can see here:

```jsx
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>
  )
}
```

Read more about [`HyperThemeEditor` component here](https://hyperthe.me/documentation/components/HyperThemeEditor).

## Creating a Custom ThemeEditor

Based on the source code of [`HyperThemeEditor`](https://hyperthe.me/documentation/components/HyperThemeEditor) we can create a custom ThemeEditor.

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

<RecoilBugAlert />

```jsx
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>
  )
}
```

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](https://react-icons.github.io/react-icons/search) you can search for the icon you need.

Read more about: [`ThemeEditor`](https://hyperthe.me/documentation/components/ThemeEditor), [`ThemeEditorDrawer`](https://hyperthe.me/documentation/components/ThemeEditorDrawer) and [`ThemeEditorButton`](https://hyperthe.me/documentation/components/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:

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

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

Add it as a new Panel to our `MyThemeEditor` component:

```jsx
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>
  )
}
```

Now you have a custom panel inside the [`ThemeEditorDrawer` component](https://hyperthe.me/documentation/components/), your theme editor should look like the example below:

```jsx
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 />)
```

## 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](https://hyperthe.me/documentation/hooks/useThemeEditor) to:

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

Check out more on the [documentation](https://hyperthe.me/documentation/hooks/useThemeEditor).

## 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:

```jsx
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} />
}
```

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:

```jsx
<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>
```

## 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`:

```tsx
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>
  )
}
```

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`](https://hyperthe.me/documentation/components/HyperThemeEditor).

```jsx
<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>
```

## Final mounting

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

Edit the `MyPanel` component like this:

```jsx
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>
  )
}
```

The final result should be like this:

```jsx
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 />)
```

**Congratulations!** You have created your first working custom panel.