Custom Emoji
An uploaded emoji that sits on the text baseline and reads as :name:.
An uploaded emoji that sits on the text baseline and reads as :name:. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.
import { CustomEmoji } from "@/registry/bases/newt/ui/custom-emoji"
/* Drawn rather than fetched, so the demo holds up offline. */
const SPARK =
"data:image/svg+xml;utf8," +
encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<circle cx="16" cy="16" r="15" fill="#5865f2"/>
<path d="M16 7l2.6 5.6L24 14l-4 3.9.9 5.6L16 21l-4.9 2.5.9-5.6-4-3.9 5.4-1.4z" fill="#fff"/>
</svg>`
)
export default function CustomEmojiDemo() {
return (
<div className="flex flex-col gap-3 text-[15px] text-newt-text-secondary">
<p>
Ship it <CustomEmoji src={SPARK} name="spark" />
</p>
<p>
<CustomEmoji src={SPARK} name="spark" size="jumbo" />
</p>
</div>
)
}<script setup lang="ts">
import { CustomEmoji } from "~~/registry/bases/newt/ui/custom-emoji"
/* Drawn rather than fetched, so the demo holds up offline. */
const spark =
"data:image/svg+xml;utf8," +
encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<circle cx="16" cy="16" r="15" fill="#5865f2"/>
<path d="M16 7l2.6 5.6L24 14l-4 3.9.9 5.6L16 21l-4.9 2.5.9-5.6-4-3.9 5.4-1.4z" fill="#fff"/>
</svg>`
)
</script>
<template>
<div class="flex flex-col gap-3 text-[15px] text-newt-text-secondary">
<p>Ship it <CustomEmoji :src="spark" name="spark" /></p>
<p><CustomEmoji :src="spark" name="spark" size="jumbo" /></p>
</div>
</template>Installation
pnpm dlx newtui@latest add custom-emojinpx newtui@latest add custom-emojiyarn dlx newtui@latest add custom-emojibunx newtui@latest add custom-emojiInstall the dependencies
pnpm dlx --yes class-variance-authoritynpx --yes class-variance-authorityyarn dlx --yes class-variance-authoritybunx --yes class-variance-authorityCopy the source into your project
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"
import { cn } from "@/lib/utils"
/*
* A custom emoji is an image that has to behave like a letter: it sits on the
* text baseline, it takes the line's colour nowhere, and it carries its name
* as its accessible text so a reader who cannot see it still hears `:name:`.
*
* A message made only of emoji renders them large — that is the `jumbo` size,
* and it is a property of the message, not of the emoji.
*/
const customEmojiVariants = cva("inline-block object-contain align-[-0.2em]", {
variants: {
size: {
inline: "h-[1.375em] w-[1.375em]",
jumbo: "h-12 w-12",
},
},
defaultVariants: { size: "inline" },
})
export interface CustomEmojiProps
extends
Omit<React.ImgHTMLAttributes<HTMLImageElement>, "alt">,
VariantProps<typeof customEmojiVariants> {
/** The name between the colons, without them. */
name: string
}
const CustomEmoji = React.forwardRef<HTMLImageElement, CustomEmojiProps>(
({ className, name, size, ...props }, ref) => (
<img
ref={ref}
/*
* `:name:` rather than the bare name: it is what the author typed, and
* it reads as an emoji rather than as a stray word in the sentence.
*/
alt={`:${name}:`}
title={`:${name}:`}
draggable={false}
className={cn(customEmojiVariants({ size }), className)}
{...props}
/>
)
)
CustomEmoji.displayName = "CustomEmoji"
export { CustomEmoji, customEmojiVariants }<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
import { customEmojiVariants, type CustomEmojiVariants } from "."
const props = withDefaults(
defineProps<{
class?: HTMLAttributes["class"]
src: string
/** The name between the colons, without them. */
name: string
size?: CustomEmojiVariants["size"]
}>(),
{ size: "inline" }
)
</script>
<template>
<!--
`:name:` rather than the bare name: it is what the author typed, and it
reads as an emoji rather than as a stray word in the sentence.
-->
<img
:src="props.src"
:alt="`:${props.name}:`"
:title="`:${props.name}:`"
:draggable="false"
:class="cn(customEmojiVariants({ size: props.size }), props.class)"
/>
</template>import { cva, type VariantProps } from "class-variance-authority"
export { default as CustomEmoji } from "./CustomEmoji.vue"
/*
* A custom emoji is an image that has to behave like a letter: it sits on the
* text baseline and carries its name as its accessible text, so a reader who
* cannot see it still hears `:name:`.
*
* A message made only of emoji renders them large — that is the `jumbo` size,
* and it is a property of the message, not of the emoji.
*/
export const customEmojiVariants = cva(
"inline-block object-contain align-[-0.2em]",
{
variants: {
size: {
inline: "h-[1.375em] w-[1.375em]",
jumbo: "h-12 w-12",
},
},
defaultVariants: { size: "inline" },
}
)
export type CustomEmojiVariants = VariantProps<typeof customEmojiVariants>Update the import paths
The files import @/lib/utils and sibling components through the aliases in your components.json. Point them at wherever those live in your project.
Usage
import { CustomEmoji } from "@/components/ui/custom-emoji"
export function Example() {
return <CustomEmoji />
}<script setup lang="ts">
import { CustomEmoji } from "@/components/ui/custom-emoji"
</script>
<template>
<CustomEmoji />
</template>API reference
CustomEmoji
children / default slot?React.ReactNode / Vue slot
Content rendered inside the component.
React.ReactNode / Vue slotclassName / class?string
Additional classes merged with the component defaults.
stringAccessibility
Preserve the native semantics of the rendered elements and pass through ARIA attributes when the surrounding context needs a label or description. Any interactive child needs a visible focus indicator, and meaning must not rely on colour alone.
Tokens
Custom Emoji reads its colours, radii and motion from the --newt-* tokens installed with the library. Override a token and every component that reads it follows, including this one — see the token layer.