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'. |
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'. |
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. |
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 | Rating[] | null | null | Left side. A single Rating (rendered as-is) or a Rating[] (averaged via averageRatings()). Property is beforeRating because before is reserved on Element. |
afterRating prop only | Rating | 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'. |
CSS custom properties
| Property | Default | Description |
|---|---|---|
--affect-kit-compare-max-width | 880px | Maximum width of the widget. |
Averaging arrays
Pass a Rating[] to either side and the widget averages it automatically.
Useful for windowed comparisons (e.g. last month vs last week):
import 'affect-kit/compare';
import { createRating } from 'affect-kit';
const cmp = document.querySelector('affect-kit-compare');
cmp.beforeRating = lastMonthRatings; // Rating[]
cmp.afterRating = lastWeekRatings; // Rating[] 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 carrying 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 };
// ^ Per-word NRC VAD Lexicon coordinates (Mohammad 2025).
// Pre-aggregate — direct lookup, identical for every rating that contains
// this name. Aggregation across labels happens only at Rating.composite.
} 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> |
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