# Styling
Choose batteries included or BYOD (bring your own design, design language and design library): every mode below keeps the same behavior, accessibility and virtualization, and only changes who owns the appearance.
## 1. Two styling modes
### Default picker
`` uses the official appearance and remains visually compatible with v4.
It continues to support the documented v4 CSS custom properties.
`` keeps the supplied composition and behavior while removing decorative appearance from every managed part: control colors/resets, rounding, typography, shadows, blur, motion and decorative icons. Browser native appearance remains. Geometry, positioning, scrolling and presence stay managed. A bare Root behaves the same. Color variables have no effect in these modes because no rule reads them; size variables still apply. `Root appearance="default"` explicitly opts into built-in leaf appearance for token-based re-skins and paints Root with `var(--epr-bg-color)` at zero specificity (no border, radius or shadow). `colorScheme` supplies variables only.
### Structural primitives
`emoji-picker-react/primitives` exposes the same behavioral renderer, unbranded by default:
- every Root applies the geometry tokens (sizes, spacing, stacking) and a zero-specificity `box-sizing: border-box` fallback, so a bare composition lays out and measures correctly with no appearance tokens; design-library classes retain their own box sizing (for example MUI's content-box inputs);
- `` defines the default color tokens (variables only). They take effect with `appearance="default"` or in your own CSS (`background: var(--epr-bg-color)`);
- token presets are exported as data: `structuralPickerTokens`, `lightPickerTokens`, `darkPickerTokens`, `defaultPickerTokens`.
With `List components`, custom emoji buttons receive geometry and behavior attributes without the default button reset, rounding, hover/focus colors or variation indicator decoration. Custom category headers receive sticky positioning and measurement without default background, blur, font styling or text transformation. Supply your own decoration and visible focus. Native `SearchInput` and its design-library `as` component receive no managed search appearance.
Headless composition uses this managed rendering contract; consumers retain the documented geometry while choosing their design components.
## 2. Structural CSS
The library guarantees correct behavior only while these structural responsibilities remain intact:
| Part | Reserved structural responsibility |
| --- | --- |
| `root` | picker-instance containing block where required by overlays |
| `panel` | expanded-region presence/layout used by picker mode transitions |
| `viewport` | vertical scroll container, horizontal clipping and measurement boundary |
| `list` | logical/virtualized grid container |
| `category` | category positioning/measurement boundary |
| `category-content` | grid layout and measured category height |
| `emoji` | measured cell geometry used by logical row/column calculations |
| `variation-picker` | overlay positioning that must not corrupt grid measurement |
The current implementation reserves these declarations:
- Root: `position: relative`, column flex layout, clipping; panel: flexible column with `min-height: 0` and hidden/inert presence controlled by Root.
- Viewport: relative flex child with `overflow-y: scroll` and horizontal clipping.
- List: zero list margin/padding; category content: relative grid using `--epr-emoji-fullsize` columns and `--epr-category-padding`.
- `columns` layouts: only the grid sets Root's width. Search, a horizontal CategoryNav and Preview use inline-size containment and fill the row the grid establishes, so hover/focus titles and an opening skin tone fan never resize the picker or change its column count. A vertical CategoryNav rail still adds its own width beside the grid.
- Cells: measured width/height/max dimensions from `--epr-emoji-fullsize`; the supplied inline absolute position and offsets place virtualized cells.
- Category labels: sticky top position, `--epr-category-label-height`, documented padding and stacking tokens.
Change supported size tokens to alter dimensions; preserve the supplied position styles when wrapping design-library buttons.
Not every value of `display`, `position`, `overflow`, row height or containment is safe to override. For example, forcing `overflow: visible` on Viewport or `display: contents` on a measured grid container breaks keyboard navigation and virtualization. Dimensions that affect layout are either measured from the DOM or exposed as the size tokens above, and the variation menu positions itself inside the Viewport, so you never need to change its overflow.
## 3. Appearance CSS
Consumers may customize appearance through:
- `className`;
- `style`;
- existing documented `--epr-*` variables;
- a deliberately small set of stable `data-epr-part` selectors.
Appearance includes, where it does not invalidate structural assumptions:
- color;
- background;
- border;
- border radius;
- typography;
- decorative shadows;
- icon color;
- hover/focus colors;
- spacing tokens explicitly documented as safe.
## 4. v4 CSS variable inventory
The following variables are already documented public customization surface and remain supported in v5 unless separately deprecated:
### General
- `--epr-emoji-size`
- `--epr-emoji-padding`
- `--epr-bg-color`
- `--epr-text-color`
- `--epr-picker-border-color`
- `--epr-picker-border-radius`
- `--epr-horizontal-padding`
- `--epr-highlight-color`
- `--epr-hover-bg-color`
- `--epr-focus-bg-color`
### Search
- `--epr-search-input-bg-color`
- `--epr-search-input-bg-color-active`
- `--epr-search-input-text-color`
- `--epr-search-input-placeholder-color`
- `--epr-search-border-color`
- `--epr-search-border-color-active`
- `--epr-search-input-border-radius`
- `--epr-search-input-height`
- `--epr-search-icon-color`
### Category navigation
- `--epr-category-navigation-button-size`
- `--epr-category-icon-active-color`
- `--epr-category-icon-inactive-color`
### Category labels
- `--epr-category-label-bg-color`
- `--epr-category-label-text-color`
- `--epr-category-label-height`
### Preview
- `--epr-preview-height`
- `--epr-preview-emoji-size` (v5; default `45px`)
- `--epr-preview-text-size`
- `--epr-preview-text-color`
### Native emoji font
- `--epr-font-family` — font for picker text (`sans-serif` by default). Set it to `inherit` on the picker to use the surrounding application's font. Native emoji glyphs keep their separate font stack.
- `--epr-emoji-font-family` (v5) — font stack for native emojis, e.g. a country-flag polyfill font; native support detection measures this same font.
### Skin tone
- `--epr-skin-tone-picker-menu-color`
- `--epr-skin-tone-size`
- `--epr-skin-tone-neutral-color`, `--epr-skin-tone-light-color`, `--epr-skin-tone-medium-light-color`, `--epr-skin-tone-medium-color`, `--epr-skin-tone-medium-dark-color`, `--epr-skin-tone-dark-color` — the tone swatch colors (defaults `#ffd225`, `#ffdfbd`, `#e9c197`, `#c88e62`, `#a86637`, `#60463a`).
### Dark mode
- `--epr-dark-bg-color`
- `--epr-dark-picker-border-color`
- `--epr-dark-text-color`
- `--epr-dark-search-input-bg-color`
- `--epr-dark-hover-bg-color`
The already-deprecated `--epr-emoji-gap` remains deprecated; do not revive it as a v5 design token.
## 5. Stable part selectors
Expose only parts needed for supported product styling.
Initial required part API:
- `root`
- `panel`
- `reactions`
- `reaction`
- `expand-reactions`
- `search`
- `search-input` (the native input, whether rendered by `Search`, `SearchInput` or a design-library `as` component)
- `search-clear`
- `skin-tone`
- `skin-tone-button`
- `category-nav`
- `category-tab`
- `viewport`
- `list`
- `category`
- `category-label`
- `category-content`
- `emoji`
- `variation-picker`
- `preview`
- `empty` (v5, the `Empty` primitive)
- `loading` (v5, the `Loading` primitive)
- `load-error` (v5, the `LoadError` primitive)
Part names are public API: renaming or removing one is a breaking change.
A part is not automatically a composition primitive. `category-content` and `variation-picker` are internal managed structures with stable styling hooks. Panel and Reactions are exported primitives with those same-named part selectors; see [PRIMITIVES.md](./PRIMITIVES.md#parts-and-replacements).
### Library data attributes
Beyond `data-epr-part`, the library emits a small set of value-carrying data attributes. All of them live in the reserved `data-epr-*` namespace so they can never collide with consumer `data-*` props:
| Attribute | On | Value |
| --- | --- | --- |
| `data-epr-unified` | `[data-epr-part="emoji"]` | lowercase unified code actually rendered, including skin-tone variation |
| `data-epr-category` | `[data-epr-part="category"]` | category id, or the custom group name |
| `data-epr-emojis-per-row` | `[data-epr-part="category-content"]` | measured column count |
| `data-epr-direction` | `[data-epr-part="skin-tone"]` | fan axis (`horizontal` / `vertical`) |
State is exposed through ARIA where ARIA has a word for it: the active category tab is `[data-epr-part="category-tab"][aria-selected="true"]`, a vertical tab bar is `[role="tablist"][aria-orientation="vertical"]`.
With `keepSearchFocus`, the emoji the arrow keys highlight carries `[data-epr-highlighted]` while focus stays in the search input (which names it through `aria-activedescendant`). Give it the same look as keyboard focus; the default appearance does.
These replace v4's undocumented `data-unified`, `data-name` and `data-emojis-per-row`. Like part names, they are public API.
## 6. Emoji item boundary
Emoji cells and category headers can be replaced through `List components={{ Emoji, CategoryHeader }}` (see [API.md](./API.md#9-custom-emoji-cells-and-category-headers)). The library keeps owning their behavior: each component receives the library-owned props (type, role, class, position style, tabIndex, aria-label, `data-epr-*`) and must spread them onto its element. Ordering, virtualization and grid semantics stay library-owned; there is no render prop over the whole list.
### Hiding category titles
Hide titles with `[data-epr-part="category-label"] { display: none }` and set `--epr-category-label-height: 0px`. A hidden title measures 0, so virtualization offsets stay correct.
## 7. Specificity and cascade
Library CSS is unlayered by default, and token declarations are wrapped in `:where()` (zero specificity):
- any consumer selector that sets an `--epr-*` token — plain CSS, CSS Modules, Emotion, styled-components, MUI `styled`/`sx`, Tailwind arbitrary properties — wins regardless of load order;
- because the structural rules are unlayered, global application resets (`* { margin: 0; padding: 0 }`, `button { all: unset }`, …) cannot break the picker's layout — unlayered rules beat any layered reset, and the picker's selectors out-specify bare element resets;
- part overrides (`[data-epr-part="…"]` under your root class) win by ordinary specificity;
- layered frameworks can opt the picker into a layer with `cssLayer="epr"` and declare it first, e.g. Tailwind v4: `@layer epr, theme, base, components, utilities;` before importing Tailwind, so utilities override it. In that mode an unlayered global reset in the app would also override the picker, so keep resets in Tailwind's `base` layer;
- under jsdom, which ignores `@layer` rules, the CSS is always emitted unlayered so test environments keep computed styles.
Rules:
- structural correctness must not depend on Tailwind/CSS Modules being loaded in a particular order;
- cosmetic consumer overrides should win through ordinary cascade without requiring `!important`;
- broad `!important` usage is not needed for cosmetic overrides.
## 8. Structural failures
CSS that breaks the structural rules in section 2 voids the virtualization and keyboard navigation guarantees. Change size tokens or `columns` instead.