Skip to content
pixelfor Expo
Ctrl K

Made by Pixel

Profile card

A profile in two moods. Full or background, with the same little details.

Try it on your phone

Profile card · full

Scan to open Profile card · full in Expo Go

Install Expo Go first, then scan this QR to run the native component.

GET EXPO GO ONGoogle Play

This demo uses Expo SDK 54. If your installed Expo Go supports a different SDK, get the compatible build. iOS needs a build supporting this SDK.

Make it yours

The selected variant and its shared base.

profile-card-full.tsx

import React from 'react';
import { ProfileCardBase, type ProfileCardProps } from './profile-card-base';
export type ProfileCardFullProps = ProfileCardProps;
export default function ProfileCardFull(props: ProfileCardFullProps) {
  return <ProfileCardBase {...props} variant="full" />;
}

profile-card-base.tsx

import React, { useState } from 'react';
import { Image, Pressable, StyleSheet, Text, View, type ViewStyle } from 'react-native';
import { useFonts } from 'expo-font';
import { Inter_400Regular } from '@expo-google-fonts/inter/400Regular';
import { Inter_500Medium } from '@expo-google-fonts/inter/500Medium';
import { Inter_600SemiBold } from '@expo-google-fonts/inter/600SemiBold';
import { HugeiconsIcon } from '@hugeicons/react-native';
import ArrowRight02Icon from '@hugeicons/core-free-icons/ArrowRight02Icon';
import Bookmark02Icon from '@hugeicons/core-free-icons/Bookmark02Icon';
import NoteIcon from '@hugeicons/core-free-icons/NoteIcon';
import StarIcon from '@hugeicons/core-free-icons/StarIcon';
import UserIcon from '@hugeicons/core-free-icons/UserIcon';
import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg';
export type ProfileCardProps = {
  width?: number;
  name?: string;
  handle?: string;
  rating?: string | number;
  posts?: string | number;
  followers?: string | number;
  following?: boolean;
  saved?: boolean;
  onFollowPress?: (following: boolean) => void;
  onBookmarkPress?: (saved: boolean) => void;
  onContactPress?: () => void;
  style?: ViewStyle;
};
type ProfileCardBaseProps = ProfileCardProps & { variant: 'background' | 'full' };
const CARD_WIDTH = 516;
const CARD_HEIGHT = 636;
const AVATAR = require('./assets/boy-avatar.png');
const GREEN_FIELD = require('./assets/green-field.png');
const METRICS = [
  { key: 'rating', label: 'Rating', Icon: StarIcon, iconX: 41, valueX: 86 },
  { key: 'posts', label: 'Posts', Icon: NoteIcon, iconX: 191, valueX: 234 },
  { key: 'followers', label: 'Followers', Icon: UserIcon, iconX: 348, valueX: 392 },
] as const;
export function ProfileCardBase({
  variant,
  width = 344,
  name = 'Olivia Rhye',
  handle = '@Olivia_',
  rating = '4.9',
  posts = '125',
  followers = '12.4K',
  following,
  saved,
  onFollowPress,
  onBookmarkPress,
  onContactPress,
  style,
}: ProfileCardBaseProps) {
  const [fontsLoaded] = useFonts({ Inter_400Regular, Inter_500Medium, Inter_600SemiBold });
  const [localFollowing, setLocalFollowing] = useState(false);
  const [localSaved, setLocalSaved] = useState(variant === 'background');
  const isFollowing = following ?? localFollowing;
  const isSaved = saved ?? localSaved;
  const isBackground = variant === 'background';
  const scale = width / CARD_WIDTH;
  const height = CARD_HEIGHT * scale;
  const foreground = isBackground ? '#FFFFFF' : '#08090B';
  const regularFont = fontsLoaded ? 'Inter_400Regular' : undefined;
  const mediumFont = fontsLoaded ? 'Inter_500Medium' : undefined;
  const semiboldFont = fontsLoaded ? 'Inter_600SemiBold' : undefined;
  const metricValues = { rating, posts, followers };
  const follow = () => {
    const next = !isFollowing;
    if (following === undefined) setLocalFollowing(next);
    onFollowPress?.(next);
  };
  const bookmark = () => {
    const next = !isSaved;
    if (saved === undefined) setLocalSaved(next);
    onBookmarkPress?.(next);
  };
  return (
    <View
      accessibilityRole="summary"
      accessibilityLabel={`${name}, ${handle}. Rating ${rating}, ${posts} posts, ${followers} followers.`}
      style={[
        styles.outer,
        {
          width,
          height,
          borderRadius: 40 * scale,
          backgroundColor: isBackground ? '#178E5A' : '#FFFFFF',
          boxShadow: isBackground
            ? '0px 9px 20px rgba(22, 52, 39, 0.19)'
            : '0px 9px 20px rgba(24, 26, 29, 0.11)',
        },
        style,
      ]}
    >
      <View style={[styles.clip, { borderRadius: 40 * scale }]}>
        {isBackground ? (
          <Image source={GREEN_FIELD} resizeMode="cover" style={StyleSheet.absoluteFill} />
        ) : (
          <View
            style={[
              styles.banner,
              {
                left: 30 * scale,
                top: 37 * scale,
                width: 455 * scale,
                height: 179 * scale,
                borderRadius: 26 * scale,
              },
            ]}
          >
            <Image source={GREEN_FIELD} resizeMode="cover" style={StyleSheet.absoluteFill} />
          </View>
        )}
        <Image
          source={AVATAR}
          resizeMode="contain"
          style={[
            styles.avatar,
            {
              left: (isBackground ? 32 : 49) * scale,
              top: 152 * scale,
              width: 120 * scale,
              height: 120 * scale,
            },
          ]}
        />
        <Text
          numberOfLines={1}
          style={[
            styles.name,
            {
              left: 31 * scale,
              top: 298 * scale,
              width: 285 * scale,
              color: foreground,
              fontSize: 25 * scale,
              lineHeight: 31 * scale,
              fontFamily: semiboldFont,
            },
          ]}
        >
          {name}
        </Text>
        <Text
          numberOfLines={1}
          style={[
            styles.handle,
            {
              left: 31 * scale,
              top: 337 * scale,
              width: 285 * scale,
              color: isBackground ? '#F7F8F3' : '#5B5C62',
              fontSize: 20 * scale,
              lineHeight: 25 * scale,
              fontFamily: regularFont,
            },
          ]}
        >
          {handle}
        </Text>
        <Pressable
          accessibilityRole="button"
          accessibilityLabel={isFollowing ? 'Unfollow' : 'Follow'}
          onPress={follow}
          style={[
            styles.followButton,
            {
              left: 330 * scale,
              top: 301 * scale,
              width: 94 * scale,
              height: 50 * scale,
              borderRadius: 25 * scale,
              backgroundColor: isBackground ? 'transparent' : '#000000',
              borderColor: isBackground ? '#FFFFFF' : 'transparent',
              borderWidth: isBackground ? 1.4 * scale : 0,
            },
          ]}
        >
          <Text
            numberOfLines={1}
            style={{ color: '#FFFFFF', fontSize: 17 * scale, lineHeight: 22 * scale, fontFamily: regularFont }}
          >
            {isFollowing ? 'Following' : 'Follow'}
          </Text>
        </Pressable>
        <Pressable
          accessibilityRole="button"
          accessibilityLabel={isSaved ? 'Remove bookmark' : 'Bookmark profile'}
          onPress={bookmark}
          style={[
            styles.bookmarkButton,
            {
              left: 435 * scale,
              top: 301 * scale,
              width: 50 * scale,
              height: 50 * scale,
              borderRadius: 25 * scale,
              borderColor: isBackground ? '#FFFFFF' : '#D9DADC',
              borderWidth: 1.2 * scale,
            },
          ]}
        >
          <HugeiconsIcon
            icon={Bookmark02Icon}
            size={24 * scale}
            color={foreground}
            strokeWidth={1.8}
            fill={isSaved ? foreground : 'none'}
          />
        </Pressable>
        {!isBackground && (
          <View
            style={[
              styles.divider,
              { left: 31 * scale, top: 375 * scale, width: 455 * scale, height: 1.2 * scale },
            ]}
          />
        )}
        {METRICS.map(({ key, label, Icon, iconX, valueX }) => (
          <React.Fragment key={key}>
            <View style={[styles.metricIcon, { left: iconX * scale, top: 407 * scale, width: 30 * scale, height: 32 * scale }]}>
              <HugeiconsIcon
                icon={Icon}
                size={29 * scale}
                color={foreground}
                strokeWidth={key === 'posts' ? 2 : 1.5}
                fill={key === 'posts' ? 'none' : foreground}
              />
            </View>
            <Text
              numberOfLines={1}
              style={[
                styles.metricValue,
                {
                  left: valueX * scale,
                  top: 405 * scale,
                  width: (CARD_WIDTH - valueX - 15) * scale,
                  color: foreground,
                  fontSize: 33 * scale,
                  lineHeight: 40 * scale,
                  fontFamily: mediumFont,
                },
              ]}
            >
              {metricValues[key]}
            </Text>
            <Text
              numberOfLines={1}
              style={[
                styles.metricLabel,
                {
                  left: valueX * scale,
                  top: 462 * scale,
                  width: (CARD_WIDTH - valueX - 15) * scale,
                  color: isBackground ? '#F0F2EA' : '#5D5D61',
                  fontSize: 20 * scale,
                  lineHeight: 25 * scale,
                  fontFamily: regularFont,
                },
              ]}
            >
              {label}
            </Text>
          </React.Fragment>
        ))}
        <Pressable
          accessibilityRole="button"
          accessibilityLabel="Get in Touch"
          onPress={onContactPress}
          style={[
            styles.contactButton,
            {
              left: 17 * scale,
              top: 529 * scale,
              width: 482 * scale,
              height: 85 * scale,
              borderRadius: 43 * scale,
              backgroundColor: isBackground ? '#FFFFFF' : '#080808',
            },
          ]}
        >
          {!isBackground && (
            <Svg width={482 * scale} height={85 * scale} style={StyleSheet.absoluteFill}>
              <Defs>
                <LinearGradient id="profile-contact-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
                  <Stop offset="0%" stopColor="#000000" />
                  <Stop offset="55%" stopColor="#101010" />
                  <Stop offset="100%" stopColor="#777777" />
                </LinearGradient>
              </Defs>
              <Rect width={482 * scale} height={85 * scale} fill="url(#profile-contact-gradient)" />
            </Svg>
          )}
          <View
            style={[
              styles.arrowDisc,
              {
                left: 18 * scale,
                width: 50 * scale,
                height: 50 * scale,
                borderRadius: 25 * scale,
                backgroundColor: isBackground ? '#000000' : '#FFFFFF',
              },
            ]}
          >
            <HugeiconsIcon
              icon={ArrowRight02Icon}
              size={31 * scale}
              color={isBackground ? '#FFFFFF' : '#000000'}
              strokeWidth={2.3}
            />
          </View>
          <Text
            style={[
              styles.contactText,
              {
                color: isBackground ? '#111111' : '#FFFFFF',
                fontSize: 25 * scale,
                lineHeight: 31 * scale,
                fontFamily: regularFont,
              },
            ]}
          >
            Get in Touch
          </Text>
        </Pressable>
      </View>
    </View>
  );
}
const styles = StyleSheet.create({
  outer: { position: 'relative' },
  clip: { position: 'absolute', top: 0, right: 0, bottom: 0, left: 0, overflow: 'hidden' },
  banner: { position: 'absolute', overflow: 'hidden' },
  avatar: { position: 'absolute' },
  name: { position: 'absolute', includeFontPadding: false, letterSpacing: -0.4 },
  handle: { position: 'absolute', includeFontPadding: false, letterSpacing: -0.3 },
  followButton: { position: 'absolute', alignItems: 'center', justifyContent: 'center' },
  bookmarkButton: { position: 'absolute', alignItems: 'center', justifyContent: 'center' },
  divider: { position: 'absolute', backgroundColor: '#D8D8DA' },
  metricIcon: { position: 'absolute', alignItems: 'center', justifyContent: 'center' },
  metricValue: { position: 'absolute', includeFontPadding: false, letterSpacing: -0.8 },
  metricLabel: { position: 'absolute', includeFontPadding: false, letterSpacing: -0.25 },
  contactButton: { position: 'absolute', overflow: 'hidden', justifyContent: 'center' },
  arrowDisc: { position: 'absolute', alignItems: 'center', justifyContent: 'center' },
  contactText: { width: '100%', textAlign: 'center', includeFontPadding: false, letterSpacing: -0.35 },
});
Visual inspiration on Pinterest