Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
6ed31bc
getCSS - first implementation
ameerf-wix Dec 7, 2025
c8102db
fix build and lint
ameerf-wix Dec 7, 2025
094cbf7
Merge branch 'master' of github.com:wix-incubator/interact into get_css
ameerf-wix Dec 8, 2025
f111288
gitignore
ameerf-wix Dec 8, 2025
9f34f35
wip
ameerf-wix Dec 15, 2025
36fd96e
Merge branch 'master' of github.com:wix-incubator/interact into get_css
ameerf-wix Dec 15, 2025
b117e78
wip
ameerf-wix Dec 17, 2025
72b6f90
Merge branch 'master' of github.com:wix-incubator/interact into get_css
ameerf-wix Dec 18, 2025
8a0c428
wip
ameerf-wix Dec 21, 2025
c030958
wip
ameerf-wix Dec 21, 2025
f6917c1
wip
ameerf-wix Dec 22, 2025
e084981
fixing conditions cascading logic and transitions
ameerf-wix Dec 24, 2025
38f5df3
updated tests
ameerf-wix Dec 24, 2025
1085c50
Merge branch 'master' of github.com:wix-incubator/interact into get_css
ameerf-wix Jan 4, 2026
005a624
PR comments
ameerf-wix Jan 4, 2026
dc008a6
Merge branch 'master' into get_css
ameerabuf Jan 7, 2026
f280704
Merge branch 'get_css' of github.com:wix-incubator/interact into get_css
ameerf-wix Jan 11, 2026
54208db
Merge branch 'master' of github.com:wix-incubator/interact into get_css
ameerf-wix Jan 11, 2026
a41f451
PR fixes
ameerf-wix Jan 11, 2026
29abbe8
PR fixes
ameerf-wix Jan 11, 2026
bb114f4
infra changes
ameerf-wix Jan 11, 2026
1dfe84b
docs and demo - v1
ameerf-wix Jan 11, 2026
a7d644d
docs and demo - v1
ameerf-wix Jan 11, 2026
39eca8e
Merge branch 'master' of github.com:wix-incubator/interact into get_css
ameerf-wix Jan 13, 2026
fe20333
pr fixes
ameerf-wix Jan 20, 2026
fe89e7c
Merge branch 'master' of github.com:wix-incubator/interact into get_css
ameerf-wix Jan 20, 2026
58c50d4
Merge branch 'master' of github.com:wix-incubator/interact into get_css
ameerf-wix Jan 21, 2026
e51a7bc
yarn format
ameerf-wix Jan 21, 2026
85bf50d
Merge branch 'master' of github.com:wix-incubator/interact into get_css
ameerf-wix Jan 22, 2026
e3b7582
wip
ameerf-wix Feb 15, 2026
db95a87
Merge branch 'master' of github.com:wix-incubator/interact into get_css
ameerf-wix Feb 16, 2026
3316716
wip
ameerf-wix Feb 19, 2026
49eb8de
wip
ameerf-wix Mar 2, 2026
fbb2500
Merge branch 'master' of github.com:wix-incubator/interact into get_css
ameerf-wix Mar 26, 2026
c848200
wip
ameerf-wix Mar 28, 2026
82d3a97
wip
ameerf-wix Mar 28, 2026
41ac484
wip
ameerf-wix Mar 28, 2026
57f5a46
wip
ameerf-wix Apr 14, 2026
3860e67
Merge branch 'master' of github.com:wix-incubator/interact into get_css
ameerf-wix Apr 14, 2026
c5c90e3
wip
ameerf-wix Apr 14, 2026
1433546
wip
ameerf-wix Apr 14, 2026
a190b06
wip
ameerf-wix Apr 14, 2026
dbe9dce
wip
ameerf-wix Apr 14, 2026
e7bb50b
wip
ameerf-wix Apr 15, 2026
a5f791f
wip
ameerf-wix Apr 15, 2026
73c2293
wip
ameerf-wix Apr 15, 2026
6c6693d
Merge branch 'master' of github.com:wix-incubator/interact into get_css
ameerf-wix Apr 15, 2026
ca228bd
wip
ameerf-wix Apr 15, 2026
859c9b1
wip
ameerf-wix Apr 15, 2026
f2a1701
wip
ameerf-wix Apr 16, 2026
222de0e
wip
ameerf-wix Apr 16, 2026
bace05a
wip
ameerf-wix Apr 16, 2026
f696caf
wip
ameerf-wix Apr 16, 2026
14654f9
wip
ameerf-wix Apr 16, 2026
514c61e
wip
ameerf-wix Apr 16, 2026
4d52382
Merge branch 'master' into get_css
ameerabuf Apr 16, 2026
04b3822
Merge branch 'get_css' of github.com:wix-incubator/interact into get_css
ameerf-wix Apr 16, 2026
b7e8f5e
wip
ameerf-wix Apr 16, 2026
5d1c6ce
Merge branch 'master' of github.com:wix-incubator/interact into get_css
ameerf-wix Apr 26, 2026
b857beb
some PR fixes
ameerf-wix Apr 26, 2026
474dc38
some PR fixes
ameerf-wix Apr 26, 2026
d2b3ad9
some PR fixes
ameerf-wix Apr 26, 2026
f7787ef
some PR fixes
ameerf-wix Apr 26, 2026
be23525
some PR fixes
ameerf-wix Apr 27, 2026
27c6ae8
some PR fixes
ameerf-wix Apr 27, 2026
b4cb3f0
some PR fixes
ameerf-wix Apr 27, 2026
9cfca48
some PR fixes
ameerf-wix Apr 27, 2026
dc02e70
Merge branch 'master' into get_css
ameerabuf Apr 28, 2026
154ddf8
Merge branch 'get_css' of github.com:wix-incubator/interact into get_css
ameerf-wix May 3, 2026
c540916
Merge branch 'master' of github.com:wix-incubator/interact into get_css
ameerf-wix May 3, 2026
810910c
removing redundant logic
ameerf-wix May 3, 2026
f501a2f
removing redundant logic
ameerf-wix May 4, 2026
f78b145
removing redundant logic
ameerf-wix May 4, 2026
0515019
Merge branch 'master' into get_css
ameerabuf May 4, 2026
34a7a7c
Merge branch 'get_css' of github.com:wix-incubator/interact into get_css
ameerf-wix May 7, 2026
ff98474
Merge branch 'master' of github.com:wix-incubator/interact into get_css
ameerf-wix May 7, 2026
8a47be7
PR fixes
ameerf-wix May 7, 2026
9314a3d
PR fixes
ameerf-wix May 7, 2026
78f803e
PR fixes
ameerf-wix May 7, 2026
ad28114
Merge branch 'master' into get_css
ameerabuf May 7, 2026
f7cac5a
Merge branch 'get_css' of github.com:wix-incubator/interact into get_css
ameerf-wix May 7, 2026
dacdf88
PR fixes
ameerf-wix May 7, 2026
33d9cc3
PR comments
ameerf-wix May 11, 2026
92412a1
PR comments
ameerf-wix May 11, 2026
8e27101
Merge branch 'master' of github.com:wix-incubator/interact into get_css
ameerf-wix May 11, 2026
da25114
PR comments
ameerf-wix May 11, 2026
923e7cf
PR comments
ameerf-wix May 11, 2026
01523dd
PR comments
ameerf-wix May 11, 2026
4a4f5dc
PR comments
ameerf-wix May 11, 2026
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
359 changes: 346 additions & 13 deletions packages/interact/src/core/css.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,350 @@
import { InteractConfig } from '../types';

export function generate(_config: InteractConfig): string {
const css: string[] = [
`@media (prefers-reduced-motion: no-preference) {
[data-interact-initial="true"] > :first-child:not([data-motion-enter="done"]) {
visibility: hidden;
transform: none;
translate: none;
scale: none;
rotate: none;
import type {
InteractConfig,
GetCSSResult,
Effect,
EffectRef,
TimeEffect,
TransitionEffect,
CreateTransitionCSSParams,
Interaction,
Condition,
} from '../types';
import {
createStateRuleAndCSSTransitions,
applySelectorCondition,
generateId,
isTimeTrigger,
shortestRepeatingPatternLength,
getFullPredicateByType,
getSelectorCondition
} from '../utils';
import { getSelector } from './Interact';
import { keyframesToCSS } from './utilities';
import { effectToAnimationOptions } from '../handlers/utilities';
import { getCSSAnimation, MotionKeyframeEffect } from '@wix/motion';

const DEFAULT_INITIAL = {
visibility: 'hidden',
transform: 'none',
translate: 'none',
scale: 'none',
rotate: 'none',
};

interface AnimationProps {
animation: string,
composition: CompositeOperation,
custom: Keyframe,
conditions: string[],
animationCustomPropName: string,
}

interface TransitionProps {
transition: string,
conditions: string[],
transitionCustomPropName: string,
}
interface CSSAnimationResult {
animation: string,
composition: CompositeOperation,
custom: Keyframe,
name: string,
keyframes: Keyframe[],
}

interface CSSTransitionResult {
stateRule: string,
transitions: string[],
}
Comment thread
ameerabuf marked this conversation as resolved.
Outdated

function getTransitionData(
effect: Effect & { key: string },
childSelector: string
): CSSTransitionResult {
const args: CreateTransitionCSSParams = {
key: effect.key,
effectId: (effect as Effect).effectId!,
transition: (effect as TransitionEffect).transition,
properties: (effect as TransitionEffect).transitionProperties,
childSelector,
};
return createStateRuleAndCSSTransitions(args);
}

function getAnimationData(effect: Effect): CSSAnimationResult[] {
const animationOptions = effectToAnimationOptions(effect as TimeEffect);

const cssAnimations = getCSSAnimation(null, animationOptions);

return cssAnimations
.filter((anim) => anim.name)
.map((anim) => ({
animation: anim.animation,
composition: anim.composition || 'replace',
custom: anim.custom || {},
name: anim.name as string,
keyframes: anim.keyframes,
}));
}

function resolveEffect(
effectRef: Effect | EffectRef,
effectsMap: Record<string, Effect>,
interaction: Interaction,
conditionDefinitions: Record<string, Condition>,
): (Effect & { key: string }) | null {
const fullEffect: any = effectRef.effectId
? { ...effectsMap[effectRef.effectId], ...effectRef }
: { ...effectRef };

if (fullEffect.namedEffect || fullEffect.keyframeEffect || fullEffect.transition) {
Comment thread
ameerabuf marked this conversation as resolved.
Outdated
if (!fullEffect.key) {
fullEffect.key = interaction.key;
}

if (interaction.conditions && interaction.conditions.length) {
fullEffect.conditions = [...new Set(...interaction.conditions, ...(fullEffect.conditions || []))]
.filter((condition) => conditionDefinitions[condition]);
}

const { keyframeEffect } = fullEffect;
if (keyframeEffect && !keyframeEffect.name) {
keyframeEffect.name = (effectRef as TimeEffect & { keyframeEffect: MotionKeyframeEffect }).keyframeEffect ?
generateId() : effectRef.effectId;
Comment thread
ameerabuf marked this conversation as resolved.
Outdated
}

fullEffect.initial = fullEffect.initial === 'disable' ?
Comment thread
ameerabuf marked this conversation as resolved.
Outdated
undefined : (fullEffect.initial || DEFAULT_INITIAL);

return fullEffect;
}

return null;
Comment thread
ameerabuf marked this conversation as resolved.
Outdated
}

function buildCascadingTransitionCustomPropRule(
selector: string,
props: TransitionProps,
configConditions: Record<string, Condition>
) {
const { transitionCustomPropName, transition, conditions } = props;

const declaration: string = `${transitionCustomPropName}: ${transition};`;

const selectorCondition = getSelectorCondition(conditions, configConditions);
const targetSelector = selectorCondition ?
applySelectorCondition(selector, selectorCondition) : selector;

let rule = `${targetSelector} { ${declaration} }`;

['container' as const, 'media' as const].forEach((type) => {
const predicate = getFullPredicateByType(conditions, configConditions, type);
if (predicate) {
rule = `@${type} ${predicate} { ${rule} }`;
}
});

return rule;
}

function buildCascadingAnimationCustomPropRule(
Comment thread
ameerabuf marked this conversation as resolved.
Outdated
selector: string,
props: AnimationProps,
configConditions: Record<string, Condition>
) {
const declarations: string[] = [];
const { animationCustomPropName, animation, custom, conditions } = props;

const propsToApply = { [animationCustomPropName]: animation, ...custom };
for (const [key, val] of Object.entries(propsToApply)) {
if (val !== undefined && val !== null) {
declarations.push(`${key}: ${val};`);
}
}

const selectorCondition = getSelectorCondition(conditions, configConditions);
const targetSelector = selectorCondition ?
applySelectorCondition(selector, selectorCondition) : selector;

let rule = `${targetSelector} { ${declarations.join(' ')} }`;
Comment thread
ameerabuf marked this conversation as resolved.
Outdated

['container' as const, 'media' as const].forEach((type) => {
const predicate = getFullPredicateByType(conditions, configConditions, type);
if (predicate) {
rule = `@${type} ${predicate} { ${rule} }`;
}
});

return rule;
}

function buildTransitionRule(
selector: string,
propsArray: TransitionProps[],
): string {
const declarations: string[] = [];

const transitions = propsArray.map((props) => `var(${props.transitionCustomPropName}, _)`);
declarations.push(`transition: ${transitions.join(', ')};`);

return `${selector} { ${declarations.join(' ')} }`;
Comment thread
ameerabuf marked this conversation as resolved.
Outdated
}

function buildAnimationRule(
selector: string,
propsArray: AnimationProps[],
): string {
const declarations: string[] = [];

const animations = propsArray.map((props) => `var(${props.animationCustomPropName}, none)`);
declarations.push(`animation: ${animations.join(', ')};`);

let compositions = propsArray.map((props) => props.composition);
const compositionRepeatLength = shortestRepeatingPatternLength(compositions);
compositions = compositions.slice(0, compositionRepeatLength);

if (compositions.length === 1 && compositions[0] === 'replace') {
compositions = [];
}
}`
];

declarations.push(`animation-composition: ${compositions.join(', ')};`);

return `${selector} { ${declarations.join(' ')} }`;
}

/**
* Generates CSS for time-based animations from an InteractConfig.
*
* @param config - The interact configuration containing effects and interactions
* @returns GetCSSResult with keyframes and animationRules
*/
export function getCSS(config: InteractConfig): GetCSSResult {
Comment thread
ameerabuf marked this conversation as resolved.
Outdated
const keyframeMap = new Map<string, string>();
const selectorTransitionPropsMap = new Map<
string,
TransitionProps[]
>();
const selectorAnimationPropsMap = new Map<
string,
AnimationProps[]
>();
const transitionRules: string[] = [];

const configConditions = config.conditions || {};

for (const interaction of config.interactions) {
if (!isTimeTrigger(interaction.trigger)) {
continue;
}

for (const effectRef of interaction.effects) {
const effect = resolveEffect(effectRef, config.effects, interaction, configConditions);
if (!effect) continue;
Comment thread
ameerabuf marked this conversation as resolved.
Outdated

const childSelector = getSelector(effect, {
Comment thread
ameerabuf marked this conversation as resolved.
Outdated
asCombinator: true, // TODO: (ameerf) - correct?
addItemFilter: Boolean(effect.listItemSelector), // TODO: (ameerf) - correct?
Comment thread
ameerabuf marked this conversation as resolved.
Outdated
});

const escapedKey = CSS.escape(effect.key);
const keyWithNoSpecialChars = effect.key.replace(/[^a-zA-Z0-9_-]/g, '');
Comment thread
ameerabuf marked this conversation as resolved.
Outdated
const selector = `[data-interact-key="${escapedKey}"] ${childSelector}`;
const conditions = (effect.conditions || []);

if (
Comment thread
ameerabuf marked this conversation as resolved.
Outdated
(effect as TransitionEffect).transition ||
(effect as TransitionEffect).transitionProperties
) {
const {stateRule, transitions} = getTransitionData(effect, childSelector);
Comment thread
ameerabuf marked this conversation as resolved.
Outdated
transitionRules.push(stateRule);
if (transitions.length === 0) {
continue;
}
Comment thread
ydaniv marked this conversation as resolved.
Outdated

if (!selectorTransitionPropsMap.has(selector)) {
selectorTransitionPropsMap.set(selector, []);
}
const transitionPropsArray = selectorTransitionPropsMap.get(selector)!;

for (const transition of transitions) {
const transitionCustomPropName = `--trans-def-${keyWithNoSpecialChars}-${transitionPropsArray.length}`;
transitionPropsArray.push({
transition,
conditions,
transitionCustomPropName
});
}
}

if (
Comment thread
ameerabuf marked this conversation as resolved.
Outdated
(effect as any).namedEffect ||
(effect as any).keyframeEffect
) {
const animationDataList = getAnimationData(effect);
if (animationDataList.length === 0) {
continue;
}

if (!selectorAnimationPropsMap.has(selector)) {
selectorAnimationPropsMap.set(selector, []);
}
const animationPropsArray = selectorAnimationPropsMap.get(selector)!;

for (const data of animationDataList) {
const keyframeCSS = keyframesToCSS(data.name, data.keyframes, effect.initial);
if (keyframeCSS) {
keyframeMap.set(data.name, keyframeCSS);
}

const { animation, composition, custom } = data;
const animationCustomPropName = `--anim-def-${keyWithNoSpecialChars}-${animationPropsArray.length}`;

animationPropsArray.push({
animation,
composition,
custom,
conditions,
animationCustomPropName
});
}
}
}
}

for (const [baseSelector, transitionPropsArray] of selectorTransitionPropsMap) {
transitionPropsArray.forEach((transitionProps) => {
transitionRules.push(buildCascadingTransitionCustomPropRule(
baseSelector,
transitionProps,
configConditions
));
});

transitionRules.push(buildTransitionRule(baseSelector, transitionPropsArray));
}

const animationRules: string[] = [];
for (const [baseSelector, animationPropsArray] of selectorAnimationPropsMap) {
animationPropsArray.forEach((animationProps) => {
animationRules.push(buildCascadingAnimationCustomPropRule(
baseSelector,
animationProps,
configConditions
));
});

animationRules.push(buildAnimationRule(baseSelector, animationPropsArray));
}

return {
keyframes: Array.from(keyframeMap.values()),
animationRules,
transitionRules
};
}

export function generate(config: InteractConfig): string {
const { keyframes, animationRules, transitionRules } = getCSS(config);
const css: string[] = [...keyframes, ...animationRules, ...transitionRules];
return css.join('\n');
}
Loading