# Thumbnail (/ui/components/react/thumbnail)



<Playground component="thumbnail" />

## Usage [#usage]

```tsx
import { Thumbnail } from '@appica/ui-react/thumbnail'
```

```tsx
<Thumbnail src="/thumbnail.jpg" alt="Mountain peak" />

<Thumbnail variant="icon-primary">
  <Bolt />
</Thumbnail>
```

`Thumbnail` is a compact preview tile — the small square you see beside a file, a list row, or a media item. A single component covers both jobs:

* **Image** (`variant="image"`, the default) — shows `src` cropped to fill, preloading it like an [`Avatar`](/ui/components/react/avatar) and dropping to a placeholder icon if the image is missing or fails.
* **Icon** (`variant="icon-*"`) — a colored tile that frames an icon you pass as `children`, in soft, outline, primary, and the semantic accent colors.

Everything is sized in `em` from a single `size`, so the tile and its icon scale together. Use a preset (`2xs`–`2xl`) or a pixel number, and pick a `rounded` square or a full `circle` with `shape`.

## Examples [#examples]

### Default [#default]

The image variant. The `src` is cropped to fill the square; give it an `alt` that describes the picture.

```tsx
import { Thumbnail } from '@appica/ui-react/thumbnail'

export default function ThumbnailDefault() {
  return <Thumbnail size="xl" src="/thumbnail.jpg" alt="Snow-capped mountain peak at sunset" />
}
```

### Sizes [#sizes]

Seven presets run from `2xs` to `2xl`. Because the tile is sized in `em`, a fallback or icon inside scales right along with it.

```tsx
import { Thumbnail } from '@appica/ui-react/thumbnail'

const sizes = ['2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl'] as const

export default function ThumbnailSizes() {
  return (
    <div className="flex flex-wrap items-end justify-center gap-4">
      {sizes.map((size) => (
        <Thumbnail key={size} size={size} src="/thumbnail.jpg" alt="Mountain peak" />
      ))}
    </div>
  )
}
```

### Custom size [#custom-size]

Pass a number to `size` for an exact pixel dimension when a preset doesn't fit — it drives the tile's `font-size`, so everything inside stays proportional.

```tsx
import { Thumbnail } from '@appica/ui-react/thumbnail'

export default function ThumbnailCustomSize() {
  return (
    <div className="flex items-end gap-4">
      <Thumbnail size={40} src="/thumbnail.jpg" alt="Mountain peak" />
      <Thumbnail size={56} src="/thumbnail.jpg" alt="Mountain peak" />
      <Thumbnail size={88} src="/thumbnail.jpg" alt="Mountain peak" />
    </div>
  )
}
```

### Shape [#shape]

`shape="rounded"` (the default) is a squircle-style rounded square; `shape="circle"` clips the image to a full circle.

```tsx
import { Thumbnail } from '@appica/ui-react/thumbnail'

export default function ThumbnailShape() {
  return (
    <div className="flex items-center gap-4">
      <Thumbnail size="xl" shape="rounded" src="/thumbnail.jpg" alt="Mountain peak" />
      <Thumbnail size="xl" shape="circle" src="/thumbnail.jpg" alt="Mountain peak" />
    </div>
  )
}
```

### Icon variants [#icon-variants]

The `icon-*` variants drop the image and frame an icon child instead — `icon-soft` and `icon-outline` for neutral tiles, `icon-primary` and `icon-primary-outline` for the brand color, and `icon-secondary` / `icon-error` / `icon-success` / `icon-warning` / `icon-info` for the semantic accents.

```tsx
import { Thumbnail } from '@appica/ui-react/thumbnail'
import { Folder, ImageSquareRounded, Bolt, Heart, Star, Trash, Check, Bell, Mail } from '@appica/icons-react'

const variants = [
  { variant: 'icon-soft', icon: <Folder /> },
  { variant: 'icon-outline', icon: <ImageSquareRounded /> },
  { variant: 'icon-primary', icon: <Bolt /> },
  { variant: 'icon-primary-outline', icon: <Heart /> },
  { variant: 'icon-secondary', icon: <Star /> },
  { variant: 'icon-error', icon: <Trash /> },
  { variant: 'icon-success', icon: <Check /> },
  { variant: 'icon-warning', icon: <Bell /> },
  { variant: 'icon-info', icon: <Mail /> },
] as const

export default function ThumbnailIconVariants() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-4">
      {variants.map(({ variant, icon }) => (
        <Thumbnail key={variant} variant={variant} size="lg">
          {icon}
        </Thumbnail>
      ))}
    </div>
  )
}
```

### Fallback [#fallback]

When the image is missing or fails to load, the image variant falls back to a built-in placeholder icon — no broken-image glyph ever flashes.

```tsx
import { Thumbnail } from '@appica/ui-react/thumbnail'

export default function ThumbnailFallback() {
  return <Thumbnail size="xl" src="/thumbnail-does-not-exist.jpg" alt="Unavailable image" />
}
```

### Media list [#media-list]

Thumbnails shine next to text. Mix an image tile with icon tiles down a list so files, folders, and media share one consistent rhythm.

```tsx
import { Thumbnail } from '@appica/ui-react/thumbnail'
import { Folder, Layout2, Music } from '@appica/icons-react'

const items = [
  {
    thumb: <Thumbnail size="md" src="/thumbnail.jpg" alt="Mountain peak" />,
    title: 'Summit.jpg',
    meta: 'Image · 2.4 MB',
  },
  {
    thumb: (
      <Thumbnail size="md" variant="icon-soft">
        <Folder />
      </Thumbnail>
    ),
    title: 'Field notes',
    meta: 'Folder · 18 items',
  },
  {
    thumb: (
      <Thumbnail size="md" variant="icon-primary">
        <Music />
      </Thumbnail>
    ),
    title: 'Trailhead.mp3',
    meta: 'Audio · 3:42',
  },
  {
    thumb: (
      <Thumbnail size="md" variant="icon-secondary">
        <Layout2 />
      </Thumbnail>
    ),
    title: 'Moodboard',
    meta: 'Album · 24 photos',
  },
]

export default function ThumbnailMediaList() {
  return (
    <ul className="divide-border border-border w-full max-w-90 divide-y rounded-xl border">
      {items.map((item) => (
        <li key={item.title} className="flex items-center gap-3 px-4 py-3">
          {item.thumb}
          <div className="flex flex-col">
            <span className="text-foreground-intense text-sm font-medium">{item.title}</span>
            <span className="text-foreground-muted text-xs">{item.meta}</span>
          </div>
        </li>
      ))}
    </ul>
  )
}
```

### Next.js Image [#nextjs-image]

To serve an optimized image, pass a Next.js `<Image>` to the `render` prop. The wrapper lifts the `src` so the preloader still drives the fallback, and applies its own `object-cover` and rounded-corner classes. Give the `Image` a `width`/`height` matching the tile's pixel size.

```tsx
import Image from 'next/image'
import { Thumbnail } from '@appica/ui-react/thumbnail'

export default function ThumbnailNextImage() {
  return (
    <Thumbnail
      size="2xl"
      render={<Image src="/thumbnail.jpg" alt="Snow-capped mountain peak at sunset" width={80} height={80} />}
    />
  )
}
```

### Loading [#loading]

`onLoadingStatusChange` reports the image's lifecycle, so you can overlay a [`Skeleton`](/ui/components/react/skeleton) while it loads and drop it once the photo arrives. Press **Reload** to fetch a fresh copy and watch it again.

```tsx
'use client'

import { useState, useEffect } from 'react'
import { Thumbnail } from '@appica/ui-react/thumbnail'
import { Skeleton } from '@appica/ui-react/skeleton'
import { Button } from '@appica/ui-react/button'

export default function ThumbnailLoading() {
  const [src, setSrc] = useState<string>()
  const [loaded, setLoaded] = useState(false)
  const [nonce, setNonce] = useState(0)

  useEffect(() => {
    setSrc(undefined)
    setLoaded(false)
    // Demo only: defer the image ~2.5s so the loading skeleton stays visible.
    const id = setTimeout(() => setSrc(`/thumbnail.jpg?reload=${nonce}`), 2500)
    return () => clearTimeout(id)
  }, [nonce])

  return (
    <div className="flex flex-col items-center gap-4">
      <div className="relative inline-flex">
        <Thumbnail
          size="2xl"
          alt="Mountain peak"
          onLoadingStatusChange={(status) => setLoaded(status === 'loaded')}
          {...(src ? { src } : {})}
        />
        {!loaded && <Skeleton className="absolute inset-0 rounded-[calc(tan(atan2(var(--radius-md),2.5rem))*100%)]" />}
      </div>
      <Button variant="outline" size="sm" onClick={() => setNonce((n) => n + 1)} disabled={!src}>
        Reload
      </Button>
    </div>
  )
}
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your tree in `DirectionProvider` so direction-aware behavior follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

The tile itself is symmetric, so it looks the same in either direction; what flips is the surrounding layout — in a media row the thumbnail moves to the right and the text to its left. For setup details and caveats, see the [RTL guide](/ui/docs/react/rtl).

<RtlPreview component="thumbnail" />

## API reference [#api-reference]

`Thumbnail` is a single component. For the image variant it builds on [Base UI's Avatar](https://base-ui.com/react/components/avatar) to preload the source and manage the fallback; the icon variants render a plain tile. It forwards `ref` and remaining attributes to the root element.

| Prop                    | <ColMinWidth width="210">Type</ColMinWidth>                                                                                                                                              | Default     | <ColMinWidth width="210">Description</ColMinWidth>                                 |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------- |
| `variant`               | <Code>'image' \| 'icon-soft' \| 'icon-outline' \| 'icon-primary' \| 'icon-primary-outline' \| 'icon-secondary' \| 'icon-error' \| 'icon-success' \| 'icon-warning' \| 'icon-info'</Code> | `'image'`   | Image tile, or a colored icon tile.                                                |
| `size`                  | <Code>'2xs' \| 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl' \| number</Code>                                                                                                            | `'md'`      | A preset scale, or a pixel number for an exact size.                               |
| `shape`                 | <Code>'rounded' \| 'circle'</Code>                                                                                                                                                       | `'rounded'` | Rounded square or full circle.                                                     |
| `src`                   | `string`                                                                                                                                                                                 | —           | Image URL (image variant). Lifted from `render` when omitted.                      |
| `alt`                   | `string`                                                                                                                                                                                 | `''`        | Alternative text describing the image. Defaults to `""` (decorative) when omitted. |
| `children`              | `ReactNode`                                                                                                                                                                              | —           | The icon to frame (icon variants).                                                 |
| `render`                | <Code>ReactElement \| ((props, state) => ReactElement)</Code>                                                                                                                            | —           | Swap the `<img>` for another element — e.g. a Next.js `<Image>`.                   |
| `onLoadingStatusChange` | <Code>(status: 'idle' \| 'loading' \| 'loaded' \| 'error') => void</Code>                                                                                                                | —           | Fires as the image moves through its loading lifecycle.                            |
| `className`             | `string`                                                                                                                                                                                 | —           | Extra classes, merged via `tailwind-merge`.                                        |

## Accessibility [#accessibility]

* For a meaningful image, give it an `alt` that describes the picture; for a purely decorative thumbnail, use `alt=""`.
* The icon variants are decorative tiles — the icon child carries no accessible name on its own, so make sure the surrounding context (a filename, a label) names what the thumbnail represents.
* The built-in fallback icon is marked `aria-hidden`; the `alt` text is what's announced when the image can't load.
* Pass an explicit `width`/`height` to a `render`ed Next.js `<Image>` so the layout doesn't shift while the image loads.
