Skip to content

Card Scroller

Layout and navigationNativeMIT
View in Mobile Storybook

Snap-to-card horizontal scroller with auto-center + fade-off.

bun add @otfdashkit/ui-native

Code

import { 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.