Getting started
npm install affect-kit
Imports
Each component has its own entry point. Import only what you need:
import 'affect-kit/rater'; // registers <affect-kit-rater> import 'affect-kit/result'; // registers <affect-kit-result> import 'affect-kit/face'; // registers <affect-kit-face> import 'affect-kit/compare'; // registers <affect-kit-compare> // Or register all four at once: import 'affect-kit';
TypeScript types
import type { Rating, EmotionLabel, EmotionName } from 'affect-kit';
import { createRating, averageRatings } from 'affect-kit'; Quick wiring: rater → result
<affect-kit-rater id="rater"></affect-kit-rater>
<affect-kit-result id="result" show-face show-labels color-mode></affect-kit-result>
<script type="module">
import 'affect-kit/rater';
import 'affect-kit/result';
const rater = document.querySelector('affect-kit-rater');
const result = document.querySelector('affect-kit-result');
rater.addEventListener('change', e => result.rating = e.detail);
</script> React
Custom elements work in React 19+ with native support, or React 18 with a thin ref wrapper.
TypeScript types are available via the global HTMLElementTagNameMap augmentation
shipped in each component entry point.
// React 19+
import 'affect-kit/rater';
function App() {
return (
<affect-kit-rater
color-mode
onchange={(e) => console.log(e.detail)}
/>
);
} 2. <affect-kit-rater>
The primary capture interface. The user drags on a V/A pad to set a gut-feeling position —
the face glyph and background color update in real time. On release, 55 emotion chips
from the NRC VAD Lexicon rearrange by proximity to the pad position. The user taps chips
to intensity-rate them at 1, 2, or 3. Every pad release and chip toggle after the first
placement fires a change event with a Rating object.
Attributes / props
| Attribute | Type | Default | Description |
|---|---|---|---|
color-mode | 'background' | 'words' | null | null | How V/A color is applied. 'background' tints the surface; chips adapt their contrast. 'words' paints each chip with its own NRC lexicon color (rainbow constellation). Legacy boolean usage (<... color-mode>) maps to 'background'. |
theme | 'light' | 'dark' | 'auto' | 'light' | Surface theme. 'dark' flips ink to white on a dark panel; mono chips invert (chip fills from a faint white tint up to solid white at level 3, with dark text). 'auto' follows prefers-color-scheme. Orthogonal to color-mode. |
animated | boolean | true | Enables breath + tremor animation on the face glyph. Always respects prefers-reduced-motion: reduce. Set animated="false" in HTML to opt out. |
show-vad | boolean | false | Shows a debug readout of the current V / A / D values below the chip list. |
submit-label | string | 'Done' | Label for the commit button shown once at least one chip is selected. |
Events
| Event | Detail type | When it fires |
|---|---|---|
change | Rating | On every pointer release from the pad, and on every chip toggle after the first pad placement. Not fired on the initial drag before first release. |
commit | Rating | Fired once when the user taps the Done button. Use this as the explicit "I'm finished" signal rather than wiring to every intermediate change. Requires at least one chip to be selected (the button is hidden otherwise). |
Methods
| Method | Signature | Description |
|---|---|---|
setRating() | (rating: Rating) => void | Programmatically pre-fills the rater from a Rating object. Reveals chips immediately. Useful for editing a previously captured rating. |
reset() | () => void | Clears all state back to the initial empty position — pad at center, no chips selected, face neutral. |
CSS custom properties
| Property | Default | Description |
|---|---|---|
--affect-kit-rater-max-width | 640px | Maximum width of the widget. Override to fit narrow or wide containers. |
3. <affect-kit-result>
A display panel for a captured Rating. Renders the selected emotion words
scaled by intensity (level 3 is largest, level 1 is smallest), an optional face glyph,
and an optional color tint. Designed to pair with <affect-kit-rater>
but can also be driven from stored data. The face and color always follow
rating.face (the pre-verbal pad gesture), not the composite label coordinates —
preserving the visual signal of the gut feeling even when labels shift the V/A/D values.
← Rate something on the rater above to see it here, or this is pre-seeded.
Attributes / props
| Attribute / prop | Type | Default | Description |
|---|---|---|---|
rating prop only | Rating | null | null | The rating to display. Set via JavaScript property — not an HTML attribute. The component renders nothing when null. |
show-face | boolean | false | Show the face glyph. Face shape and color follow rating.face, not the composite label V/A. |
show-labels | boolean | true | Show the selected emotion words, scaled continuously by intensity level. |
color-mode | 'background' | 'words' | null | null | 'background' paints a V/A glow on the panel; 'words' colors each word by its own NRC lexicon V/A (background stays neutral — useful when averaging many ratings would wash out a single tint). Legacy boolean attribute maps to 'background'. |
theme | 'light' | 'dark' | 'auto' | 'light' | Surface theme. 'dark' flips ink to white on a dark panel; in color-mode="words", each label switches to a lighter V/A color variant so it stays legible against the dark surface. 'auto' follows prefers-color-scheme. |
layout | 'auto' | 'stack' | 'row' | 'auto' | Preferred face-vs-words orientation. 'auto' uses a container query at 360px. 'stack' keeps face above words at any width. 'row' unlocks side-by-side from 300px, falling back to stacked below the floor rather than overflowing. |
show-vad | boolean | false | Show the raw V / A / D readout (debug). |
align | 'left' | 'center' | 'right' | 'center' | Word alignment within the panel. |
variant | 'default' | 'compact' | 'default' | Sizing preset. 'compact' reduces padding for tight layouts. |
animated | boolean | true | Enables breath animation on the face glyph. Respects prefers-reduced-motion. |
CSS custom properties
| Property | Default | Description |
|---|---|---|
--affect-kit-result-max-width | 640px | Maximum width of the widget. |
--affect-kit-font-size | 1rem | Base font size. Every internal size is em-relative to this — scale the whole widget with one variable. |
--affect-kit-ink | #1a1a1a | Ink color for emotion words. |
Layout custom properties
These pierce the shadow DOM and let a parent container (like
<affect-kit-compare>) drive the inner face/words layout via
container queries without JS:
| Property | Controls |
|---|---|
--_face-dir | flex-direction of the content row |
--_face-align | align-items of the content row |
--_face-gap | Gap between face and words |
--_face-step | Word size growth per intensity level |
--_face-mb | Face zone margin-bottom |
--_face-mt | Face zone margin-top |
4. <affect-kit-face>
A standalone animated face glyph driven directly by v (valence) and
a (arousal) props. Runs a 60 fps animation loop by default — breath
(subtle scale oscillation) and tremor (position noise scaled by arousal). Size is
set via CSS; the SVG fills its container. Used internally by both
<affect-kit-rater> and <affect-kit-result>.
Attributes / props
| Attribute / prop | Type | Default | Description |
|---|---|---|---|
v | number | 0 | Valence ∈ [−1, 1]. Drives brow angle, eye shape, and mouth curve. Clamped silently if out of range. |
a | number | 0 | Arousal ∈ [−1, 1]. Drives eye openness, animation tremor amplitude, and lip parting. Clamped silently. |
animated | boolean | true | Enables breath + tremor animation. Use animated="false" in HTML. Always respects prefers-reduced-motion: reduce regardless of this attribute. |
theme | 'light' | 'dark' | 'auto' | 'light' | Stroke color. 'dark' flips the face to white strokes (intended for dark surfaces). 'auto' follows prefers-color-scheme. Strokes use currentColor internally so consumers can also set color directly. |
motionScale prop only | number | 1 | Scales all animation amplitude in [0, 1]. The rater sets this to ~0.2 while the user is actively dragging so the face stays readable, then restores to 1.0 on release. |
Methods
| Method | Signature | Description |
|---|---|---|
triggerShock() | () => void | Triggers a brief high-frequency shake. Called by <affect-kit-rater> internally when a high-arousal emotion chip is toggled up, so the face reacts to the selection. |
Default size
The element defaults to 120 × 120px (set via :host styles).
Override freely with CSS — the SVG fills 100% of its container:
affect-kit-face { width: 80px; height: 80px; }
/* or inline: */
<affect-kit-face style="width:200px;height:200px"></affect-kit-face> 5. <affect-kit-compare>
A paired side-by-side display for two ratings or two rating arrays. When
color-mode is on, the card background is a gradient from the
left rating's V/A color to the right rating's V/A color — the color transition
itself tells the story. Layout responds to host width via container queries:
side-by-side when wide, stacked when narrow. Each half's face sits on the outer
edge so the faces frame the gradient. The widget makes no claims about what the
comparison means — interpretation belongs to the user.
Attributes / props
| Attribute / prop | Type | Default | Description |
|---|---|---|---|
beforeRating prop only | Rating | null | null | Left side. For time-series, average upstream with averageRatings() and pass the result. Property is beforeRating because before is reserved on Element. |
afterRating prop only | Rating | null | null | Right side. Same shape as beforeRating. |
before-label | string | 'Before' | Caption above the left rating. |
after-label | string | 'After' | Caption above the right rating. |
show-face | boolean | false | Show face glyphs in each half. Forwarded to each inner <affect-kit-result>. |
show-labels | boolean | true | Show emotion words in each half. |
color-mode | 'background' | 'words' | null | null | 'background' paints a card gradient between the two ratings' V/A colors. 'words' drops the gradient and colors each label in each half by its own lexicon V/A — keeps individual labels distinct even when averaging many ratings would otherwise wash the gradient to gray. Legacy boolean attribute maps to 'background'. |
theme | 'light' | 'dark' | 'auto' | 'light' | Surface theme; forwarded to each inner <affect-kit-result>. 'dark' flips the card to dark with light captions; words mode picks lighter V/A variants. 'auto' follows prefers-color-scheme. |
layout | 'auto' | 'stack' | 'row' | 'auto' | Preferred halves orientation. 'auto' uses container queries (720px full content / 380px with face-or-labels off). 'stack' keeps halves vertically stacked at any width. 'row' uses content-aware thresholds (640px full content / 260px light content), falling back to stacked below the floor. |
CSS custom properties
| Property | Default | Description |
|---|---|---|
--affect-kit-compare-max-width | 880px | Maximum width of the widget. |
Comparing time series
For windowed comparisons (last month vs last week), call
averageRatings() upstream and pass the result. Compare takes a
single Rating per side — averaging on every render is a perf
footgun for large arrays, and consumers have the windowing/memoization
context the component doesn't.
import 'affect-kit/compare';
import { averageRatings } from 'affect-kit';
const cmp = document.querySelector('affect-kit-compare');
cmp.beforeRating = averageRatings(lastMonthRatings);
cmp.afterRating = averageRatings(lastWeekRatings);
React consumers: useAverageRatings() in @affect-kit/react
memoizes by array reference for free.
6. TypeScript types
import type { Rating, EmotionLabel, EmotionName } from 'affect-kit';
import { createRating, averageRatings } from 'affect-kit'; Rating
The central data object. Emitted by change and commit events,
consumed by result and compare. Two VAD sources are preserved separately so researchers
can use either or both:
interface Rating {
/** Unix timestamp (ms) at commit time. */
timestamp: number;
/** Pad position that drives the face glyph and color. The pre-verbal gut gesture. */
face: { v: number; a: number };
/** Selected emotion words; each optionally carries its lexicon VAD coordinates. */
labels: EmotionLabel[];
/**
* Intensity-weighted centroid of selected labels' VAD values.
* Novel derivation grounded in affective theory — not independently validated.
* null when no labels were selected.
*/
composite: { v: number; a: number; d: number } | null;
}
// To get a single resolved VAD vector:
// rating.composite ?? { v: rating.face.v, a: rating.face.a, d: 0 } EmotionLabel
interface EmotionLabel {
name: EmotionName; // strict — must be in the validated vocabulary
level: number; // 1 | 2 | 3 from rater; widened to number for averaged data
vad?: { v: number; a: number; d: number };
// ^ Optional per-word NRC VAD Lexicon coordinates (NRC v2.1, Mohammad 2025).
// The rater always emits with vad inline (Rating is a self-describing
// snapshot). For DB persistence, strip vad before insert — it's a
// deterministic function of `name`. Rehydrate on read via
// `rehydrate(rating)` or by reading `EMOTION_LABELS[name]`.
} EMOTION_LABELS
Canonical V/A/D coordinates for every word in the vocabulary, as a plain Record keyed by name. Useful for rehydrating stripped Ratings from a database, or for any consumer code that needs the coordinate of a known emotion without going through the rater.
import { EMOTION_LABELS } from 'affect-kit';
const { v, a, d } = EMOTION_LABELS['joy'];
// → { v: 0.96, a: 0.65, d: 0.59 } (NRC v2.1) stripVad() / rehydrate()
Helpers for the DB-persistence flow. stripVad() drops
the redundant coordinates before insert; rehydrate()
fills them back on read.
import { stripVad, rehydrate } from 'affect-kit';
// Persist
const lean = stripVad(rating); // labels: { name, level }
db.ratings.insert(lean);
// Read
const fromDb = await db.ratings.get(id);
result.rating = rehydrate(fromDb); // labels: { name, level, vad } createRating()
Construct a Rating from a face position and optional labels. Throws on any label name not in the validated vocabulary:
import { createRating } from 'affect-kit';
const r = createRating({
face: { v: 0.5, a: 0.3 }, // pad position (required)
labels: [ // optional
{ name: 'calm', level: 2 },
{ name: 'grateful', level: 1 },
],
}); averageRatings()
Average an array of Rating objects into a single Rating. Label levels are averaged continuously — the result widget renders averaged levels as continuous intermediate sizes:
import { averageRatings } from 'affect-kit';
const avg = averageRatings(sessionRatings); // Rating[] → Rating 7. React wrapper
@affect-kit/react wraps all four components using
@lit/react.
You get typed camelCase props, idiomatic onChange handlers, and correct
property/attribute mapping — without writing any ref plumbing.
Install
npm install affect-kit @affect-kit/react
Import
import { Rater, Result, Face, Compare } from '@affect-kit/react';
import type { Rating } from 'affect-kit'; Usage
import { Rater, Result } from '@affect-kit/react';
import { useState } from 'react';
import type { Rating } from 'affect-kit';
export function RatingWidget() {
const [rating, setRating] = useState<Rating | null>(null);
return (
<>
<Rater colorMode onChange={(e) => setRating(e.detail)} />
{rating && <Result rating={rating} showFace showLabels colorMode />}
</>
);
} Props
All HTML attributes map to camelCase props (color-mode →
colorMode, show-face → showFace, etc.).
Complex properties (rating, beforeRating,
afterRating) are passed directly as object props — no serialisation.
Events
| Component | React prop | Native event | Detail type |
|---|---|---|---|
Rater | onChange | change | CustomEvent<Rating> |
Rater | onCommit | commit | CustomEvent<Rating> |
useAverageRatings()
Hook that wraps averageRatings() with useMemo keyed by
array reference. Useful when passing time-series data to <Compare>,
which takes a single Rating per side — without memoization, a fresh
inline array literal would re-average on every render.
import { Compare, useAverageRatings } from '@affect-kit/react';
function WeekComparison({ lastWeek, thisWeek }) {
const before = useAverageRatings(lastWeek);
const after = useAverageRatings(thisWeek);
return <Compare beforeRating={before} afterRating={after} showFace colorMode="background" />;
} Refs
Pass a ref to access the underlying element and call instance methods:
import { useRef } from 'react';
import { Rater } from '@affect-kit/react';
import type { AffectKitRater } from 'affect-kit/rater';
const ref = useRef<AffectKitRater>(null);
// ref.current.reset() — clears pad + chip selection