Skip to content

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-emoji
npx newtui@latest add custom-emoji
yarn dlx newtui@latest add custom-emoji
bunx newtui@latest add custom-emoji

Install the dependencies

pnpm dlx --yes class-variance-authority
npx --yes class-variance-authority
yarn dlx --yes class-variance-authority
bunx --yes class-variance-authority

Copy 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

PropType
children / default slot?React.ReactNode / Vue slot

Content rendered inside the component.

TypeReact.ReactNode / Vue slot
className / class?string

Additional classes merged with the component defaults.

Typestring

Accessibility

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.