Made by Pixel
Profile card
A profile in two moods. Full or background, with the same little details.
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 },
});