Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## @wix/interact-validate

### [Unreleased]

#### Added

- `reducedMotionHandling` accepted on the config, interactions, sequences and effects, with checks that a named effect exists and is of the same kind (`REDUCED_MOTION_EFFECT_NOT_FOUND`, `REDUCED_MOTION_INCOMPATIBLE_EFFECT`) and the `REDUCED_MOTION` rule category (`REDUCED_MOTION_SELF_REFERENCE`, `REDUCED_MOTION_SHADOWED_EFFECT_ID`, `REDUCED_MOTION_ANIMATION_END_SOURCE_MISMATCH`, `REDUCED_MOTION_COLLAPSE_CANCELS`)
- An effect named only by a `reducedMotionHandling` is no longer reported as `UNUSED_EFFECT`

### [0.2.1] - 2026-10-25

#### Added
Expand Down Expand Up @@ -112,6 +119,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## @wix/interact

### [Unreleased]

#### Added

- `reducedMotionHandling` on the config, interactions, sequences and effects — `'ignore'` (default), `'cancel'`, `'collapse'` or an effectId — sets what an effect does under `prefers-reduced-motion: reduce`; the most specific level wins. `generate()` emits both outcomes under exclusive `prefers-reduced-motion` media, and the runtime picks the matching one, without adding effects to the config
- `forceReducedMotion` option for `generate()`, matching `Interact.forceReducedMotion`

#### Changed

- **Breaking:** `Interact.forceReducedMotion` applies the reduced-motion outcome of `reducedMotionHandling` instead of passing `reducedMotion` to `@wix/motion`, so on its own it no longer changes anything. To keep the old behavior, set `reducedMotionHandling: 'collapse'` on the config (finite multi-iteration animations now collapse instead of being dropped)
- **Breaking:** runtime transitions no longer drop their `transition` under `prefers-reduced-motion: reduce`, matching `generate()`. Use `reducedMotionHandling: 'collapse'` to keep it
- `reducedMotion` removed from `InteractOptions`, and the `context` argument from `Interact.getSequence()` / `Interact.addToSequence()`
- 0ms transitions that have a delay are kept, so the delay is honored

### [2.6.0] - 2026-11-02

#### Changed
Expand Down
1 change: 1 addition & 0 deletions apps/website/assets/critical-css-configs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const TILT_UP_OPTS = {

/** Landing page (`index.html`) — viewEnter entrances for FOUC CSS generation. */
export const landingPageConfig = {
reducedMotionHandling: 'collapse',
interactions: [
{
key: 'perf-text',
Expand Down
4 changes: 1 addition & 3 deletions apps/website/assets/main.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,7 @@ const primitiveInteractions = primitivePrefixes.flatMap((prefix) =>

// --- Interact Configuration ---
const config = {
reducedMotionHandling: 'collapse',
conditions: {
Mobile: {
type: 'media',
Expand Down Expand Up @@ -1739,9 +1740,6 @@ updateTunnelBounds();
window.addEventListener('resize', buildGrid);
window.addEventListener('resize', updateTunnelBounds);

// Respect reduced motion settings
Interact.forceReducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;

// Register presets
Interact.registerEffects(presets);

Expand Down
298 changes: 298 additions & 0 deletions docs/reduced-motion-design.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions packages/interact-validate/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ const RULE_CODE_MAP: Record<string, string> = {
FUNCTION_OFFSET_EASING: 'OFFSET_EASING',
NAMED_EFFECT_UNKNOWN_PARAM: 'NAMED_EFFECT_PARAMS',
NAMED_EFFECT_INVALID_PARAM: 'NAMED_EFFECT_PARAMS',
REDUCED_MOTION_SELF_REFERENCE: 'REDUCED_MOTION',
REDUCED_MOTION_COLLAPSE_CANCELS: 'REDUCED_MOTION',
REDUCED_MOTION_ANIMATION_END_SOURCE_MISMATCH: 'REDUCED_MOTION',
REDUCED_MOTION_SHADOWED_EFFECT_ID: 'REDUCED_MOTION',
};

export function finalize(
Expand Down
3 changes: 2 additions & 1 deletion packages/interact-validate/src/schema/effects.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from 'zod';
import { Keyframe, RangeOffset } from './primitives';
import { Keyframe, RangeOffset, ReducedMotionHandling } from './primitives';
import { withPluginFields } from './plugins';

export const StateActionType = z.enum(['add', 'remove', 'toggle', 'clear']);
Expand Down Expand Up @@ -131,6 +131,7 @@ const EffectBase = {
listContainer: z.string().optional(),
listItemSelector: z.string().optional(),
conditions: z.array(z.string().min(1)).optional(),
reducedMotionHandling: ReducedMotionHandling.optional(),
};

export const StateEffect = withPluginFields(
Expand Down
14 changes: 13 additions & 1 deletion packages/interact-validate/src/schema/interactions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from 'zod';
import { Condition } from './primitives';
import { Condition, ReducedMotionHandling } from './primitives';
import {
Effect,
ViewProgressEffect,
Expand All @@ -17,6 +17,7 @@ import { withPluginFields } from './plugins';
import type { Path, SemanticIssue } from '../types';
import { walkConfig } from '../walkConfig';
import { collectSemanticWarnings } from '../semantic';
import { collectReducedMotionIssues, getReducedMotionReferences } from '../semantic/reducedMotion';

export const TriggerType = z.enum([
'hover',
Expand Down Expand Up @@ -58,6 +59,7 @@ const InteractionBase = {
listContainer: z.string().optional(),
listItemSelector: z.string().optional(),
conditions: z.array(z.string().min(1)).optional(),
reducedMotionHandling: ReducedMotionHandling.optional(),
};

const hasEffectsOrSequences = (interaction: { effects?: unknown[]; sequences?: unknown[] }) =>
Expand Down Expand Up @@ -220,6 +222,7 @@ export const InteractConfigSchema = z
effects: z.record(z.string().min(1), Effect).optional(),
sequences: z.record(z.string().min(1), SequenceConfig).optional(),
conditions: z.record(z.string().min(1), Condition).optional(),
reducedMotionHandling: ReducedMotionHandling.optional(),
interactions: z.array(Interaction),
})
.strict()
Expand Down Expand Up @@ -265,6 +268,10 @@ export const InteractConfigSchema = z
}
},
});

collectReducedMotionIssues(config)
.filter(({ severity }) => severity === 'error')
.forEach(({ severity: _, ...issue }) => ctx.addIssue(issue as any));
})
.transform((config) => {
const configEffects = config.effects ?? {};
Expand Down Expand Up @@ -304,6 +311,8 @@ export const InteractConfigSchema = z
},
});

getReducedMotionReferences(config).forEach(Set.prototype.delete, effectIdReferences);

effectIdReferences.forEach((effectId) => {
warnings.push({
code: 'custom',
Expand Down Expand Up @@ -331,6 +340,9 @@ export const InteractConfigSchema = z

// Append the rule-derived semantic warnings/info nudges (A/C/D + animationEnd self-reference).
warnings.push(...collectSemanticWarnings(config));
warnings.push(
...collectReducedMotionIssues(config).filter(({ severity }) => severity !== 'error'),
);
Comment thread
ameerf-wix marked this conversation as resolved.
Resolved

return { ...config, warnings };
});
6 changes: 6 additions & 0 deletions packages/interact-validate/src/schema/primitives.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { z } from 'zod';

// a string other than the enum values is an effectId in config.effects
export const ReducedMotionHandling = z.union([
z.enum(['ignore', 'cancel', 'collapse']),
z.string().min(1),
]);

export const Keyframe = z.record(z.string(), z.union([z.string(), z.number()]));

export const LengthPercentage = z
Expand Down
2 changes: 2 additions & 0 deletions packages/interact-validate/src/schema/sequences.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from 'zod';
import { TimeEffect, TimeEffectRef, TimeTriggerType } from './effects';
import { ReducedMotionHandling } from './primitives';

const SequenceOptionsConfig = {
delay: z.number().int().nonnegative().optional(),
Expand All @@ -10,6 +11,7 @@ const SequenceOptionsConfig = {
triggerType: TimeTriggerType.optional(),
sequenceId: z.string().optional(),
conditions: z.array(z.string().min(1)).optional(),
reducedMotionHandling: ReducedMotionHandling.optional(),
};

export const SequenceConfig = z
Expand Down
Loading
Loading