Reduced motion
How Kapow reads the platform's reduced-motion setting on each target, what every effect does instead, and how to override it.
Some people turn motion off because it makes them ill or because they find it distracting. Kapow follows that setting on every platform and swaps each effect for a quiet version: a short fade or highlight, with no particles and no movement.
You do not have to call anything. Change and conditional effects check the setting each time they start; transitions check it when they compose.
Where the setting comes from
| Target | Source |
|---|---|
| Android | ValueAnimator.areAnimatorsEnabled() on API 26 and later. Below API 26, Compose’s MotionDurationScale, which is 0 when animations are off. That scale is read on all versions. |
| iOS | Reduce Motion in Accessibility settings. Compose also reports it as a motion duration scale of 0. |
| wasmJs | The prefers-reduced-motion: reduce media query. |
| JVM desktop | No system source. Motion is full unless you override it. |
On Android, both checks matter. Turning “Remove animations” on sets the animator duration scale to 0, and Compose passes that scale to coroutines as MotionDurationScale. Kapow treats a scale of 0 as reduced motion even on versions where the platform call is not available.
What each effect does instead
| Effect | With reduced motion |
|---|---|
spray, rise |
No particles. The element briefly brightens. |
smoke |
Nothing. |
spin |
No rotation. The element briefly brightens. |
pulse |
No rings. The element’s area flashes in the pulse color and fades. |
shine |
No sweep. A brief highlight that fades out. |
glow |
A faint, short halo that does not pulse. |
ripple |
No spreading circle. The element’s shape tints in the ripple color and fades. |
flash |
The same flash, at 35 % strength instead of 85 %. |
burst |
No burst. The element briefly brightens. |
speedLines |
No lines. The element briefly brightens. |
sparkle |
No stars. The element briefly brightens. |
fireworks |
No rocket and no sparks. The element briefly brightens. |
Conditional glow |
The steady halo appears without a fade. |
borderBeam |
The beam stands still: the whole outline is lit evenly. |
marchingAnts |
The dashes stand still. |
rainbowBorder |
The gradient stands still. |
sparkles |
Nothing. |
orbit |
Nothing. |
bubbles |
Nothing. |
flame |
Nothing. |
grayscale |
The element turns gray without a fade. |
spotlight |
The dim appears without a fade. |
confettiPop |
No confetti. The element briefly brightens. |
trace |
Nothing travels. The whole outline appears at once and fades. |
strike |
The line appears whole instead of drawing across, then holds and fades. |
stamp |
No drop, no squash and no dust. The element briefly brightens. |
glitch |
No split and no shifted slices. The element briefly brightens. |
hearts |
One heart fades in and out in place. No small hearts. |
shimmer |
No band travels. The element is lightened evenly while the condition holds. |
breathe |
The element does not move. |
ping |
No growing rings. One faint ring stays still around the element while the condition holds. |
confettiRain |
No pieces fall. |
kapowSkeleton |
The placeholder is still, with no shimmer, and the reveal is a plain cross-fade. |
KapowTypewriter |
The full text shows at once, without a cursor. |
KapowCounter |
The new value appears at once. |
kapowTilt |
The element stays flat and draws no sheen. |
Progress fill |
Drawn at the progress you pass, like every progress effect. |
Progress trace |
Drawn at the progress you pass, like every progress effect. |
Mask transitions: iris, clock, wipe, glare, blinds, dissolve, curtain, shapeIris, checker, tear, blob |
A crossfade over the same duration or shorter, with no mask. |
poof, anvil, pop, shatter, disintegrate |
A fade over the same duration or shorter. No particles, no drop, no scale. |
Feedback calls are not affected. A haptic you asked for still fires under reduced motion.
The effects pages list the reduced behavior of every effect, including repeat, pushDown and the progress effects.
Choosing the behavior per effect
Each effect has its own quiet version, listed above. To choose another one for a single effect, call reducedMotion(mode) on it, on change, conditional and progress effects and on KapowEnter and KapowExit. The ReducedMotionMode values are:
Defaultkeeps the effect’s own fallback. It is what you get without the wrapper.Nothingdraws nothing and sends no work to the frame loop. Feedback events still fire.EndStateshows the effect’s final look at once, without motion. Effects that end at rest draw nothing.Fadefades the final look in and out over a short time instead of moving.
For transitions, Default and Fade fade the content, and Nothing and EndState show or remove it at once, without a delay. For progress effects only Nothing has an effect. The mode applies only while reduced motion is on; with full motion it changes nothing.
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.jantursky.kapow.ChangeEffect
import com.jantursky.kapow.ReducedMotionMode
import com.jantursky.kapow.changeEffect
@Composable
fun Score(score: Int) {
BasicText(
text = "$score",
modifier = Modifier.changeEffect(
score,
ChangeEffect.confettiPop().reducedMotion(ReducedMotionMode.Nothing),
),
)
}
Overriding the setting
LocalKapowMotion overrides the platform for a part of the tree. It has three values:
KapowMotion.System, the default, follows the platform.KapowMotion.Fullalways plays full effects.KapowMotion.Reducedalways plays the reduced version.
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import com.jantursky.kapow.KapowMotion
import com.jantursky.kapow.LocalKapowMotion
@Composable
fun MotionSettings(reduceMotion: Boolean, content: @Composable () -> Unit) {
CompositionLocalProvider(
LocalKapowMotion provides if (reduceMotion) KapowMotion.Reduced else KapowMotion.System,
content = content,
)
}
On JVM desktop there is nothing to follow, so this is the way to offer an in-app switch. Use Reduced in tests to check the quiet path.
If you override to Full, you are playing motion for a user who asked for none. Do it for a preview or a demo, not for the app.
JVM desktop
A desktop window has no system setting to follow, so offer one in your app and feed it to LocalKapowMotion. The sample keeps the choice in a checkbox row and provides it to everything below:
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import com.jantursky.kapow.KapowMotion
import com.jantursky.kapow.LocalKapowMotion
@Composable
fun DesktopApp(content: @Composable () -> Unit) {
// Read this from your own settings store, so the choice survives a restart.
var reduceMotion by remember { mutableStateOf(false) }
Column {
Row(
Modifier.toggleable(
value = reduceMotion,
role = Role.Switch,
onValueChange = { reduceMotion = it },
),
) {
BasicText(if (reduceMotion) "Reduce motion: on" else "Reduce motion: off")
}
CompositionLocalProvider(
LocalKapowMotion provides if (reduceMotion) KapowMotion.Reduced else KapowMotion.System,
content = content,
)
}
}
System is the right default: on the desktop it means full motion today, and it follows the platform wherever a system source exists. A user who turned motion off in an operating system setting cannot be detected on the JVM, which is why the in-app switch matters.
Reading it yourself
isReducedMotion() is a composable that returns the value effects here would use. It resolves LocalKapowMotion first, then the platform. Use it to match your own animations to Kapow’s.
import androidx.compose.runtime.Composable
import com.jantursky.kapow.isReducedMotion
@Composable
fun rememberCardDuration(): Int = if (isReducedMotion()) 0 else 300
Not verified on a device yet
The Android, iOS and browser detection is covered by tests and by running the demo. Reading the Reduce Motion setting on a real iPhone has not been checked, and neither has wasm in Safari.