logoChisa UI

Command Palette

Search for a command to run...

Componentschevron_rightSlidingNumber

SlidingNumber

An animated number component that slides digits up and down like a mechanical counter.

9:41
signal_cellular_altwifibattery_full
Current ARR:
$
.

Installation

$

Usage

Example.tsx
import { SlidingNumber } from './components/SlidingNumber';
export default function App() {
return (
<SlidingNumber value={123.45} fontStyle={{ fontSize: 32, fontWeight: 'bold' }} />
);
}

Full Component Code

SlidingNumber.tsx
1import React, { useEffect, useState } from 'react';
2import { View, Text, StyleSheet, LayoutChangeEvent } from 'react-native';
3import Animated, { useSharedValue, useAnimatedStyle, withSpring, useDerivedValue } from 'react-native-reanimated';
4
5const Digit = ({ value, place, height, fontStyle }) => {
6 const valueRoundedToPlace = Math.floor(value / place) % 10;
7 const animatedValue = useSharedValue(valueRoundedToPlace);
8
9 useEffect(() => {
10 animatedValue.value = withSpring(valueRoundedToPlace, { stiffness: 280, damping: 18, mass: 0.3 });
11 }, [valueRoundedToPlace]);
12
13 return (
14 <View style={{ height, overflow: 'hidden' }}>
15 <View style={StyleSheet.absoluteFill}>
16 {Array.from({ length: 10 }).map((_, i) => (
17 <Number key={i} mv={animatedValue} number={i} height={height} fontStyle={fontStyle} />
18 ))}
19 </View>
20 <Text style={[styles.text, fontStyle, { opacity: 0 }]}>0</Text>
21 </View>
22 );
23};
24
25const Number = ({ mv, number, height, fontStyle }) => {
26 const translateY = useDerivedValue(() => {
27 const placeValue = mv.value % 10;
28 const offset = (10 + number - placeValue) % 10;
29 let memo = offset * height;
30 if (offset > 5) memo -= 10 * height;
31 return memo;
32 });
33
34 const animatedStyle = useAnimatedStyle(() => ({ transform: [{ translateY: translateY.value }] }));
35
36 return (
37 <Animated.View style={[styles.numberContainer, animatedStyle]}>
38 <Text style={[styles.text, fontStyle]}>{number}</Text>
39 </Animated.View>
40 );
41};
42
43export const SlidingNumber = ({ value, fontStyle, containerStyle, decimalSeparator = '.', padStart = false }) => {
44 const [digitHeight, setDigitHeight] = useState(0);
45 const absValue = Math.abs(value);
46 const [integerPart, decimalPart] = absValue.toString().split('.');
47 const integerValue = parseInt(integerPart, 10);
48 const paddedInteger = padStart && integerValue < 10 ? `0${integerPart}` : integerPart;
49 const integerDigits = paddedInteger.split('');
50 const integerPlaces = integerDigits.map((_, i) => Math.pow(10, integerDigits.length - i - 1));
51
52 return (
53 <View style={[styles.row, containerStyle]}>
54 {value < 0 && <Text style={[styles.text, fontStyle]}>-</Text>}
55 {digitHeight === 0 && <View style={{ position: 'absolute', opacity: 0 }} onLayout={(e) => setDigitHeight(e.nativeEvent.layout.height)}><Text style={[styles.text, fontStyle]}>0</Text></View>}
56 {digitHeight > 0 && (
57 <View style={styles.row}>
58 {integerDigits.map((_, index) => (
59 <Digit key={`pos-${integerPlaces[index]}`} value={integerValue} place={integerPlaces[index]} height={digitHeight} fontStyle={fontStyle} />
60 ))}
61 {decimalPart && (
62 <>
63 <Text style={[styles.text, fontStyle]}>{decimalSeparator}</Text>
64 {decimalPart.split('').map((_, index) => (
65 <Digit key={`decimal-${index}`} value={parseInt(decimalPart, 10)} place={Math.pow(10, decimalPart.length - index - 1)} height={digitHeight} fontStyle={fontStyle} />
66 ))}
67 </>
68 )}
69 </View>
70 )}
71 </View>
72 );
73};
74
75const styles = StyleSheet.create({
76 row: { flexDirection: 'row', alignItems: 'center' },
77 text: { fontSize: 16 },
78 numberContainer: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center', alignItems: 'center' },
79});

Props

NameTypeDefaultDescription
valuenumber-The number to display
fontStyleStyleProp<TextStyle>-Styles for the digits
containerStyleStyleProp<ViewStyle>-Styles for the container
decimalSeparatorstring.Separator char for decimals
padStartbooleanfalsePad integer part with leading zero