Transitions
Animate widget movement with CSS transitions or springs.
Flexiboards is headless, so widgets jump between cells by default. Set transition on a widget, or on widgetDefaults, to animate the movement instead. cssTransitionConfig() supplies the default durations and easing. These demos use native controls and the example styling. They disable movement when the system requests reduced motion:
Reduced motion is on; widgets move without animation.
<script lang="ts">
import {
FlexiBoard,
FlexiTarget,
FlexiWidget,
cssTransitionConfig,
type FlexiTargetPartialConfiguration
} from '@flexiboards/svelte';
import { onMount } from 'svelte';
let enableTransitions = $state(true);
let reducedMotion = $state(true);
onMount(() => {
const query = window.matchMedia('(prefers-reduced-motion: reduce)');
const update = () => {
reducedMotion = query.matches;
};
update();
query.addEventListener('change', update);
return () => query.removeEventListener('change', update);
});
const targetConfig: FlexiTargetPartialConfiguration = $derived({
layout: { type: 'free', minRows: 2, maxRows: 2, minColumns: 2, maxColumns: 2 },
columnSizing: '100px',
rowSizing: '100px',
widgetDefaults: {
transition: enableTransitions && !reducedMotion ? cssTransitionConfig() : undefined,
className: 'rounded-lg border bg-primary p-4 text-primary-foreground'
}
});
</script>
<label><input type="checkbox" bind:checked={enableTransitions} /> Enable transitions</label>
<p>
{reducedMotion
? 'Reduced motion is on; widgets move without animation.'
: 'Drag A or B to another cell.'}
</p>
<FlexiBoard>
<FlexiTarget key="main" config={targetConfig}>
<FlexiWidget x={0} y={0}>A</FlexiWidget>
<FlexiWidget x={1} y={1}>B</FlexiWidget>
</FlexiTarget>
</FlexiBoard>Enable transitions and drag a widget to an empty cell. The dropped widget animates into place unless reduced motion is enabled. Toggle the system preference while the demo is open to check that it updates.
Choosing a preset
Two presets ship with the library. Each returns a complete transition configuration:
cssTransitionConfig()uses sine in-out for 150ms moves, circ-out for 200ms drops, and ease-out for 150ms resizing.springTransitionConfig()uses response times of 0.20s for moves, 0.24s for drops, and 0.18s for resizing, with a small bounce on drop. These control the spring’s response, rather than a fixed end time.
simpleTransitionConfig() is deprecated. It retains the original 150ms preset, with ease-in-out for moves and ease-out for drops and resizing.
The CSS preset accepts global easing overrides through --ease-flexi-move, --ease-flexi-drop, and --ease-flexi-resize. Compare CSS and spring motion in the registry demo.
Customising transitions
A transition configuration has three optional entries, move, drop, and resize, one per kind of widget movement. Each is either a plain { duration, easing } object (a CSS transition, with duration in milliseconds and any CSS easing function including cubic-bezier()), or an animation adapter:
cssTransition({ duration, easing })is what the plain object resolves to.spring({ duration, bounce })is a dependency-free spring using SwiftUI’s parameterisation:durationin seconds is the response time, andbounceruns from0(critically damped) to1.
Mix them per entry, for example a spring for drop and a CSS transition for resize. Any entry you omit plays no animation. The exact shape is in the FlexiWidgetTransitionConfiguration reference.
The example below drives the move and drop durations from a slider:
Reduced motion is on; widgets move without animation.
<script lang="ts">
import {
FlexiBoard,
FlexiTarget,
FlexiWidget,
cssTransitionConfig,
type FlexiTargetPartialConfiguration
} from '@flexiboards/svelte';
import { onMount } from 'svelte';
let duration = $state(150);
let reducedMotion = $state(true);
onMount(() => {
const query = window.matchMedia('(prefers-reduced-motion: reduce)');
const update = () => {
reducedMotion = query.matches;
};
update();
query.addEventListener('change', update);
return () => query.removeEventListener('change', update);
});
const targetConfig: FlexiTargetPartialConfiguration = $derived({
layout: { type: 'free', minRows: 2, maxRows: 2, minColumns: 2, maxColumns: 2 },
columnSizing: '100px',
rowSizing: '100px',
widgetDefaults: {
transition: !reducedMotion
? { move: { duration, easing: 'ease-in-out' }, drop: { duration, easing: 'ease-out' } }
: undefined,
className: 'rounded-lg border bg-primary p-4 text-primary-foreground'
}
});
</script>
<label
>Transition duration: {duration}ms
<input type="range" min="50" max="500" step="50" bind:value={duration} /></label
>
<p>
{reducedMotion
? 'Reduced motion is on; widgets move without animation.'
: 'Drag A or B to another cell.'}
</p>
<FlexiBoard>
<FlexiTarget key="main" config={targetConfig}>
<FlexiWidget x={0} y={0}>A</FlexiWidget>
<FlexiWidget x={1} y={1}>B</FlexiWidget>
</FlexiTarget>
</FlexiBoard>Gotchas
- Why plain CSS transitions don’t work. Widgets are placed with
grid-rowandgrid-column, which CSS cannot transition. The library measures the before and after boxes and animates a transform between them, so add your transitions through this configuration rather than a stylesheet. - Reduced motion. Transitions are opt-in, so honour
prefers-reduced-motionby leavingtransitionunset when it matches. See Accessibility. - Configuration lives in core. The presets, adapters, and types come from
@flexiboards/coreand are re-exported by each adapter package, so the configuration is identical across frameworks.