When building a blog with Typecho, people usually rely on the OwO library to add emoji support. Like this:

OwO emoji picker
The OwO library's integration approach fits the traditional frontend development model better, whereas the current open-source Shiro theme is built on NextJS, so we can't simply drop it in. That means we need to build a simplified emoji picker ourselves.
Creating the emoji data file
In OwO, emoji data comes from an OwO.json file. For the Shiro theme, we can create an owo.ts file under src/lib to define the emoji data. In this file, we create an owo object whose structure matches the one in OwO.json, so we can carry over the emoji data from the previous Typecho theme.
export const owo = {
颜文字: {
type: "emoticon",
container: [
{
icon: 'OωO',
text: 'OωO',
},
//...
],
},
Emoji: {
type: "emoji",
container: [
{
icon: '😂',
text: 'Face with Tears of Joy',
},
//...
],
},
图片表情: {
type: "image",
container: [
{
icon: "https://raw.githubusercontent.com/url8/CDN-EMOTE/main/2233/chijing.png",
text: "face1",
},
//...
],
},
};
Adding the emoji button
The Shiro theme uses the Emoji Picker library as its emoji selector:
But this picker scrolls choppily on my computer, so I want to replace it with a custom emoji picker component I wrote myself. And to avoid branch conflicts, I'll also steer clear of modifying the original emoji picker component EmojiPicker.
1. Import the custom emoji picker component:
First, in src\components\modules\comment\CommentBox\UniversalTextArea.tsx, replace the imported EmojiPicker component with our own emoji picker component Stickers:
const Stickers = dynamic(() =>
import('../../shared/Stickers').then((mod) => mod.Stickers),
)
2. Replace the original EmojiPicker component:
In the UniversalTextArea component's return, replace <EmojiPicker onEmojiSelect={handleInsertEmoji} /> with <Stickers handleInsertEmoji={handleInsertEmoji} />:
return (
<TextArea
bordered={false}
wrapperClassName={className}
ref={taRef}
defaultValue={value}
onKeyDown={handleKeyDown}
onChange={(e) => setter("text", e.target.value)}
placeholder={placeholder}
onCmdEnter={(e) => {
e.preventDefault();
sendComment();
}}
>
<CommentBoxSlotPortal>
<>
{!isMobile && (
<FloatPopover
mobileAsSheet
trigger="click"
TriggerComponent={EmojiButton}
headless
popoverClassNames="pointer-events-auto" // [!code --]
popoverClassNames="pointer-events-auto shadow-perfect relative z-[2] rounded-xl border border-zinc-400/20 bg-white/80 p-4 outline-none backdrop-blur-lg dark:border-zinc-500/30 dark:bg-neutral-900/80 max-w-lg" // [!code ++]
>
<EmojiPicker onEmojiSelect={handleInsertEmoji} /> // [!code --]
<Stickers handleInsertEmoji={handleInsertEmoji} /> // [!code ++]
</FloatPopover>
)}
</>
</CommentBoxSlotPortal>
</TextArea>
);
3. Modify the emoji button:
Finally, modify the emoji button at the bottom-left of the text box:
const EmojiButton = () => {
return (
<div className="ml-4 text-base md:text-xs" role="button" tabIndex={0}>
<span>OwO</span>
</div>
)
}
And with that, we've finished modifying the comment box's text area.
The emoji picker component
Next we need to write the emoji picker component Stickers we just imported. This component will handle rendering the emoji picker and responding to user clicks.
Head into the src\components\modules\shared directory and create a new file named Stickers.tsx.
Inside the component, we use useState to manage the current emoji category currentCategory (i.e. 颜文字, Emoji, or any other category), and useMemo to cache the emoji categories and the currently selected emoji data.
One thing to note: since kaomoji vary in length, we need to check whether the current category is 颜文字 and, if so, use a Grid layout to align the elements:
<div
className={`max-h-80 gap-4 overflow-x-hidden overflow-y-scroll pb-4 md:pb-0 ${
currentCategory === '颜文字'
? 'grid grid-cols-4'
: 'flex flex-wrap'
}`}
>
{ ... }
</div>
Otherwise it turns out looking as uneven as this:

Without Grid layout
Here's the complete code for Stickers.tsx:
import { memo, useCallback, useMemo, useState } from 'react'
import type { FC } from 'react'
import { owo } from '~/lib/owo'
type EmojisType = typeof owo
type CategoryKey = keyof EmojisType
export const Stickers: FC<{
handleInsertEmoji: (emoji: string) => void
}> = memo(({ handleInsertEmoji }) => {
const [currentCategory, setCurrentCategory] = useState<CategoryKey>('颜文字')
const emojis: EmojisType = owo
const handleClickEmoji = useCallback(
(emoji: { icon: string; text: any }) => {
if (emoji.icon.startsWith('http')) {
handleInsertEmoji(``)
} else {
handleInsertEmoji(emoji.icon)
}
},
[handleInsertEmoji],
)
const categories = useMemo(
() => Object.keys(emojis) as CategoryKey[],
[emojis],
)
const currentEmojis = useMemo(
() => emojis[currentCategory]?.container || [],
[emojis, currentCategory],
)
return (
<>
{emojis ? (
<>
<div className="mb-2 flex flex-wrap gap-4 text-xs">
{categories.map((category) => (
<button
key={category}
className={`rounded-md px-2 py-1 ${
currentCategory === category ? 'text-accent' : 'opacity-50'
}`}
onClick={() => setCurrentCategory(category)}
>
{category}
</button>
))}
</div>
<div
className={`max-h-80 gap-4 overflow-x-hidden overflow-y-scroll pb-4 md:pb-0 ${
currentCategory === '颜文字'
? 'grid grid-cols-4'
: 'flex flex-wrap'
}`}
>
{currentEmojis.map((item: { icon: string; text: string }) => (
<button
key={item.text}
title={item.text}
onClick={() => handleClickEmoji(item)}
style={{
backgroundImage: item.icon.startsWith('http')
? `url(${item.icon})`
: '',
}}
className={
item.icon.startsWith('http')
? 'h-10 w-10 bg-cover bg-center bg-no-repeat'
: ''
}
>
{!item.icon.startsWith('http') && item.icon}
</button>
))}
</div>
</>
) : (
<div>Loading Emojis...</div>
)}
</>
)
})
Stickers.displayName = 'Stickers'
Finally, just rebuild the project and you can use the emoji picker in Shiro 😊.