Media Resources¶
MediaResourceRenderer is the unified component for rendering all server-managed media (images and videos). Use it whenever the resource comes from the API.
When to Use¶
| Source | Component |
|---|---|
API MediaResource object |
MediaResourceRenderer |
Static local asset (/public/) |
SafeImage (see Images) |
| Raw CDN path string | SafeImage with getServerFileUrl() |
Quick Start¶
import { MediaResourceRenderer } from "@/components/MediaResource";
<MediaResourceRenderer
resource={product.heroImage}
context="card"
/>
context Prop¶
Sets sensible defaults for the five standard placement types. All defaults can be overridden with explicit props.
| Context | mode default |
aspectRatio default |
objectFit default |
Gallery |
|---|---|---|---|---|
thumbnail |
static |
1/1 |
cover |
enabled |
card |
static |
4/3 |
cover |
enabled |
preview |
static |
auto |
contain |
enabled |
modal |
interactive |
auto |
contain |
disabled |
inline |
static |
auto |
contain |
disabled |
mode Prop¶
| Mode | Behavior |
|---|---|
static |
Display only — image or video renders with no interaction layer |
interactive |
Adds zoom/pan/rotate, a drawing toolbar, and comment pin support |
Props Reference¶
| Prop | Type | Default | Description |
|---|---|---|---|
resource |
MediaResourceMinimal \| MediaResourcePublic |
required | The media resource to render |
context |
"thumbnail" \| "card" \| "preview" \| "modal" \| "inline" |
"card" |
Sets rendering defaults (see above) |
mode |
"static" \| "interactive" |
from context | Display-only or full interactive mode |
aspectRatio |
string |
from context | CSS aspect-ratio (e.g. "16/9", "auto") |
forceAspectRatio |
boolean |
false |
Enforce aspect ratio even when resource dimensions are known |
objectFit |
"cover" \| "contain" \| "fill" |
from context | CSS object-fit for the media element |
preset |
ImagePreset |
from context | Override the SafeImage sizing preset |
fill |
boolean |
true |
Stretch to fill the container |
width |
number |
— | Fixed width (disables fill) |
height |
number |
— | Fixed height (disables fill) |
priority |
boolean |
false |
Pass priority to next/image for LCP images |
thumbnailUrl |
string |
— | Override the displayed URL (useful for custom previews) |
actions |
Action<MediaResourceInput>[] |
— | Context-menu actions shown on right-click |
slots |
SlotDefinition[] |
— | Hover-only overlay content (see Slots) |
persistentSlots |
SlotDefinition[] |
— | Always-visible overlay content (see Slots) |
showCaption |
boolean |
false |
Show filename caption below the media |
hoverPreview |
boolean |
false |
Show a tooltip preview on hover |
className |
string |
— | Container class |
mediaClassName |
string |
— | Class applied to the <img> or <video> element |
toolbarClassName |
string |
— | Class applied to the interactive toolbar |
galleryKey |
string |
— | Group key for the lightbox gallery (see Gallery) |
disableGallery |
boolean |
false |
Opt out of gallery entirely |
hideHoverInfo |
boolean |
false |
Suppress the metadata hover overlay |
annotationContextType |
string |
— | Entity type for comment pins (requires mode="interactive") |
annotationContextId |
string |
— | Entity ID for comment pins (requires mode="interactive") |
disableDrawing |
boolean |
— | Suppress drawing toolbar in interactive mode |
disableCommentPins |
boolean |
— | Suppress comment pins in interactive mode |
disableInitialWheelZoom |
boolean |
— | Don't auto-engage wheel-zoom on mount |
onToolActiveChange |
(active: boolean) => void |
— | Called when a drawing/annotation tool activates |
onClick |
(resource: MediaResourceInput, e: React.MouseEvent) => void |
— | Custom click handler (overrides gallery open) |
onLoad |
(e: SyntheticEvent) => void |
— | Fired when media loads successfully |
onError |
(e: SyntheticEvent) => void |
— | Fired on load error |
onContextMenu |
(resource: MediaResourceInput, e: React.MouseEvent) => void |
— | Called on right-click alongside the action menu (does not replace it) |
Action Menu¶
Default behavior¶
actions is optional. When omitted, the renderer automatically calls useMediaResourceActions(resource) internally and uses the result as the context-menu action list. To disable the context menu entirely, pass actions={[]}.
Usage patterns¶
1. Use defaults as-is (omit actions)
// No actions prop needed — the renderer wires the standard set automatically
<MediaResourceRenderer resource={resource} context="card" />
2. Extend defaults via useMediaResourceActions
import { useMediaResourceActions } from "@/hooks/actions/useMediaResourceActions";
const actions = useMediaResourceActions(resource, {
exclude: ["copyUrl"], // remove specific actions by ID
extraActions: [shareAction], // appended after built-in actions
});
<MediaResourceRenderer resource={resource} actions={actions} />
3. Replace entirely with a custom action list
import type { Action } from "@/types/actionMenu";
import type { MediaResourceInput } from "@/types/mediaResource";
const customActions: Action<MediaResourceInput>[] = [
{
type: "action",
id: "share",
label: "Share",
icon: Share2,
onSelect: ({ data }) => shareResource(data),
},
];
<MediaResourceRenderer resource={resource} actions={customActions} />
Worked example — adding a share action via extraActions¶
import { Share2 } from "lucide-react";
import type { StandardAction } from "@/types/actionMenu";
import type { MediaResourceInput } from "@/types/mediaResource";
import { useMediaResourceActions } from "@/hooks/actions/useMediaResourceActions";
const shareAction: StandardAction<MediaResourceInput> = {
type: "action",
id: "share",
label: "Share",
icon: Share2,
onSelect: ({ data }) => {
navigator.clipboard.writeText(data.url);
},
};
function MyCard({ resource }: { resource: MediaResourceInput }) {
const actions = useMediaResourceActions(resource, {
extraActions: [shareAction],
});
return <MediaResourceRenderer resource={resource} actions={actions} />;
}
Default action IDs¶
| ID | Type | Description |
|---|---|---|
openInNewTab |
StandardAction |
Opens the raw CDN URL in a new browser tab |
download |
CustomAction (images) / StandardAction (video) |
Images: format selector split-button (webp/png/jpeg); video: direct download |
copyUrl |
StandardAction |
Copies the CDN URL to the clipboard |
| — | SeparatorAction |
Visual divider (id: "clipboard-separator") |
toggleClipboard |
StandardAction |
Adds or removes the resource from the media clipboard |
metadata |
CustomAction |
Inline metadata panel (always rendered last) |
Slots¶
slots and persistentSlots accept SlotDefinition[] to place overlay content inside the renderer:
type SlotDefinition = {
id: string;
content: React.ReactNode;
position?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
className?: string;
};
slots— visible only while the user hovers over the mediapersistentSlots— always visible regardless of hover state
Hover Metadata Overlay¶
When hovering, a tooltip displays resource metadata automatically. Fields shown:
- Type, resolution, aspect ratio, orientation
- File size, format, duration (video)
- Color profile, creation date
Disable with hideHoverInfo={true}.
buildMetadataRows(resource, overrides?)¶
Returns a MetadataRow[] array for building custom metadata displays outside the renderer:
import { buildMetadataRows } from "@/components/MediaResource/utils/buildMetadataRows";
const rows = buildMetadataRows(resource, {
resolution: "Custom label",
});
Gallery Integration¶
Multiple MediaResourceRenderer instances sharing a galleryKey are grouped into a single lightbox gallery. Clicking any instance opens the full-screen viewer with navigation between all items in the group.
// Group all product images in one gallery
{product.images.map((img) => (
<MediaResourceRenderer
key={img.id}
resource={img}
galleryKey="product-gallery"
/>
))}
- Single image — omit
galleryKey; a per-instance gallery is created automatically - Opt out — set
disableGallery={true}
Annotations¶
Enable comment pins and drawing tools in interactive mode:
<MediaResourceRenderer
resource={revision.outputImage}
mode="interactive"
context="modal"
annotationContextType="shot_revision"
annotationContextId={revision.id}
/>
Both annotationContextType and annotationContextId must be provided together. Individual tools can be suppressed with disableDrawing or disableCommentPins.
Driver System¶
The renderer auto-detects the resource type and delegates to the appropriate driver:
| Driver | Activated when |
|---|---|
ImageDriver |
resource.mediaType is image |
VideoDriver |
resource.mediaType is video |
Drivers handle the underlying SafeImage/<video> element, zoom state, and interactive tool wiring. You do not interact with drivers directly.
Source Files¶
| File | Contents |
|---|---|
src/components/MediaResource/MediaResourceRenderer.tsx |
Main renderer component |
src/components/MediaResource/drivers/ImageDriver.tsx |
Image rendering + zoom/pan |
src/components/MediaResource/drivers/VideoDriver.tsx |
Video rendering |
src/components/MediaResource/interactive/InteractiveToolbar.tsx |
Drawing + annotation toolbar |
src/components/MediaResource/interactive/CommentPinOverlay.tsx |
Comment pin layer |
src/components/MediaResource/utils/buildMetadataRows.ts |
Metadata row builder |
src/components/MediaResource/utils/formatters.ts |
Size/duration formatters |
src/components/MediaResource/MediaResourceOverlay.tsx |
Hover metadata overlay |
src/components/MediaResource/index.ts |
Public exports |
src/hooks/actions/useMediaResourceActions.tsx |
Standard action set hook |