Frame

PreviousNext

An image frame primitive with aspect ratio control, composed with FrameLabelsWrapper and FrameTitle for overlaid content

'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

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

Usage

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

PropTypeDefaultDescription
imageImageProps | nullRequiredImage props passed to the Image component. null renders no image
sizesstringundefinedResponsive 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
classNamestringundefinedAdditional CSS classes
childrenReactNodeundefinedOverlay content (use FrameLabelsWrapper + FrameTitle)

Examples

With button

'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

'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

'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

'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