---
title: Frame
description: An image frame primitive with aspect ratio control, composed with FrameLabelsWrapper and FrameTitle for overlaid content
---

```tsx
'use client';

import { Frame } from '@/ui/frame/Frame';
import { FrameLabelsWrapper } from '@/ui/frame/FrameLabelsWrapper';
import { FrameTitle } from '@/ui/frame/FrameTitle';

export function FrameDemo() {
  return (
    <Frame
      format="horizontal"
      className="max-w-380"
      image={{
        alt: 'Mountain landscape',
        src: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4',
      }}
    >
      <FrameLabelsWrapper hasImage>
        <FrameTitle className="w-full justify-center text-center" coverLink hasImage href="#">
          <div>
            <div className="text-b2 font-semibold">Mountain Resort</div>
            <div className="text-b3">Winter</div>
          </div>
        </FrameTitle>
      </FrameLabelsWrapper>
    </Frame>
  );
}
```

## Overview

Frame is a low-level primitive that renders an image inside an aspect-ratio container with rounded corners and overflow clipping. It is designed to be composed with `FrameLabelsWrapper` and `FrameTitle` to build image cards with overlaid content.

**When to use Frame:**
- You need an image card with custom overlaid content (tags, titles, buttons)
- You want full control over the layout inside the frame
- You need to combine multiple overlay elements

**Related components:**
- `FrameWrapper` — the base container without image logic, for custom content
- `FrameLabelsWrapper` — overlay container with optional gradient
- `FrameTitle` — clickable/static title bar inside a frame
- `SliderFrame` — multi-image carousel variant

## Installation

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/frame.json
```

Or manually, copy the following code into `/ui/frame/Frame.tsx`:

```tsx
import type { FunctionComponent } from 'react';

import { Image, type ImageProps } from '../Image';

import { FrameWrapper, type FrameWrapperProps } from './FrameWrapper';

export interface FrameProps extends FrameWrapperProps {
  image: ImageProps | null;
  sizes?: ImageProps['sizes'];
}

export const Frame: FunctionComponent<FrameProps> = ({ children, image, sizes, ...props }) => {
  return (
    <FrameWrapper {...props}>
      {image && (
        <Image {...image} className="h-full w-full object-cover" sizes={sizes ?? image.sizes} />
      )}
      {children}
    </FrameWrapper>
  );
};
```

## Usage

```tsx showLineNumbers
import { Frame } from '@/ui/frame/Frame';
import { FrameLabelsWrapper } from '@/ui/frame/FrameLabelsWrapper';
import { FrameTitle } from '@/ui/frame/FrameTitle';

export function Component() {
  return (
    <Frame
      format="horizontal"
      className="max-w-380"
      image={{
        alt: 'Mountain landscape',
        src: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4',
      }}
    >
      <FrameLabelsWrapper hasImage>
        <FrameTitle className="w-full justify-center text-center" coverLink hasImage href="#">
          <div className="text-b2 font-semibold">Mountain Resort</div>
        </FrameTitle>
      </FrameLabelsWrapper>
    </Frame>
  );
}
```

## API Reference

### Frame

| Prop        | Type                                                | Default        | Description                                               |
| ----------- | --------------------------------------------------- | -------------- | --------------------------------------------------------- |
| `image`     | `ImageProps \| null`                                | Required       | Image props passed to the Image component. `null` renders no image |
| `sizes`     | `string`                                            | `undefined`    | Responsive image sizes attribute, overrides `image.sizes` |
| `format`    | `"horizontal" \| "vertical" \| "square" \| "custom"` | `"horizontal"` | Aspect ratio of the frame                                 |
| `as`        | `"article" \| "div" \| "span"`                     | `"article"`    | HTML element to render                                    |
| `className` | `string`                                            | `undefined`    | Additional CSS classes                                    |
| `children`  | `ReactNode`                                         | `undefined`    | Overlay content (use FrameLabelsWrapper + FrameTitle)     |

## Examples

### With button

```tsx
'use client';

import { Button } from '@/ui/buttons/Button';
import { Frame } from '@/ui/frame/Frame';
import { FrameLabelsWrapper } from '@/ui/frame/FrameLabelsWrapper';
import { FrameTitle } from '@/ui/frame/FrameTitle';

export function FrameHorizontalDemo() {
  return (
    <Frame
      format="horizontal"
      className="max-w-380"
      image={{
        alt: 'Mountain landscape',
        src: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4',
      }}
    >
      <FrameLabelsWrapper hasImage>
        <FrameTitle className="w-full justify-between" coverLink hasImage href="#">
          <div>
            <div className="text-b2 font-semibold">Alpine Adventures</div>
            <div className="text-b3">Explore</div>
          </div>
          <Button
            aria-label="Explore Alpine Adventures"
            className="pointer-events-auto shrink-0"
            color="white"
            icon="ArrowDefaultRight"
            theme="outline"
            variant="circle"
          />
        </FrameTitle>
      </FrameLabelsWrapper>
    </Frame>
  );
}
```

### Vertical format

```tsx
'use client';

import { Button } from '@/ui/buttons/Button';
import { Frame } from '@/ui/frame/Frame';
import { FrameLabelsWrapper } from '@/ui/frame/FrameLabelsWrapper';
import { FrameTitle } from '@/ui/frame/FrameTitle';

export function FrameVerticalDemo() {
  return (
    <Frame
      format="vertical"
      className="max-w-280"
      image={{
        alt: 'Mountain landscape',
        src: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4',
      }}
    >
      <FrameLabelsWrapper hasImage>
        <FrameTitle className="w-full justify-between" coverLink hasImage href="#">
          <div>
            <div className="text-b2 font-semibold">Ski Resort</div>
            <div className="text-b3">Alpine</div>
          </div>
          <Button
            aria-label="Explore Ski Resort"
            className="pointer-events-auto shrink-0"
            color="white"
            icon="ArrowDefaultRight"
            theme="outline"
            variant="circle"
          />
        </FrameTitle>
      </FrameLabelsWrapper>
    </Frame>
  );
}
```

### With tag

```tsx
'use client';

import { Button } from '@/ui/buttons/Button';
import { Tag } from '@/ui/Tag';
import { Frame } from '@/ui/frame/Frame';
import { FrameLabelsWrapper } from '@/ui/frame/FrameLabelsWrapper';
import { FrameTitle } from '@/ui/frame/FrameTitle';

export function FrameWithTagDemo() {
  return (
    <Frame
      format="horizontal"
      className="max-w-380"
      image={{
        alt: 'Mountain landscape',
        src: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4',
      }}
    >
      <FrameLabelsWrapper hasImage>
        <Tag
          label="Best seller"
          className="mx-12 mt-12 self-start px-8 sm:mx-24 sm:mt-24 sm:px-16"
        />
        <FrameTitle className="w-full justify-between" coverLink hasImage href="#">
          <div>
            <div className="text-b2 font-semibold">Mountain Resort</div>
            <div className="text-b3">Winter</div>
          </div>
          <Button
            aria-label="Explore Mountain Resort"
            className="pointer-events-auto shrink-0"
            color="white"
            icon="ArrowDefaultRight"
            theme="outline"
            variant="circle"
          />
        </FrameTitle>
      </FrameLabelsWrapper>
    </Frame>
  );
}
```

### Without image

```tsx
'use client';

import { Button } from '@/ui/buttons/Button';
import { Tag } from '@/ui/Tag';
import { Frame } from '@/ui/frame/Frame';
import { FrameLabelsWrapper } from '@/ui/frame/FrameLabelsWrapper';
import { FrameTitle } from '@/ui/frame/FrameTitle';

export function FrameNoImageDemo() {
  return (
    <Frame format="horizontal" image={null} className="bg-saffron h-240 w-380">
      <FrameLabelsWrapper hasImage={false}>
        <Tag
          label="Partnership"
          border="ultramarine"
          color="white"
          backgroundColor="ultramarine"
          className="mx-12 mt-12 self-start px-8 sm:mx-24 sm:mt-24 sm:px-16"
        />
        <FrameTitle className="w-full justify-between" coverLink href="#">
          <div>
            <div className="text-b2 font-semibold">Special Offer</div>
            <div className="text-b3">Explore</div>
          </div>
          <Button
            aria-label="Explore Special Offer"
            className="pointer-events-auto shrink-0"
            color="black"
            icon="ArrowDefaultRight"
            theme="outline"
            variant="circle"
          />
        </FrameTitle>
      </FrameLabelsWrapper>
    </Frame>
  );
}
```

## Notes

- Frame renders as an `<article>` by default — use `as="div"` or `as="span"` for non-semantic contexts
- The image fills the frame with `object-cover`, preserving aspect ratio
- Pass `image={null}` to render the frame without a background image (e.g., with a background color via `className`)
- Overlay content should be wrapped in `FrameLabelsWrapper` to handle pointer events and the gradient background
