A biblioteca definitiva para Bottom Sheets, Modals e overlays no React Native. Moderna, elegante e 100% personalizável.
"Velvet UI torna cada interação suave como veludo" 🌟
Construída em 🇲🇿 Moçambique para todo o mundo 🌍.
- ✨ 60+ animações pré-definidas com physics avançadas
- 🎨 100% personalizável - cores, tamanhos, comportamentos
- 📱 Gestos nativos com haptic feedback
- 🚀 Performance otimizada com Reanimated 3
- 💎 Design moderno seguindo Material 3 e iOS guidelines
- 🔧 TypeScript completo com intellisense
- 🌍 Suporte completo para iOS e Android
- 🎭 Temas dinâmicos com modo escuro automático
# npm
npm install react-native-velvet-ui
# yarn
yarn add react-native-velvet-ui
# pnpm
pnpm add react-native-velvet-ui# Peer dependencies necessárias
npm install react-native-reanimated react-native-gesture-handler react-native-haptic-feedbackcd ios && pod installAdicione ao MainActivity.java:
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;import { VelvetProvider } from 'react-native-velvet-ui';
export default function App() {
return (
<VelvetProvider>
<YourApp />
</VelvetProvider>
);
}import { VelvetBottomSheet } from 'react-native-velvet-ui';
function MyComponent() {
const [visible, setVisible] = useState(false);
return (
<>
<Button title="Abrir Bottom Sheet" onPress={() => setVisible(true)} />
<VelvetBottomSheet
visible={visible}
onClose={() => setVisible(false)}
animation="slideUp"
haptics="light"
>
<Text>Conteúdo do Bottom Sheet</Text>
</VelvetBottomSheet>
</>
);
}import { VelvetModal } from 'react-native-velvet-ui';
<VelvetModal
visible={visible}
onClose={() => setVisible(false)}
animation="zoomBounce"
blur={15}
theme="dark"
>
<Text>Modal content here</Text>
</VelvetModal><VelvetBottomSheet
visible={true}
onClose={() => {}}
// Animações
animation="slideUp" | "fadeSlide" | "bounceUp" | "elasticSlide"
duration={300}
easing="bezier(0.25, 0.46, 0.45, 0.94)"
// Tamanhos
height="auto" | 400 | "50%" | "80%"
snapPoints={['25%', '50%', '80%']}
// Comportamento
dismissible={true}
closeOnBackdrop={true}
closeOnSwipe={true}
// Estilo
theme="light" | "dark" | "auto"
backgroundColor="#ffffff"
cornerRadius={20}
// Haptics
haptics="light" | "medium" | "heavy"
>
{/* Conteúdo */}
</VelvetBottomSheet><VelvetModal
visible={true}
onClose={() => {}}
// Animações
animation="fade" | "scale" | "slideDown" | "zoomBounce"
duration={250}
// Backdrop
blur={10}
backdropColor="rgba(0,0,0,0.5)"
// Posicionamento
position="center" | "top" | "bottom"
margin={20}
// Comportamento
dismissible={true}
closeOnBackdrop={true}
>
{/* Conteúdo */}
</VelvetModal><VelvetActionSheet
visible={true}
onClose={() => {}}
title="Escolha uma opção"
options={[
{
title: "Editar",
icon: "edit",
onPress: () => {},
style: "default"
},
{
title: "Excluir",
icon: "trash",
onPress: () => {},
style: "destructive"
}
]}
cancelButton={{
title: "Cancelar",
style: "cancel"
}}
/>slideUp- Desliza de baixo para cimafadeSlide- Fade + slide suavebounceUp- Bounce elásticoelasticSlide- Slide com elastic easingspring- Spring physics natural
fade- Fade simplesscale- Scale do centrozoomBounce- Zoom com bounceflipX/flipY- Flip 3Drotate- Rotação 360°glitch- Efeito glitch
import { createVelvetTheme, VelvetProvider } from 'react-native-velvet-ui';
const customTheme = createVelvetTheme({
colors: {
primary: '#007AFF',
secondary: '#5856D6',
success: '#34C759',
light: {
background: '#FFFFFF',
surface: '#F2F2F7',
text: '#000000',
},
dark: {
background: '#000000',
surface: '#1C1C1E',
text: '#FFFFFF',
}
},
spacing: {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32
}
});
export default function App() {
return (
<VelvetProvider theme={customTheme}>
<YourApp />
</VelvetProvider>
);
}import { velvetThemes } from 'react-native-velvet-ui';
// Temas disponíveis
velvetThemes.light
velvetThemes.dark
velvetThemes.mozambique // Verde, amarelo, vermelho
velvetThemes.material // Material Design 3
velvetThemes.ios // iOS style
velvetThemes.nature // Verde natureza
velvetThemes.ocean // Azul oceanoimport { useVelvetBottomSheet } from 'react-native-velvet-ui';
function MyComponent() {
const {
show,
hide,
isVisible,
height,
setHeight,
snapToPoint
} = useVelvetBottomSheet();
const showSheet = () => {
show({
height: '50%',
animation: 'bounceUp',
content: <MySheetContent />
});
};
return (
<Button title="Show Sheet" onPress={showSheet} />
);
}import { useVelvetModal } from 'react-native-velvet-ui';
function MyComponent() {
const { show, hide, isVisible } = useVelvetModal();
const showModal = () => {
show({
animation: 'zoomBounce',
blur: 15,
content: <MyModalContent />
});
};
return <Button title="Show Modal" onPress={showModal} />;
}import { useVelvetHaptics } from 'react-native-velvet-ui';
function MyComponent() {
const { play, buttonPress, notification } = useVelvetHaptics();
return (
<TouchableOpacity
onPress={() => {
buttonPress('primary');
// Sua lógica aqui
}}
>
<Text>Botão com Haptic</Text>
</TouchableOpacity>
);
}<VelvetButton
title="Clique aqui"
variant="primary" | "secondary" | "outline" | "ghost"
size="small" | "medium" | "large"
loading={false}
disabled={false}
leftIcon="plus"
rightIcon="arrow-right"
haptics="light"
onPress={() => {}}
/><VelvetCard
elevation={4}
borderRadius="md"
padding="lg"
// Animações
pressAnimation="scale" | "opacity" | "lift"
// Gradientes
gradient={['#FF6B6B', '#4ECDC4']}
gradientDirection="horizontal"
onPress={() => {}}
>
<Text>Conteúdo do card</Text>
</VelvetCard><VelvetInput
placeholder="Digite aqui"
value={value}
onChangeText={setValue}
variant="outline" | "filled" | "underline"
leftIcon="search"
rightIcon="eye"
error={errorMessage}
success={successMessage}
// Validação
rules={[
{ required: true, message: "Campo obrigatório" },
{ minLength: 3, message: "Mínimo 3 caracteres" }
]}
/>import { VelvetHaptics } from 'react-native-velvet-ui';
// Métodos disponíveis
VelvetHaptics.light(); // Toque leve
VelvetHaptics.medium(); // Toque médio
VelvetHaptics.heavy(); // Toque forte
VelvetHaptics.success(); // Sucesso
VelvetHaptics.warning(); // Aviso
VelvetHaptics.error(); // Erro
VelvetHaptics.selection(); // Seleção
VelvetHaptics.celebrate(); // Celebração
// Uso nos componentes
<VelvetBottomSheet
haptics="medium"
onOpen={() => VelvetHaptics.success()}
/>import { VelvetConfig } from 'react-native-velvet-ui';
VelvetConfig.setDefaults({
// Animações
defaultAnimationDuration: 300,
defaultEasing: 'bezier(0.25, 0.46, 0.45, 0.94)',
// Haptics
enableHaptics: true,
defaultHapticIntensity: 'medium',
// Performance
enableNativeDriver: true,
enableReanimated: true,
// Localização
locale: 'pt-MZ', // pt, pt-MZ, pt-BR, en
});
// Configuração para dispositivos low-end
VelvetConfig.setPerformanceMode('balanced');
// Adicionar strings personalizadas
VelvetConfig.addStrings('pt-MZ', {
close: 'Fechar',
cancel: 'Cancelar',
confirm: 'Confirmar',
loading: 'Carregando...'
});import React, { useState } from 'react';
import { View, Text, ScrollView, Image } from 'react-native';
import {
VelvetProvider,
VelvetBottomSheet,
VelvetModal,
VelvetActionSheet,
VelvetButton,
VelvetCard,
createVelvetTheme,
useVelvetHaptics
} from 'react-native-velvet-ui';
// Tema personalizado
const ecommerceTheme = createVelvetTheme({
colors: {
primary: '#E91E63',
secondary: '#9C27B0',
success: '#4CAF50',
}
});
function ProductCard({ product, onPress }) {
const { buttonPress } = useVelvetHaptics();
return (
<VelvetCard
pressAnimation="scale"
onPress={() => {
buttonPress('primary');
onPress(product);
}}
style={{ margin: 8 }}
>
<Image source={{ uri: product.image }} style={styles.productImage} />
<Text style={styles.productName}>{product.name}</Text>
<Text style={styles.productPrice}>{product.price} MT</Text>
</VelvetCard>
);
}
function ProductDetailSheet({ product, visible, onClose }) {
const [showOptions, setShowOptions] = useState(false);
return (
<>
<VelvetBottomSheet
visible={visible}
onClose={onClose}
snapPoints={['40%', '80%']}
animation="elasticSlide"
header={
<View style={styles.sheetHeader}>
<Text style={styles.sheetTitle}>{product?.name}</Text>
</View>
}
footer={
<View style={styles.sheetFooter}>
<VelvetButton
title="Adicionar ao Carrinho"
variant="primary"
onPress={() => {}}
haptics="success"
/>
<VelvetButton
title="Opções"
variant="outline"
onPress={() => setShowOptions(true)}
/>
</View>
}
>
<ScrollView style={styles.sheetContent}>
<Image source={{ uri: product?.image }} style={styles.detailImage} />
<Text style={styles.description}>{product?.description}</Text>
<Text style={styles.price}>{product?.price} MT</Text>
</ScrollView>
</VelvetBottomSheet>
<VelvetActionSheet
visible={showOptions}
onClose={() => setShowOptions(false)}
title="Opções do Produto"
options={[
{
title: "Adicionar aos Favoritos",
icon: "heart",
onPress: () => {}
},
{
title: "Compartilhar",
icon: "share",
onPress: () => {}
},
{
title: "Ver Similares",
icon: "grid",
onPress: () => {}
}
]}
/>
</>
);
}
export default function EcommerceApp() {
const [selectedProduct, setSelectedProduct] = useState(null);
const [showProductDetail, setShowProductDetail] = useState(false);
const products = [
{ id: 1, name: "iPhone 15", price: "85.000", image: "...", description: "..." },
{ id: 2, name: "Samsung Galaxy", price: "75.000", image: "...", description: "..." },
];
const handleProductPress = (product) => {
setSelectedProduct(product);
setShowProductDetail(true);
};
return (
<VelvetProvider theme={ecommerceTheme}>
<View style={styles.container}>
<Text style={styles.title}>Loja Velvet</Text>
<ScrollView>
{products.map(product => (
<ProductCard
key={product.id}
product={product}
onPress={handleProductPress}
/>
))}
</ScrollView>
<ProductDetailSheet
product={selectedProduct}
visible={showProductDetail}
onClose={() => setShowProductDetail(false)}
/>
</View>
</VelvetProvider>
);
}
const styles = {
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
title: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginVertical: 20,
},
productImage: {
width: '100%',
height: 150,
borderRadius: 8,
},
productName: {
fontSize: 16,
fontWeight: '600',
marginTop: 8,
},
productPrice: {
fontSize: 14,
color: '#E91E63',
fontWeight: 'bold',
},
sheetHeader: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
sheetTitle: {
fontSize: 20,
fontWeight: 'bold',
},
sheetContent: {
padding: 16,
},
sheetFooter: {
padding: 16,
gap: 8,
},
detailImage: {
width: '100%',
height: 200,
borderRadius: 12,
},
description: {
fontSize: 16,
lineHeight: 24,
marginVertical: 12,
},
price: {
fontSize: 24,
fontWeight: 'bold',
color: '#E91E63',
},
};# Executar testes
npm test
# Testes com cobertura
npm run test-coverage
# Lint
npm run lint
# Formatar código
npm run formatAdoramos contribuições da comunidade!
- 🍴 Fork do repositório
- 🌿 Crie uma branch (
git checkout -b feature/nova-funcionalidade) - 📝 Commit das alterações (
git commit -am 'Adiciona nova funcionalidade') - 📤 Push para a branch (
git push origin feature/nova-funcionalidade) - 🔄 Abra um Pull Request
MIT License - veja LICENSE para detalhes.
Arnaldo Tomo
Full Stack Developer & UI/UX Enthusiast
🇲🇿 Moçambique | 🌍 Criando experiências móveis excepcionais