Skip to content
Kapow
Esc
↑↓navigate↵open⌘Jpreview
On this page

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:

  • Default keeps the effect’s own fallback. It is what you get without the wrapper.
  • Nothing draws nothing and sends no work to the frame loop. Feedback events still fire.
  • EndState shows the effect’s final look at once, without motion. Effects that end at rest draw nothing.
  • Fade fades 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.Full always plays full effects.
  • KapowMotion.Reduced always 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.

Was this page helpful?