Card Scroller
Layout and navigationNativeMITSnap-to-card horizontal scroller with auto-center + fade-off.
bun add @otfdashkit/ui-native
Code
@otfdashkit/ui-nativeimport { useState } from 'react'
import { View, Text } from 'react-native'
import {
CardScroller,
SizableText,
YStack,
} from '@otfdashkit/ui-native'
import { Section, ShowcaseFrame } from '../../components/ShowcaseFrame'
interface DemoCard {
id: string
title: string
body: string
accent: string
}
const WORKOUT_TYPES: DemoCard[] = [
{ id: 'run', title: 'Run', body: '5 km · 26 min', accent: '#ff375f' },
{ id: 'ride', title: 'Ride', body: '12 km · 38 min', accent: '#00d4d4' },
{ id: 'strength', title: 'Strength', body: '5 sets · 45 min', accent: '#a3ff12' },
{ id: 'yoga', title: 'Yoga', body: 'Flow · 30 min', accent: '#fbbf24' },
{ id: 'hiit', title: 'HIIT', body: '20 rounds · 15 min', accent: '#a78bfa' },
]
const PLANS: DemoCard[] = [
{ id: 'p1', title: '8-week strength', body: 'Build foundation', accent: '#a3ff12' },
{ id: 'p2', title: '5K trainer', body: 'Couch → 5K', accent: '#ff375f' },
{ id: 'p3', title: 'Daily mobility', body: '15 min/day', accent: '#fbbf24' },
]
// Pure RN View + Text — NO Tamagui components inside Reanimated's
// Animated.View. On Android/Hermes, Tamagui's style resolution (both $tokens
// AND inline style overrides via the Tamagui style prop system) can silently
// fail inside animated trees, making text invisible. RN's native Text with
// a plain style object is always reliable.
function renderCard(c: DemoCard) {
return (
<View style={{
flex: 1,
padding: 20,
backgroundColor: 'rgba(255,255,255,0.06)',
borderRadius: 14,
borderWidth: 1,
borderColor: c.accent,
justifyContent: 'space-between',
gap: 8,
minHeight: 100,
}}>
<Text style={{ fontSize: 18, fontWeight: '700', color: c.accent }}>
{c.title}
</Text>
<Text style={{ fontSize: 13, color: 'rgba(255,255,255,0.65)' }}>
{c.body}
</Text>
</View>
)
}
export default function CardScrollerShowcase() {
const [centerIdx, setCenterIdx] = useState(2)
const [startIdx, setStartIdx] = useState(0)
return (
<ShowcaseFrame
title="CardScroller"
description="Snap-to-card horizontal scroller. Center-snap auto-aligns the focused card to viewport center; off-center cards fade + scale down."
>
<Section title="Center snap (default)">
<YStack height={180}>
<CardScroller
data={WORKOUT_TYPES}
keyExtractor={(c) => c.id}
renderItem={(c) => renderCard(c)}
itemWidth={220}
initialIndex={2}
onIndexChange={setCenterIdx}
/>
</YStack>
<SizableText size="$2" color="$color11">Centered: {WORKOUT_TYPES[centerIdx]?.title}</SizableText>
</Section>
<Section title="Percentage width — 70% of viewport">
<YStack height={180}>
<CardScroller
data={PLANS}
keyExtractor={(c) => c.id}
renderItem={(c) => renderCard(c)}
itemWidth="70%"
initialIndex={1}
/>
</YStack>
</Section>
<Section title="Start snap, no fade">
<YStack height={180}>
<CardScroller
data={WORKOUT_TYPES}
keyExtractor={(c) => c.id}
renderItem={(c) => renderCard(c)}
itemWidth={180}
snap="start"
fadeOff={false}
paddingHorizontal={16}
onIndexChange={setStartIdx}
/>
</YStack>
<SizableText size="$2" color="$color11">Index: {startIdx}</SizableText>
</Section>
<Section title="Custom gap + small list">
<YStack height={180}>
<CardScroller
data={PLANS}
keyExtractor={(c) => c.id}
renderItem={(c) => renderCard(c)}
itemWidth={240}
gap={20}
/>
</YStack>
</Section>
</ShowcaseFrame>
)
}
Free with the SDK. This pattern ships in @otfdashkit/ui-native under the MIT license.