emoteer

Theming

Override defaults, plug Tailwind tokens, build a custom theme

Emoteer ships a set of styled defaults that you can keep, partially override, or replace entirely. The theme is built on CSS custom properties so it plays nicely with every styling stack — Tailwind, CSS modules, vanilla CSS or styled components.

Using the default theme

Import the prebuilt stylesheet once — plain CSS, no Tailwind needed:

app/layout.tsx
import '@emoteer/react/styles.css';

On Tailwind v4? Import the preset from your main CSS instead, to also get the bg-em-* token utilities:

app/global.css
@import 'tailwindcss';
@import '@emoteer/react/tailwind';

The defaults use a set of CSS variables, prefixed with --em-:

VariablePurposeLight defaultDark default
--em-bgSurface backgroundhsl(0 0% 100%)hsl(0 0% 3.9%)
--em-fgForeground / text colorhsl(0 0% 9%)hsl(0 0% 98%)
--em-mutedSecondary texthsl(215 16% 47%)hsl(0 0% 55%)
--em-borderHairlines, separators, outlineshsl(0 0% 85%)hsl(0 0% 20%)
--em-primaryPrimary interactive color (focus rings, selection, active chips)hsl(0 0% 87%)inherits
--em-hoverSubtle hover backgroundhsl(210 40% 96%)hsl(0 0% 8%)
--em-activeActive / pressed backgroundhsl(210 40% 91%)hsl(0 0% 15%)
--em-shadowDrop shadow base colorhsl(0 0% 0%)inherits
--em-radiusBase border radius0.5reminherits

Each variable falls back to a matching token from your app if you define one — the chain is --em-<name> → --<name> → hardcoded default. For example, setting --primary in your app automatically propagates to --em-primary.

Overriding variables

Scope overrides to any ancestor — the nearest declaration wins.

app/globals.css
:root {
  --em-primary: hsl(85, 75%, 40%);
  --em-radius: 1rem;
}

.dark {
  --em-primary: hsl(85, 80%, 70%);
}

Plugging Tailwind tokens

Map Emoteer variables to the tokens from your own Tailwind theme.

app/globals.css
:root {
  --em-primary: var(--color-primary);
  --em-border: var(--color-border);
  --em-bg: var(--color-background);
  --em-fg: var(--color-foreground);
  --em-muted: var(--color-muted-foreground);
  --em-hover: var(--color-accent);
  --em-active: var(--color-secondary);
}

Per-slot overrides

For the visual properties where it matters — radius, background, border color — each slot exposes its own CSS variable that falls back to the matching global (--em-radius, --em-bg, --em-border). Override one slot without disturbing the rest, from pure CSS, without Tailwind.

Radius

VariableSlot
--em-radius-rootEmoteList.Root outer container
--em-radius-searchEmoteList.Search input
--em-radius-inputEmoteInput
--em-radius-textareaEmoteTextArea
--em-radius-autocomplete-inputEmoteAutocomplete.Input
--em-radius-autocomplete-listEmoteAutocomplete.List popover
--em-radius-reaction-itemReactionButton.Item / ReactionButton.Plus
--em-radius-reaction-chipReactionCounter chips

Background

VariableSlot
--em-bg-rootEmoteList.Root
--em-bg-inputEmoteInput
--em-bg-textareaEmoteTextArea
--em-bg-autocomplete-inputEmoteAutocomplete.Input
--em-bg-autocomplete-listEmoteAutocomplete.List popover
--em-bg-reaction-itemReactionButton.Item / ReactionButton.Plus
--em-bg-reaction-stickerReactionButton.Sticker
--em-bg-reaction-chipReactionCounter chip (inactive)

Border color

VariableSlot
--em-border-rootEmoteList.Root
--em-border-searchEmoteList.Search
--em-border-previewEmoteList.Preview top separator
--em-border-inputEmoteInput
--em-border-textareaEmoteTextArea
--em-border-autocomplete-inputEmoteAutocomplete.Input
--em-border-autocomplete-listEmoteAutocomplete.List popover
--em-border-reaction-itemReactionButton.Item / ReactionButton.Plus
--em-border-reaction-stickerReactionButton.Sticker
--em-border-reaction-chipReactionCounter chip (inactive)

Border width

Slot widths preserve the default hairline look (most at 0.5px). Override --em-border-width to bump every hairline at once, or set a specific slot individually.

VariableSlotDefault
--em-border-widthglobal hairline fallback0.5px
--em-border-width-rootEmoteList.Root1px
--em-border-width-searchEmoteList.Searchinherits
--em-border-width-previewEmoteList.Previewinherits
--em-border-width-inputEmoteInputinherits
--em-border-width-textareaEmoteTextAreainherits
--em-border-width-autocomplete-inputEmoteAutocomplete.Inputinherits
--em-border-width-autocomplete-listEmoteAutocomplete.List1px
--em-border-width-reaction-itemReactionButton.Item / .Plusinherits
--em-border-width-reaction-stickerReactionButton.Sticker2px
--em-border-width-reaction-chipReactionCounter chip1px

Example — mix globals and per-slot overrides

app/globals.css
:root {
  /* Base */
  --em-radius: 0.75rem;
  --em-border: hsl(0 0% 85%);
  --em-border-width: 1px; /* retire the 0.5px hairlines everywhere */

  /* Search as a pill with an accent outline */
  --em-radius-search: 9999px;
  --em-border-search: hsl(85 75% 40%);
  --em-border-width-search: 2px;

  /* Reaction chips sharp and filled */
  --em-radius-reaction-chip: 0;
  --em-bg-reaction-chip: hsl(0 0% 96%);
}

Per-slot className

Every slot also accepts a className, appended to the element as-is (no tailwind-merge, no class soup to fight). Combine it with the prebuilt styles to tweak a slot, or use it on its own in headless mode:

<EmoteList.Root>
  <EmoteList.Search className="my-search" />
  <EmoteList.Tabs />
  <EmoteList.Grid />
</EmoteList.Root>

Headless mode

The markup carries no styling of its own — every element exposes a stable data-scope / data-part hook (and data-* state), so you can style it with any CSS approach.

Import nothing (or only @emoteer/theme/css for the --em-* tokens) and target the contract directly:

app/globals.css
@import '@emoteer/theme/css'; /* optional: just the tokens */

[data-scope='emote-list'][data-part='root'] {
  width: 22rem;
  background: var(--em-bg);
  border: 1px solid var(--em-border);
  border-radius: 1rem;
}
[data-scope='emote-list'][data-part='cell']:hover {
  background: var(--em-hover);
}
[data-scope='emote-list'][data-part='tab'][data-active] {
  font-weight: 700;
}

State is exposed via data-* (data-active, data-favorited, data-disabled, data-loading/data-empty) and ARIA (aria-selected, aria-pressed). Slider, scroll-area and popover internals expose their underlying Zag scopes ([data-scope='slider'], [data-scope='scroll-area'], [data-scope='popover'][data-state='open']). See the component reference for the full part list.

On this page