react-native-timer-picker
Version:
A simple, flexible, performant duration picker for React Native apps 🔥 Great for timers, alarms and duration inputs ⏰🕰️⏳ Includes iOS-style haptic and audio feedback 🍏
626 lines (538 loc) • 64.2 kB
Markdown
# React Native Timer Picker ⏰🕰️⏳
[]()

[](https://www.npmjs.com/package/react-native-timer-picker)
[](https://www.npmjs.com/package/react-native-timer-picker)
A simple, flexible, performant duration picker component for React Native apps 🔥
Great for timers, alarms and duration inputs.
Works with Expo and bare React Native apps ✅
Includes iOS-style haptic and audio feedback 🍏
- [Demos 📱](#demos-)
- [Installation 🚀](#installation-)
- [Peer Dependencies 👶](#peer-dependencies-)
- [Linear Gradient](#linear-gradient)
- [Masked View](#masked-view)
- [Examples 😎](#examples-)
- [Timer Picker Modal (Dark Mode) 🌚](#timer-picker-modal-dark-mode-)
- [Timer Picker Modal (Light Mode) 🌞](#timer-picker-modal-light-mode-)
- [Timer Picker Modal with Custom Buttons 🎨](#timer-picker-modal-with-custom-buttons-)
- [Timer Picker with Transparent Fade-Out (Dark Mode) 🌒](#timer-picker-with-transparent-fade-out-dark-mode-)
- [Timer Picker with Customisation (Light Mode) 🌔](#timer-picker-with-customisation-light-mode-)
- [Props 💅](#props-)
- [TimerPicker ⏲️](#timerpicker-️)
- [Custom Styles 👗](#custom-styles-)
- [Performance](#performance)
- [Custom FlatList](#custom-flatlist)
- [TimerPickerModal ⏰](#timerpickermodal-)
- [Custom Styles 👕](#custom-styles--1)
- [Methods 🔄](#methods-)
- [TimerPicker](#timerpicker)
- [TimerPickerModal](#timerpickermodal)
- [Picker Feedback 📳🔉](#picker-feedback-)
- [Audio Feedack](#audio-feedack)
- [Haptic Feedback](#haptic-feedback)
- [Feedback Example](#feedback-example)
- [Expo-Specific Audio/Haptic Feedback (DEPRECATED)](#expo-specific-audiohaptic-feedback-deprecated)
- [Contributing 🧑🤝🧑](#contributing-)
- [Dev Setup](#dev-setup)
- [GitHub Guidelines](#github-guidelines)
- [Limitations ⚠](#limitations-)
- [License 📝](#license-)
<br>
## Demos 📱
**Try it out for yourself on [Expo Snack](https://snack.expo.dev/@nuumi/react-native-timer-picker-demo)!** Make sure to run it on a mobile to see it working properly.
<p>
<img src="demos/example1.gif" width="250" height="550" style="margin-right:50px"/>
<img src="demos/example2.gif" width="250" height="550" style="margin-right:50px"/>
<img src="demos/example3.gif" width="250" height="550" />
</p>
<p>
<img src="demos/example4.gif" width="250" height="550" style="margin-right:50px"/>
<img src="demos/example5.gif" width="250" height="550"/>
</p>
<br>
## Installation 🚀
Supports React Native >= 0.72.0 and React >= 18.2.0.
Just run:
```bash
npm install react-native-timer-picker
```
or
```bash
yarn add react-native-timer-picker
```
### Peer Dependencies 👶
This component will work in your React Native Project **_without any peer dependencies_**. However, to enable certain additional features (e.g. fade-out) you will need to supply various libraries as props. These are detailed below.
#### Linear Gradient
If you want the numbers to fade in/out at the top and bottom of the picker, you will need to install either:
- [expo-linear-gradient](https://www.npmjs.com/package/expo-linear-gradient) (if using Expo)
- [react-native-linear-gradient](https://www.npmjs.com/package/react-native-linear-gradient) (if using in a bare React Native project)
**To enable the linear gradient, you need to supply the component as a prop to either TimerPickerModal or TimerPicker.**
#### Masked View
To make the numbers fade in/out on a transparent background (e.g. if the picker is rendered on top of a gradient or image), you will need to install the [@react-native-masked-view/masked-view
](https://www.npmjs.com/package/@react-native-masked-view/masked-view) component. This is as the standard LinearGradient implementation relies on there being a solid background colour. You then just need to set `backgroundColor: "transparent` on the `TimerPicker` styles prop.
`import MaskedView from "@react-native-masked-view/masked-view";`
**To enable the fade-out on a transparent background, you need to supply the imported `MaskedView` component AND one of the LinearGradient components as props to either TimerPickerModal or TimerPicker (see [this example](#timer-picker-with-transparent-fade-out-dark-mode-)).**
<br>
## Examples 😎
### Timer Picker Modal (Dark Mode) 🌚
<img src="demos/example1.gif" width="250" height="550"/>
```jsx
import { TimerPickerModal } from "react-native-timer-picker";
import { LinearGradient } from "expo-linear-gradient"; // or `import LinearGradient from "react-native-linear-gradient"`
....
const [showPicker, setShowPicker] = useState(false);
const [alarmString, setAlarmString] = useState<
string | null
>(null);
const formatTime = ({
hours,
minutes,
seconds,
}: {
hours?: number;
minutes?: number;
seconds?: number;
}) => {
const timeParts = [];
if (hours !== undefined) {
timeParts.push(hours.toString().padStart(2, "0"));
}
if (minutes !== undefined) {
timeParts.push(minutes.toString().padStart(2, "0"));
}
if (seconds !== undefined) {
timeParts.push(seconds.toString().padStart(2, "0"));
}
return timeParts.join(":");
};
return (
<View style={{backgroundColor: "#514242", alignItems: "center", justifyContent: "center"}}>
<Text style={{fontSize: 18, color: "#F1F1F1"}}>
{alarmString !== null
? "Alarm set for"
: "No alarm set"}
</Text>
<TouchableOpacity
activeOpacity={0.7}
onPress={() => setShowPicker(true)}>
<View style={{alignItems: "center"}}>
{alarmString !== null ? (
<Text style={{color: "#F1F1F1", fontSize: 48}}>
{alarmString}
</Text>
) : null}
<TouchableOpacity
activeOpacity={0.7}
onPress={() => setShowPicker(true)}>
<View style={{marginTop: 30}}>
<Text
style={{
paddingVertical: 10,
paddingHorizontal: 18,
borderWidth: 1,
borderRadius: 10,
fontSize: 16,
overflow: "hidden",
borderColor: "#C2C2C2",
color: "#C2C2C2"
}}>
{"Set Alarm 🔔"}
</Text>
</View>
</TouchableOpacity>
</View>
</TouchableOpacity>
<TimerPickerModal
closeOnOverlayPress
LinearGradient={LinearGradient}
modalProps={{
overlayOpacity: 0.2,
}}
modalTitle="Set Alarm"
onCancel={() => setShowPicker(false)}
onConfirm={(pickedDuration) => {
setAlarmString(formatTime(pickedDuration));
setShowPicker(false);
}}
setIsVisible={setShowPicker}
styles={{
theme: "dark",
}}
visible={showPicker}
/>
</View>
)
```
### Timer Picker Modal (Light Mode) 🌞
<img src="demos/example2.gif" width="250" height="550"/>
```jsx
import { TimerPickerModal } from "react-native-timer-picker";
import { LinearGradient } from "expo-linear-gradient"; // or `import LinearGradient from "react-native-linear-gradient"`
....
const [showPicker, setShowPicker] = useState(false);
const [alarmString, setAlarmString] = useState<
string | null
>(null);
const formatTime = ({
hours,
minutes,
seconds,
}: {
hours?: number;
minutes?: number;
seconds?: number;
}) => {
const timeParts = [];
if (hours !== undefined) {
timeParts.push(hours.toString().padStart(2, "0"));
}
if (minutes !== undefined) {
timeParts.push(minutes.toString().padStart(2, "0"));
}
if (seconds !== undefined) {
timeParts.push(seconds.toString().padStart(2, "0"));
}
return timeParts.join(":");
};
return (
<View style={{backgroundColor: "#F1F1F1", alignItems: "center", justifyContent: "center"}}>
<Text style={{fontSize: 18, color: "#202020"}}>
{alarmString !== null
? "Alarm set for"
: "No alarm set"}
</Text>
<TouchableOpacity
activeOpacity={0.7}
onPress={() => setShowPicker(true)}>
<View style={{alignItems: "center"}}>
{alarmString !== null ? (
<Text style={{color: "#202020", fontSize: 48}}>
{alarmString}
</Text>
) : null}
<TouchableOpacity
activeOpacity={0.7}
onPress={() => setShowPicker(true)}>
<View style={{marginTop: 30}}>
<Text
style={{paddingVertical: 10,
paddingHorizontal: 18,
borderWidth: 1,
borderRadius: 10,
fontSize: 16,
overflow: "hidden",
borderColor: "#8C8C8C",
color: "#8C8C8C"
}}>
{"Set Alarm 🔔"}
</Text>
</View>
</TouchableOpacity>
</View>
</TouchableOpacity>
<TimerPickerModal
closeOnOverlayPress
LinearGradient={LinearGradient}
modalTitle="Set Alarm"
onCancel={() => setShowPicker(false)}
onConfirm={(pickedDuration) => {
setAlarmString(formatTime(pickedDuration));
setShowPicker(false);
}}
setIsVisible={setShowPicker}
styles={{
theme: "light",
}}
use12HourPicker
visible={showPicker}
/>
</View>
)
```
### Timer Picker Modal with Custom Buttons 🎨
<img src="demos/example3.gif" width="250" height="550"/>
```jsx
import { TimerPickerModal } from "react-native-timer-picker";
import { LinearGradient } from "expo-linear-gradient"; // or `import LinearGradient from "react-native-linear-gradient"`
import { TouchableOpacity, Text, StyleSheet } from "react-native";
// Custom Button Component
interface CustomButtonProps {
label: string;
onPress?: () => void;
}
const CustomButton: React.FC<CustomButtonProps> = ({ label, onPress }) => {
return (
<TouchableOpacity onPress={onPress} style={styles.customButtonContainer}>
<LinearGradient
style={styles.customButtonGradient}
colors={['#bb2649', '#6c35de']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<Text style={styles.customButtonText}>{label}</Text>
</LinearGradient>
</TouchableOpacity>
);
};
// Styles
const styles = StyleSheet.create({
customButtonContainer: {
marginHorizontal: 5,
},
customButtonGradient: {
borderRadius: 15,
paddingVertical: 12,
paddingHorizontal: 20,
alignItems: 'center',
justifyContent: 'center',
},
customButtonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: '600',
},
});
....
const [showPicker, setShowPicker] = useState(false);
const [alarmString, setAlarmString] = useState<string>("00:00:00");
const formatTime = ({
hours,
minutes,
seconds,
}: {
hours?: number;
minutes?: number;
seconds?: number;
}) => {
const timeParts = [];
if (hours !== undefined) {
timeParts.push(hours.toString().padStart(2, "0"));
}
if (minutes !== undefined) {
timeParts.push(minutes.toString().padStart(2, "0"));
}
if (seconds !== undefined) {
timeParts.push(seconds.toString().padStart(2, "0"));
}
return timeParts.join(":");
};
return (
<View style={{backgroundColor: "#F1F1F1", alignItems: "center", justifyContent: "center"}}>
<Text style={{fontSize: 18, color: "#202020"}}>
{alarmString !== null ? "Alarm set for" : "No alarm set"}
</Text>
<TouchableOpacity
activeOpacity={0.7}
onPress={() => setShowPicker(true)}>
<View style={{alignItems: "center"}}>
{alarmString !== null ? (
<Text style={{color: "#202020", fontSize: 48}}>
{alarmString}
</Text>
) : null}
<TouchableOpacity
activeOpacity={0.7}
onPress={() => setShowPicker(true)}>
<View style={{marginTop: 30}}>
<Text
style={{paddingVertical: 10,
paddingHorizontal: 18,
borderWidth: 1,
borderRadius: 10,
fontSize: 16,
overflow: "hidden",
borderColor: "#8C8C8C",
color: "#8C8C8C"
}}>
Set Alarm 🔔
</Text>
</View>
</TouchableOpacity>
</View>
</TouchableOpacity>
<TimerPickerModal
cancelButton={<CustomButton label="Cancel" />}
closeOnOverlayPress
confirmButton={<CustomButton label="Confirm" />}
LinearGradient={LinearGradient}
modalProps={{
overlayOpacity: 0.2,
}}
modalTitle="Set Alarm"
onCancel={() => setShowPicker(false)}
onConfirm={(pickedDuration) => {
setAlarmString(formatTime(pickedDuration));
setShowPicker(false);
}}
setIsVisible={setShowPicker}
styles={{
theme: "dark",
}}
visible={showPicker}
/>
</View>
)
```
### Timer Picker with Transparent Fade-Out (Dark Mode) 🌒
<img src="demos/example4.gif" width="250" height="550"/>
```jsx
import { TimerPicker } from "react-native-timer-picker";
import MaskedView from "@react-native-masked-view/masked-view"; // for transparent fade-out
import { LinearGradient } from "expo-linear-gradient"; // or `import LinearGradient from "react-native-linear-gradient"`
....
return (
<LinearGradient
colors={["#202020", "#220578"]}
end={{ x: 1, y: 1 }}
start={{ x: 0, y: 0 }}
style={{alignItems: "center", justifyContent: "center"}}>
<TimerPicker
hourLabel=":"
LinearGradient={LinearGradient}
MaskedView={MaskedView}
minuteLabel=":"
padWithNItems={2}
secondLabel=""
styles={{
theme: "dark",
backgroundColor: "transparent",
pickerItem: {
fontSize: 34,
},
pickerLabelContainer: {
marginTop: -4,
right: 0,
left: undefined,
},
pickerLabel: {
fontSize: 32,
},
pickerContainer: {
paddingHorizontal: 50,
},
}}
/>
</LinearGradient>
)
```
### Timer Picker with Customisation (Light Mode) 🌔
<img src="demos/example5.gif" width="250" height="550"/>
```jsx
import { TimerPicker } from "react-native-timer-picker";
import { LinearGradient } from "expo-linear-gradient"; // or `import LinearGradient from "react-native-linear-gradient"`
....
return (
<View style={{backgroundColor: "#F1F1F1", alignItems: "center", justifyContent: "center"}}>
<TimerPicker
hideHours
LinearGradient={LinearGradient}
minuteLabel="min"
padWithNItems={3}
secondLabel="sec"
styles={{
theme: "light",
labelOffsetPercentage: 0,
pickerItem: {
fontSize: 34,
},
pickerLabel: {
fontSize: 26,
},
pickerContainer: {
paddingHorizontal: 50,
},
}}
/>
</View>
)
```
<br>
## Props 💅
### TimerPicker ⏲️
| Prop | Description | Type | Default | Required |
| :---------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------: | :------: |
| onDurationChange | Callback when the duration changes | `(duration: { days: number, hours: number, minutes: number, seconds: number }) => void` | - | false |
| initialValue | Initial value for the picker | `{ days?: number, hours?: number, minutes?: number, seconds?: number }` | - | false |
| hideDays | Hide the days picker | Boolean | true | false |
| hideHours | Hide the hours picker | Boolean | false | false |
| hideMinutes | Hide the minutes picker | Boolean | false | false |
| hideSeconds | Hide the seconds picker | Boolean | false | false |
| daysPickerIsDisabled | Disable the days picker | Boolean | false | false |
| hoursPickerIsDisabled | Disable the hours picker | Boolean | false | false |
| minutesPickerIsDisabled | Disable the minutes picker | Boolean | false | false |
| secondsPickerIsDisabled | Disable the seconds picker | Boolean | false | false |
| dayLimit | Limit on the days it is possible to select | `{ max?: Number, min?: Number }` | - | false |
| hourLimit | Limit on the hours it is possible to select | `{ max?: Number, min?: Number }` | - | false |
| minuteLimit | Limit on the minutes it is possible to select | `{ max?: Number, min?: Number }` | - | false |
| secondLimit | Limit on the seconds it is possible to select | `{ max?: Number, min?: Number }` | - | false |
| maximumDays | The highest value on the days picker | Number | 23 | false |
| maximumHours | The highest value on the hours picker | Number | 23 | false |
| maximumMinutes | The highest value on the minutes picker | Number | 59 | false |
| maximumSeconds | The highest value on the seconds picker | Number | 59 | false |
| dayInterval | The interval between values on the days picker | Number | 1 | false |
| hourInterval | The interval between values on the hours picker | Number | 1 | false |
| minuteInterval | The interval between values on the minutes picker | Number | 1 | false |
| secondInterval | The interval between values on the seconds picker | Number | 1 | false |
| dayLabel | Label for the days picker | String \| React.ReactElement | d | false |
| hourLabel | Label for the hours picker | String \| React.ReactElement | h | false |
| minuteLabel | Label for the minutes picker | String \| React.ReactElement | m | false |
| secondLabel | Label for the seconds picker | String \| React.ReactElement | s | false |
| padDaysWithZero | Pad single-digit days in the picker with a zero | Boolean | false | false |
| padHoursWithZero | Pad single-digit hours in the picker with a zero | Boolean | false | false |
| padMinutesWithZero | Pad single-digit minutes in the picker with a zero | Boolean | true | false |
| padSecondsWithZero | Pad single-digit seconds in the picker with a zero | Boolean | true | false |
| padWithNItems | Number of items to pad the picker with on either side | Number | 1 | false |
| aggressivelyGetLatestDuration | Set to True to ask DurationScroll to aggressively update the latestDuration ref | Boolean | false | false |
| allowFontScaling | Allow font in the picker to scale with accessibility settings | Boolean | false | false |
| use12HourPicker | Switch the hour picker to 12-hour format with an AM / PM label | Boolean | false | false |
| amLabel | Set the AM label if using the 12-hour picker | String | am | false |
| pmLabel | Set the PM label if using the 12-hour picker | String | pm | false |
| repeatDayNumbersNTimes | Set the number of times the list of days is repeated in the picker | Number | 3 | false |
| repeatHourNumbersNTimes | Set the number of times the list of hours is repeated in the picker | Number | 7 | false |
| repeatMinuteNumbersNTimes | Set the number of times the list of minutes is repeated in the picker | Number | 3 | false |
| repeatSecondNumbersNTimes | Set the number of times the list of seconds is repeated in the picker | Number | 3 | false |
| disableInfiniteScroll | Disable the infinite scroll feature | Boolean | false | false |
| LinearGradient | [Linear Gradient Component (required for picker fade-out)](#linear-gradient) | [expo-linear-gradient](https://www.npmjs.com/package/expo-linear-gradient).LinearGradient or [react-native-linear-gradient](https://www.npmjs.com/package/react-native-linear-gradient).default | - | false |
| MaskedView | [Masked View Component (required for picker fade-out on transparent background)](#masked-view) | [@react-native-masked-view/masked-view](https://www.npmjs.com/package/@react-native-masked-view/masked-view).default | - | false |
| FlatList | FlatList component used internally to implement each picker (day, hour, minutes and seconds). More info [below](#custom-flatlist) | [react-native](https://reactnative.dev/docs/flatlist).FlatList | `FlatList` from `react-native` | false |
| pickerFeedback | [Callback for providing audio/haptic feedback](#picker-feedback-) (fired whenever the picker ticks over a value) | `() => void \| Promise<void> ` | - | false |
| Haptics (DEPRECATED) | [Expo Haptics Namespace](#expo-specific-audiohaptic-feedback-deprecated) (please use pickerFeedback instead) | [expo-haptics](https://www.npmjs.com/package/expo-haptics) | - | false |
| Audio (DEPRECATED) | [Expo AV Audio Class](#expo-specific-audiohaptic-feedback-deprecated) | [expo-av](https://www.npmjs.com/package/expo-av).Audio (please use pickerFeedback instead) | - | false |
| clickSoundAsset (DEPRECATED) | Custom sound asset for click sound (please use pickerFeedback instead), was required for offline click sound - download default [here](https://drive.google.com/uc?export=download&id=10e1YkbNsRh-vGx1jmS1Nntz8xzkBp4_I) | require(.../somefolderpath) or {uri: www.someurl} | - | false |
| pickerContainerProps | Props for the picker container | `React.ComponentProps<typeof View>` | - | false |
| pickerGradientOverlayProps | Props for the gradient overlay (supply a different `locations` array to adjust its position) overlays | `Partial<LinearGradientProps>` | - | false |
| styles | Custom styles for the timer picker | [CustomTimerPickerStyles](#custom-styles-) | - | false |
| decelerationRate | Set how quickly the picker decelerates after the user lifts their finger | 'fast', 'normal', or Number | 0.88 | false |
#### Custom Styles 👗
The component should look good straight out of the box, but you can use these styles to make it fit in with your App's theme:
| Style Prop | Description | Type |
| :------------------------------------: | :------------------------------------------------------------------- | :--------------------------------------: |
| theme | Theme of the component | "light" \| "dark" |
| backgroundColor | Main background color | string |
| text | Base text style | TextStyle |
| labelOffsetPercentage | Percentage offset for horizonal label positioning relative to the picker | number |
For deeper style customization, you can supply the following custom styles to adjust the component in any way. These are applied on top of the default styling so take a look at those [styles](src/components/TimerPicker/styles.ts) if something isn't adjusting in the way you'd expect.
| Style Prop | Description | Type |
| :------------------------------------: | :------------------------------------------------------------------- | :--------------------------------------: |
| pickerContainer | Main container for the picker | ViewStyle & { backgroundColor?: string } |
| pickerLabelContainer | Container for the picker's labels | ViewStyle |
| pickerLabel | Style for the picker's labels | TextStyle |
| pickerAmPmContainer | Style for the picker's labels | ViewStyle |
| pickerAmPmLabel | Style for the picker's labels | TextStyle |
| pickerItemContainer | Container for each number in the picker | ViewStyle & { height?: number } |
| pickerItem | Style for each individual picker number | TextStyle |
| disabledPickerItem | Style for any numbers outside any set limits | TextStyle |
| disabledPickerContainer | Style for disabled pickers | ViewStyle |
| pickerGradientOverlay | Style for the gradient overlay (fade out) | ViewStyle |
| durationScrollFlatList | Style for the Flatlist in each picker | ViewStyle |
| durationScrollFlatListContainer | Style for the View that contains the Flatlist in each picker | ViewStyle |
| durationScrollFlatListContentContainer | Style for the Flatlist's `contentContainerStyle` prop in each picker | ViewStyle |
**Note:** There are minor limitations on `pickerContainer.backgroundColor` and `pickerItemContainer.height`. These properties must be simple values (string and number respectively) as they are used in internal calculations for scroll positioning, gradient overlays, and snap behavior. Complex computed values or union types are not supported for these specific properties.
#### Performance
When the `disableInfiniteScroll` prop is not set, the picker gives the appearance of an infinitely scrolling picker by auto-scrolling forward/back when you near the start/end of the list. When the picker auto-scrolls, a momentary flicker is visible if you are scrolling very slowly.
To mitigate for this, the list of numbers in each picker is repeated a given number of times based on the length of the list (7 times for the hours picker, and 3 times for the days/minutes/seconds picker). These have a performance trade-off: higher values mean the picker has to auto-scroll less to maintain the infinite scroll, but has to render a longer list of numbers. The number of repetitions automatically adjusts if the number of items in the picker changes (e.g. if an interval is included, or the maximum value is modified), balancing the trade-off. You can also manually adjust the number of repetitions in each picker with the `repeatHourNumbersNTimes`, `repeatMinuteNumbersNTimes` and `repeatSecondNumbersNTimes` props.
Note that you can avoid the auto-scroll flickering entirely by disabling infinite scroll. You could then set the above props to high values, so that a user has to scroll far down/up the list to reach the end of the list.
#### Custom FlatList
The library offers the ability to provide a custom component for the `<FlatList />`, instead of the default React Native component. This allows for more flexibility and integration with libraries like [react-native-gesture-handler](react-native-gesture-handler) or other components built on top of it, like [https://ui.gorhom.dev/components/bottom-sheet](https://ui.gorhom.dev/components/bottom-sheet).
E.g. if you want to place the timer picker within that bottom-sheet component, the scrolling detection from the bottom-sheet [would interfere](https://ui.gorhom.dev/components/bottom-sheet/troubleshooting#adding-horizontal-flatlist-or-scrollview-is-not-working-properly-on-android) with the one inside the timer picker, but it can be easily solved by providing the `FlatList` component from `react-native-g