Motion
Every sx view moves the same way: Box, RnText, Pressable and Image take entering, exiting, layout and animate, timed by the motion tokens and springs and reduced with the user’s setting. Any of them makes the view a Reanimated one by itself, so there is no animated twin to import; without them it stays a plain view and costs nothing more. The native views take animate as well, run by SwiftUI and Compose.
Arriving and leaving
entering plays when the view mounts, exiting when it is the one removed. A preset is named by the way the view moves, both ways: fadeUp rises into place and floats away upward. Arriving takes fast, leaving quick, both on smoothOut.
| Preset | Arriving | Leaving |
|---|---|---|
fade | fades in | fades out |
fadeUp | rises 12 pt into place | floats 12 pt upward |
fadeDown | settles 12 pt from above | sinks 12 pt |
scale | grows from 92 % | shrinks to 92 % |
<Box entering="fadeUp" exiting="fade">
<OrderShipped />
</Box>
// A preset adjusted: every field a token, but the delay
<Box entering={{preset: 'scale', duration: 'slow', delay: 120}} />
// On a spring: a pop that overshoots and settles
<Box entering={{preset: 'scale', spring: 'bouncy'}} />
// Reanimated's own, as it is
<Box entering={FadeInLeft.springify()} />A preset adjusted takes duration, easing and distance as tokens and delay in milliseconds, or a spring token instead of the duration and the curve. Every view asking for the same one shares a single worklet. Reanimated’s own builders, keyframes and worklets pass through untouched.
Animating a change
animate carries every change of the keys it covers over to the new value instead of jumping: change bg, opacity or offsetY and the view moves there, on the UI thread, through Reanimated’s CSS transitions. Nothing runs in JavaScript while it moves. true takes fast on smoothOut; a duration token, a spring token or a config picks another timing. A spring runs as the curve it draws, sampled into CSS linear().
<Pressable
animate
bg={selected ? 'fill.accent.base' : 'surface.primary'}
borderColor={selected ? 'content.accent' : 'border.tertiary'}
borderWidth={1}
onPress={pick}
/>
<Box animate="slow" offsetY={open ? 0 : 8} opacity={open ? 'o100' : 'o0'} />
// A spring token runs the change on that spring
<Box animate="snappy" scale={liked ? 1.1 : 1} />
// Sizes go through layout on every frame, so they animate only when named
<Box animate={{keys: ['h', 'opacity']}} h={expanded ? 240 : 96} />- Unless told otherwise it animates the colours (
bg,borderColor),opacityand the transform (offsetX,offsetY,rotate,scale): what the compositor moves without laying anything out. - Sizes (
w,h, the minima and maxima),borderRadius,borderWidthandbluranimate whenkeysnames them. - Write both ends:
offsetY={open ? 0 : 8}rather than leaving it out when open, so there is a value to go back to. - On iOS most colours are UIKit’s own, which Reanimated cannot interpolate. kara asks UIKit for the colour it draws in the current appearance and transitions between those, so a selected row blends on both platforms.
Making room
layout moves the view to its new place and size when the layout around it changes: a chip removed before it, a sibling growing. true is the smooth spring, which keeps its speed when another change arrives halfway; a spring token, a timed config or Reanimated’s own transition picks another. Only views with layout move, so give what sits after a changing list layout too, or it jumps while the list glides.
<Box layout>{/* into place on the smooth spring when a sibling comes or goes */}</Box>
<Box layout="bouncy" />
<Box layout={{duration: 'slow', easing: 'inOut'}} />
<Stagger exiting="scale" gap="s8" layout row wrap>
{tags.map(tag => <Tag key={tag.id} tag={tag} />)}
</Stagger>
<Box layout>{/* what sits below follows */}</Box>Springs
What a finger drives moves on a spring, and so does anything that should keep its speed when interrupted: a press let go halfway eases back instead of turning on the spot. kara has four of them, SwiftUI’s set, and each is one curve everywhere it runs. spring(name) is the token as withSpring’s config, timing(size, curve) a duration token as withTiming’s; both leave Reanimated’s own Reduce Motion check off, because kara has honoured the setting already.
import {spring, timing} from '@hzblj/kara-ui'
offset.value = withSpring(0, spring('smooth')) // let go of a drag
pressed.value = withSpring(1, spring('interactive')) // under a finger
progress.value = withTiming(1, timing('fast')) // arriving, on smoothOutPressable presses on springs: down on interactive, back on smooth, neither with a bounce. Keep bouncy for an emphasised moment, never for a control.
On native views
The stacks, Surface, Card, Text and every native view with an sx take animate too, run by the platform: SwiftUI’s .animation(_:value:) on iOS, and on Android animated graphicsLayer values, a background with an animation spec, and animateContentSize for sizes. The same token is the same spring on both. smoothOut is SwiftUI’s .smooth spring and its Compose twin, since neither takes a bézier from @expo/ui; the other curves map to the nearest platform timing.
<Island>
<HStack
animate="snappy"
bg={saved ? 'fill.accent.base' : 'surface.primary'}
onPress={toggle}
p="s16"
scale={saved ? 1 : 0.97}
>
<Text>{saved ? 'Saved for later' : 'Save for later'}</Text>
</HStack>
</Island>- On iOS a change of any covered key animates everything that changes with it in that render, the frame and the background included.
- On Android the colours, the opacity and the transform animate, and the sizes when
keysnames them. A border colour, a radius and a blur change at once; a spring takes no delay.
One after another
`Stagger` plays its children’s entering one step apart, stagger (40 ms) by default. Only the children that arrive together take turns: the first ones, or the next page of a list. A single item added later arrives at once, not after everything above it. A Box, RnText or Pressable child takes the motion itself, with no view added around it.
<Stagger gap="s8">
{lines.map(line => (
<Box bg="surface.primary" borderRadius="medium" key={line.id} p="s16">
<RnText>{line.title}</RnText>
</Box>
))}
</Stagger>
<Stagger entering="scale" from="end" interval="micro">{sizes}</Stagger>With Reanimated of your own
An animated style from useAnimatedStyle makes a Box or an RnText an animated view too, as do animatedProps. For a Reanimated view of your own, useEntering, useExiting and useLayoutTransition hand over kara’s presets, reduced, and timing(size, curve) is a withTiming config from the tokens.
import {Box, timing, useEntering} from '@hzblj/kara-ui'
import {useEffect} from 'react'
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'
const Progress = ({value}: {value: number}) => {
const fill = useSharedValue(value)
useEffect(() => {
fill.value = withTiming(value, timing('fast'))
}, [fill, value])
const style = useAnimatedStyle(() => ({transform: [{scaleX: fill.value}]}))
// An animated style makes the Box an Animated.View by itself
return <Box bg="fill.accent.primary" h={4} style={style} />
}
// kara's presets on a Reanimated view of your own
const Photo = () => <Animated.Image entering={useEntering('fade')} source={photo} />A view picks Reanimated when it mounts and keeps it, because changing the component later would remount everything under it. One whose animated style or motion arrives after mount takes animated from the start; development builds warn when it is missing.
Reduce Motion
| Motion | With motion reduced | |
|---|---|---|
entering, exiting | the preset | every preset fades |
animate | colours, opacity and the transform | the colours and the opacity blend; the rest arrives at once |
layout | moves into place on a spring | snaps |
Stagger | one after another | fades, one after another |
The setting is followed live, and KaraProvider reduceMotion overrides it, as for every component. Reanimated builders of your own follow the device’s setting themselves.
The props
| Prop | Type | Description |
|---|---|---|
| animate | SxAnimate | Carries every change to the keys it covers over to the new value, on the UI thread: change |
| animated | boolean | Renders through Reanimated from the first frame. A view decides when it mounts, so this is for one whose animated |
| entering | SxEntryExit | How the view arrives when it mounts. A preset ( |
| exiting | SxEntryExit | How the view leaves when it is the one removed. Takes what |
| layout | SxLayout | Animates the view to its new place and size when the layout around it changes: |
Carries every change to the keys it covers over to the new value, on the UI thread: change opacity, bg or offsetY and the box moves there instead of jumping. Reanimated's CSS transitions underneath. With motion reduced only the colours and opacity animate; the rest arrives at once.
Renders through Reanimated from the first frame. A view decides when it mounts, so this is for one whose animated style or motion arrives later. An animated style present at mount is noticed without it.
How the view arrives when it mounts. A preset ('fadeUp'), a preset adjusted, or Reanimated's own builder.
How the view leaves when it is the one removed. Takes what entering does.
Animates the view to its new place and size when the layout around it changes: true on the smooth spring, or a spring token, a timed config or Reanimated's own. kara's are off with motion reduced.