Alt description missing in image
Beta: Plugins coming soon!
@green-stack/corecomponentsImage
Alt description missing in image

Universal Image component

import { Image } from '@green-stack/components/Image'

Platform Optimized Images

Some primitives like the Image component have optimized versions for each environment:

  • next/image for web
  • expo-image for mobile

To automatically use the right one per render context, we’ve provided our own universal Image component:

<Image
    src={require('@app/assets/green-stack-logo.png')} // or a URL
    className="rounded-full"
    width={60}
    height={60}
    unoptimized
/>

Which you might wish to wrap with Nativewind to provide class names to:

styled.tsx
import { Image as UniversalImage } from '@green-stack/components/Image'
// ☝️ Import the universal Image component
import { styled } from 'nativewind'
 
// ⬇⬇⬇
 
export const Image = styled(UniversalImage, '')
// ☝️ Adds the ability to assign tailwind classes

Image Props

PropTypeDefaultDescription
srcstring | StaticImageDataRequiredImage source - can be URL or imported image
altstring'Alt description missing in image'Alt text for accessibility and SEO
widthnumber | stringRequired*Width in pixels or percentage
heightnumber | stringRequired*Height in pixels or percentage
classNamestring-Tailwind classes for styling
styleStyleProp<ViewStyle>{}Additional styles
priority'high' | 'normal''normal'Loading priority - ‘high’ for LCP elements
onError(error: any) => void-Called on image loading error
onLoadEnd() => void-Called when image load completes
fillboolean-Fill parent container (requires relative positioning)
contentFit'cover' | 'contain' | 'fill' | 'none' | 'scale-down''cover'How image fits container
cachePolicy'none' | 'disk' | 'memory' | 'memory-disk''disk'Image caching strategy
blurRadiusnumber0Blur effect radius in points
qualitynumber75Image quality (1-100)
sizesstring-Responsive image sizes hint
unoptimizedbooleanfalseSkip image optimization

*Required unless using fill or static import

TypeScript Definition

You can find the TypeScript definition for our Universal Image component in Image.types.ts:

Image.types.ts
type UniversalImageProps = {
 
    // -- Universal props --
 
    /**
     * Universal, will affect both Expo & Next.js - Must be one of the following:
     * - A path string like `'/assets/logo.png'`. This can be either an absolute external URL, or an internal path depending on the loader prop.
     * - A statically imported image file, like `import logo from './logo.png'` or `require('./logo.png')`.
     * 
     * When using an external URL, you must add it to `remotePatterns` in `next.config.js`.
     * @platform web, android, ios @framework expo, next.js */
    src: string | StaticImport
 
    width?: number | `${number}` | `${number}%`
    height?: number | `${number}` | `${number}%`
 
    /** Universal, will affect both Expo & Next.js
     * - Remember that the required width and height props can interact with your styling. If you use styling to modify an image's width, you should also style its height to auto to preserve its intrinsic aspect ratio, or your image will be distorted. */
    className?: string
 
    /** Universal, will affect both Expo & Next.js
     * - Remember that the required width and height props can interact with your styling. If you use styling to modify an image's width, you should also style its height to auto to preserve its intrinsic aspect ratio, or your image will be distorted. */
    style?: StyleProp<ImageStyle> | ExpoImageProps['style']
 
    alt?: string
    priority?: "low" | "normal" | "high" | null
 
    onError?: ((event: ImageErrorEventData) => void)
    onLoadEnd?: (() => void)
 
    // -- '@next/image' specific props --
 
    /** Custom function used to resolve image URLs. A loader is a function returning a URL string for the image, given the following parameters: `src`, `width`, `quality` (`number` from 0 - 1) Alternatively, you can use the [loaderFile](https://nextjs.org/docs/pages/api-reference/components/image#loaderfile) configuration in next.config.js to configure every instance of next/image in your application, without passing a prop. */
    loader?: ImageLoader
 
    fill?: boolean
    sizes?: string
    quality?: number | `${number}`
    nextPlaceholder?: PlaceholderValue | 'blur' | 'empty' | `data:image/${string}`
    loading?: 'lazy' | 'eager'
    blurDataURL?: string
    unoptimized?: boolean
 
    // -- 'expo-image' specific props --
 
    accessibilityLabel?: string
    accessible?: boolean
    allowDownscaling?: boolean
    autoplay?: boolean
    blurRadius?: number
    cachePolicy?: 'none' | 'disk' | 'memory' | 'memory-disk'
    contentFit?: ImageContentFit | 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'
    contentPosition?: ImageContentPosition | 'top' | 'bottom' | 'left' | 'right' | 'center' | 'top left' | 'top right' | ...
    enableLiveTextInteraction?: ExpoImageProps['enableLiveTextInteraction']
    focusable?: boolean
    expoPlaceholder?: ExpoImageProps['expoPlaceholder'] | string | StaticImport
    onLoadStart?: (() => void)
    onProgress?: ((event: ImageProgressEventData) => void)
    placeholderContentFit?: ImageContentFit | 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'
    recyclingKey?: string | null
    responsivePolicy?: 'static' | 'initial' | 'live'
}
Zod Schema
import { z } from 'zod'
 
const UniversalImageSchema = z.object({
    // Universal props
    src: z.union([z.string(), z.any()]), // StaticImageData type
    alt: z.string().optional(),
    width: z.union([z.number(), z.string()]).optional(),
    height: z.union([z.number(), z.string()]).optional(),
    className: z.string().optional(),
    style: z.any().optional(),
    priority: z.enum(['high', 'normal']).optional(),
    onError: z.function().optional(),
    onLoadEnd: z.function().optional(),
 
    // Next.js specific
    loader: z.function().optional(),
    fill: z.boolean().optional(),
    sizes: z.string().optional(),
    quality: z.number().min(1).max(100).optional(),
    nextPlaceholder: z.enum(['blur', 'empty']).optional(),
    loading: z.enum(['lazy', 'eager']).optional(),
    blurDataURL: z.string().optional(),
    unoptimized: z.boolean().optional(),
 
    // Expo specific
    accessibilityLabel: z.string().optional(),
    accessible: z.boolean().optional(),
    allowDownscaling: z.boolean().optional(),
    autoplay: z.boolean().optional(),
    blurRadius: z.number().optional(),
    cachePolicy: z.enum(['none', 'disk', 'memory', 'memory-disk']).optional(),
    contentFit: z.enum(['cover', 'contain', 'fill', 'none', 'scale-down']).optional(),
    contentPosition: z.string().optional(),
    enableLiveTextInteraction: z.boolean().optional(),
    focusable: z.boolean().optional(),
    expoPlaceholder: z.any().optional(),
    onLoadStart: z.function().optional(),
    onProgress: z.function().optional(),
    placeholderContentFit: z.enum(['cover', 'contain', 'fill', 'none', 'scale-down']).optional(),
    recyclingKey: z.string().optional(),
    responsivePolicy: z.enum(['static', 'initial', 'live']).optional(),
})

React Portability Patterns

Both Next.js and Expo have their own optimized Image components. This is why there are also versions specifically for each of those environments:

        • Image.expo.tsx
        • Image.next.tsx
        • Image.tsx
        • Image.types.ts
  • Image.next.tsx uses next/image for web
  • Image.expo.tsx uses expo-image for mobile
  • Image.types.ts ensures there is a shared type for both implementations

Finally, Image.tsx will retrieve whichever implementation was provided as contextImage to the <UniversalAppProviders> component, which is further passed to <CoreContext.Provider/>:

ExpoRootLayout.tsx
import { Image as ExpoImage } from '@green-stack/components/Image.expo'
 
// ... Later ...
 
<UniversalAppProviders
    contextImage={ExpoImage}
>
    ...
</UniversalAppProviders>
NextRootLayout.tsx
import { Image as NextImage } from '@green-stack/components/Image.next'
 
// ... Later ...
 
<UniversalAppProviders
    contextImage={NextImage}
>
    ...
</UniversalAppProviders>

Why this pattern?

The ‘React Portability Patterns’ used here are designed to ensure that you can easily reuse optimized versions of components across different flavours of writing React.

On the one hand, that means it’s already set up to work with both Expo and Next.js in an optimal way.

But, you can actually add your own implementations for other environments, without having to refactor the code that uses the Image component.

Supporting more environments

Just add your own Image.<environment>.tsx file that respects the shared types, and then pass it to the <UniversalAppProviders> component as contextImage.