bpinheiroms/dotfiles/expo-playground-patterns

Claude Code and agent skill by bpinheiroms/dotfiles

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

Comprehensive UI/animation patterns reference from expo-playground. Use when implementing: charts, native sheets, native toolbars, cards, card flips, expandable tabs, product grids, dropdowns, parallax, onboarding, switches, weather dashboards, masonry grids, video cards, journal cards, bottom bars, gradients, Skia charts, glassmorphism, dark mode, theme systems, or any animated UI component.

Install

npx vskill@latest install bpinheiroms/dotfiles/expo-playground-patterns

SKILL.md

Expo Playground UI Patterns Reference

Complete implementation guide for 20+ UI patterns extracted from expo-playground. Stack: React Native 0.83 + Expo 55 + NativeWind 4 + Reanimated 4 + Skia 2.


FOUNDATION

Dependencies
json
{
"expo": "^55.0.0-preview.7",
"react": "19.2.0",
"react-native": "0.83.1",
"nativewind": "^4.2.1",
"tailwindcss": "^3.3.2",
"react-native-reanimated": "~4.2.1",
"react-native-gesture-handler": "~2.30.0",
"@shopify/react-native-skia": "^2.4.18",
"expo-blur": "~55.0.2",
"expo-linear-gradient": "~55.0.2",
"expo-mesh-gradient": "~55.0.2",
"expo-video": "~55.0.2",
"expo-symbols": "~55.0.2",
"lottie-react-native": "~7.3.1",
"lucide-react-native": "^0.562.0",
"react-native-svg": "15.15.1",
"clsx": "^2.1.1",
"tailwind-merge": "^3.0.2"
}
Babel Config
js
module.exports = function (api) {
api.cache(true);
return {
presets: [['nativewind/babel', { jsxImportSource: 'nativewind' }], 'babel-preset-expo'],
plugins: ['react-native-worklets/plugin'], // Required for Skia
};
};
Metro Config
js
const { getDefaultConfig } = require('expo/metro-config');
const { withNativeWind } = require('nativewind/metro');
const config = getDefaultConfig(__dirname);
module.exports = withNativeWind(config, { input: './global.css' });
app.json experiments
json
{ "experiments": { "tsconfigPaths": true, "reactCompiler": true } }
Reanimated Shared Element Transitions
json
// package.json
{ "reanimated": { "staticFeatureFlags": { "ENABLE_SHARED_ELEMENT_TRANSITIONS": true } } }

THEME SYSTEM

Architecture: 3-layer theming

Layer 1: CSS Variables via NativeWind `vars()`

ts
// utils/color-theme.ts
import { vars } from 'nativewind';
export const themes = {
light: vars({
'--color-primary': '#000000',
'--color-invert': '#ffffff',
'--color-secondary': '#ffffff',
'--color-background': '#F4F4F5',
'--color-darker': '#F4F4F5',
'--color-text': '#000000',
'--color-highlight': '#FF2056',
'--color-border': 'rgba(0,0,0,0.15)',
}),
dark: vars({
'--color-primary': '#ffffff',
'--color-invert': '#000000',
'--color-secondary': '#262626',
'--color-background': '#0A0A0A',
'--color-darker': '#000000',
'--color-text': '#ffffff',
'--color-highlight': '#FF2056',
'--color-border': 'rgba(255,255,255,0.15)',
}),
};

Layer 2: ThemeContext

tsx
// app/contexts/ThemeContext.tsx
import { colorScheme } from 'nativewind';
const ThemeContext = createContext({ theme: 'dark', toggleTheme: () => {} });
export function ThemeProvider({ children }) {
const [currentTheme, setCurrentTheme] = useState<'light' | 'dark'>('dark');
const toggleTheme = () => {
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
setCurrentTheme(newTheme);
colorScheme.set(newTheme); // Sync NativeWind dark mode
};
return (
<ThemeContext.Provider value={{ theme: currentTheme, toggleTheme }}>
<StatusBar style={currentTheme === 'dark' ? 'light' : 'dark'} />
<View style={themes[currentTheme]}>{children}</View>
</ThemeContext.Provider>
);
}

Layer 3: Imperative Colors Hook

tsx
// app/contexts/ThemeColors.tsx
export default function useThemeColors() {
const { theme } = useTheme();
const isDark = theme === 'dark';
return {
icon: isDark ? '#fff' : '#000',
bg: isDark ? '#0A0A0A' : '#F4F4F5',
invert: isDark ? '#000' : '#fff',
secondary: isDark ? '#262626' : '#fff',
text: isDark ? '#fff' : '#000',
highlight: '#FF2056',
border: isDark ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.15)',
isDark,
// ... more colors
};
}

Tailwind Config

js
// tailwind.config.js
module.exports = {
presets: [require('nativewind/preset')],
theme: {
extend: {
colors: {
primary: 'var(--color-primary)',
invert: 'var(--color-invert)',
secondary: 'var(--color-secondary)',
background: 'var(--color-background)',
text: 'var(--color-text)',
highlight: 'var(--color-highlight)',
border: 'var(--color-border)',
darker: 'var(--color-darker)',
},
},
},
};

Usage: bg-background, text-text, bg-secondary, border-border, text-highlight


UTILITY: cn() - Class Merging

ts
// lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }

UTILITY: useShadow - Cross-Platform Shadows

ts
// utils/useShadow.ts
export const shadowPresets = {
small: { elevation: 3, shadowColor: '#000', shadowOpacity: 0.2, shadowRadius: 2.5, shadowOffset: { width: 0, height: 1 } },
medium: { elevation: 8, shadowColor: '#000', shadowOpacity: 0.2, shadowRadius: 5, shadowOffset: { width: 0, height: 3 } },
large: { elevation: 15, shadowColor: '#000', shadowOpacity: 0.2, shadowRadius: 10.84, shadowOffset: { width: 0, height: 10 } },
card: { elevation: 4, shadowColor: '#000', shadowOpacity: 0.2, shadowRadius: 3.84, shadowOffset: { width: 0, height: 2 } },
};
// Usage: <View style={shadowPresets.large} className="rounded-2xl bg-secondary">

ANIMATION RECIPES

Spring Configs Reference
ConfigFeelUse For
{ damping: 100, stiffness: 600 }Fast, snappyTabs, card flips, grid reorders
{ damping: 100, stiffness: 900 }Very stiffChip indicators, card scale
{ damping: 90, stiffness: 700 }Quick, slight bounceDropdowns, info panels
{ damping: 80, stiffness: 400 }SmoothImage zoom, background transitions
{ damping: 15, stiffness: 150 }Soft, elasticOpacity fade, subtle reveals
{ bounciness: 10, speed: 12 }Playful bounceSwitch thumb
{ tension: 100, friction: 8 }RN Animated springSliding indicators
Easing Reference
EasingUsage
Easing.out(Easing.cubic)Standard arrivals/exits
Easing.bezier(0.25, 0.1, 0.25, 1)Apple-style smooth motion
Easing.bezier(0.1, 1, 0.94, 1)Snappy expand (bottom bar)
Easing.out(Easing.back(0.5))Subtle overshoot entrance
Easing.out(Easing.back(1))Visible overshoot
Easing.in(Easing.cubic)Exit animations
Easing.inOut(Easing.cubic)Symmetric transitions
Counting Animation (number interpolation)
tsx
// Works with RN Animated. For smooth number transitions.
const countAnim = useRef(new Animated.Value(0)).current;
const [displayValue, setDisplayValue] = useState(startValue);
useEffect(() => {
const start = displayValue;
const end = targetValue;
countAnim.setValue(0);
const listener = countAnim.addListener(({ value }) => {
setDisplayValue(Math.round(start + (end - start) * value));
});
Animated.timing(countAnim, { toValue: 1, duration: 500, useNativeDriver: false }).start();
return () => countAnim.removeListener(listener);
}, [targetValue]);
Math-Based Stagger (no withDelay)
tsx
// Each item offsets its progress from a single shared value
const getStaggeredStyle = (index: number) => {
return useAnimatedStyle(() => {
const delay = index * 0.08;
const adjustedProgress = Math.max(0, Math.min(1, (sharedValue.value - delay) / (1 - delay)));
const opacity = interpolate(adjustedProgress, [0, 0.5, 1], [0, 0, 1]);
const translateY = interpolate(adjustedProgress, [0, 1], [10, 0]);
return { opacity, transform: [{ translateY }] };
});
};
Multi-Stop Interpolation for Staggered Reveals
tsx
// Content stays hidden for first 30%, tabs fade out early, content fades in late
const tabOpacity = interpolate(value, [0, 0.3, 1], [1, 0, 0]);
const contentOpacity = interpolate(value, [0, 0.3, 0.6, 1], [0, 0, 0, 1]);
const height = interpolate(value, [0, 0.3, 1], [collapsed, collapsed, expanded]);
Data-Synced Fade Transition
tsx
// Fade out -> change data at midpoint -> fade in
const fadeAnim = useRef(new Animated.Value(1)).current;
const animateTransition = (callback: () => void) => {
Animated.sequence([
Animated.timing(fadeAnim, { toValue: 0, duration: 200, useNativeDriver: true }),
Animated.timing(fadeAnim, { toValue: 1, duration: 200, useNativeDriver: true }),
]).start();
setTimeout(callback, 200); // Execute at midpoint
};

PATTERN: 3D Card Flip

Reanimated shared value + perspective transform + opacity interpolation.

tsx
const rotation = useSharedValue(0);
const flipCard = () => {
rotation.value = withSpring(rotation.value === 0 ? 180 : 0, { damping: 100, stiffness: 600 });
};
const frontStyle = useAnimatedStyle(() => ({
transform: [{ perspective: 1000 }, { rotateY: `${rotation.value}deg` }],
opacity: interpolate(rotation.value, [0, 90, 180], [1, 0, 0]),
}));
const backStyle = useAnimatedStyle(() => ({
transform: [{ perspective: 1000 }, { rotateY: `${rotation.value - 180}deg` }],
opacity: interpolate(rotation.value, [0, 90, 180], [0, 0, 1]),
}));
// Layout: two Animated.Views with position: absolute, full size
<View style={{ height: 450 }}>
<Animated.View style={[StyleSheet.absoluteFill, frontStyle]}>{/* Front content */}</Animated.View>
<Animated.View style={[StyleSheet.absoluteFill, backStyle]}>{/* Back content */}</Animated.View>
</View>

PATTERN: Glassmorphism / Blur

tsx
import { BlurView } from 'expo-blur';
// Light glass (over images)
<BlurView intensity={40} tint="light" style={{ position: 'absolute', ...StyleSheet.absoluteFillObject }}>
<View style={{ backgroundColor: 'rgba(255,255,255,0)' }} />
</BlurView>
// Dark glass (bottom bars, tooltips)
<BlurView intensity={10} tint="dark" className="rounded-full overflow-hidden bg-black/50">
{/* Content */}
</BlurView>
// Glass button
<BlurView intensity={80} tint="light" style={{ width: 40, height: 40, borderRadius: 20, overflow: 'hidden' }}>
<Feather name="arrow-left" size={20} color="white" />
</BlurView>

PATTERN: Expanding Hotspots (Product Detail)

Interactive hotspots over an image that expand from circle to card with glassmorphism.

tsx
// Three coordinated shared values for background image
const imageScale = useSharedValue(1);
const imageTranslateX = useSharedValue(0);
const imageTranslateY = useSharedValue(0);
// Each hotspot has: expanded (0|1) + otherExpanded (0|1)
const containerStyle = useAnimatedStyle(() => ({
width: interpolate(expanded.value, [0, 1], [34, 220]),
height: interpolate(expanded.value, [0, 1], [34, 130]),
borderRadius: interpolate(expanded.value, [0, 1], [24, 20]),
opacity: interpolate(otherExpanded.value, [0, 1], [1, 0]), // Hide when sibling expands
transform: [{ scale: interpolate(otherExpanded.value, [0, 1], [1, 0.6]) }],
}));
// Plus icon rotates 45deg to become X
const iconStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${interpolate(expanded.value, [0, 1], [0, 45])}deg` }],
}));
// Content reveal with delay (appears after 20% expansion)
const contentStyle = useAnimatedStyle(() => {
const progress = Math.max(0, Math.min(1, (expanded.value - 0.2) / 0.8));
return {
opacity: interpolate(progress, [0, 0.5, 1], [0, 0, 1]),
transform: [{ translateY: interpolate(progress, [0, 1], [40, 0]) }],
};
});
// On tap: image zooms to hotspot area
imageScale.value = withSpring(product.scale, { damping: 80, stiffness: 400 });
imageTranslateX.value = withSpring(product.imageOffset.x, { damping: 80, stiffness: 400 });
imageTranslateY.value = withSpring(product.imageOffset.y, { damping: 80, stiffness: 400 });

PATTERN: Hero Transition (Notes/Card Expand)

Measure card position, open modal, animate from card position to centered modal.

tsx
// Step 1: Measure card position
const cardRef = useRef<View>(null);
cardRef.current?.measureInWindow((x, y, width, height) => {
setCardLayout({ x, y, width, height });
setSelectedNote(note);
});

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
107
Repository forks
8
7-day trend score
23

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/bpinheiroms/dotfiles/expo-playground-patterns/badge)](https://verified-skill.com/skills/bpinheiroms/dotfiles/expo-playground-patterns)

Something wrong with this skill or its page? Tell us.