# Media component

The media component shows an asset from the CDN in a way that fits its moderation status: the media for allowed assets, a notice and a reveal button for age-restricted ones, and an unavailable notice for blocked and purged ones. [Widgets](/docs/widgets) covers installing it, styling and translating.

## Show an asset

Pass its project, ID and stored moderation status, or an asset from the API as `asset`, which also supplies its media type and stored preview (placeholder, average colour and size); props you set win over the asset's values. Pass `canView` when your app lets this viewer see age-restricted media:

**React**

```tsx
import type { AssetModerationStatus } from "@calmlens/sdk/app";
import { CalmLensMedia } from "@calmlens/sdk/react";

type PostPhotoProps = {
  projectId: string;
  assetId: string;
  status: AssetModerationStatus;
  alt: string;
  /** Width divided by height, which you store when the image is uploaded. */
  aspectRatio: number;
  canView: boolean;
  onReveal: () => void;
};

export function PostPhoto({
  projectId,
  assetId,
  status,
  alt,
  aspectRatio,
  canView,
  onReveal,
}: PostPhotoProps) {
  return (
    <CalmLensMedia
      projectId={projectId}
      assetId={assetId}
      status={status}
      canView={canView}
      onReveal={onReveal}
      alt={alt}
      aspectRatio={aspectRatio}
      widths={[400, 800]}
      sizes="(min-width: 768px) 50vw, 100vw"
    />
  );
}
```

**Vue**

```vue
<script setup lang="ts">
import type { AssetModerationStatus } from "@calmlens/sdk/app";
import { CalmLensMedia } from "@calmlens/sdk/vue";

defineProps<{
  projectId: string;
  assetId: string;
  status: AssetModerationStatus;
  alt: string;
  /** Width divided by height, which you store when the image is uploaded. */
  aspectRatio: number;
  canView: boolean;
}>();
const emit = defineEmits<{ reveal: [] }>();
</script>

<template>
  <CalmLensMedia
    :project-id="projectId"
    :asset-id="assetId"
    :status="status"
    :can-view="canView"
    :alt="alt"
    :aspect-ratio="aspectRatio"
    :widths="[400, 800]"
    sizes="(min-width: 768px) 50vw, 100vw"
    @reveal="emit('reveal')"
  />
</template>
```

**Svelte**

```svelte
<script lang="ts">
  import type { AssetModerationStatus } from "@calmlens/sdk/app";
  import { CalmLensMedia } from "@calmlens/sdk/svelte";

  type Props = {
    projectId: string;
    assetId: string;
    status: AssetModerationStatus;
    alt: string;
    /** Width divided by height, which you store when the image is uploaded. */
    aspectRatio: number;
    canView: boolean;
    onReveal: () => void;
  };
  let { projectId, assetId, status, alt, aspectRatio, canView, onReveal }: Props = $props();
</script>

<CalmLensMedia
  {projectId}
  {assetId}
  {status}
  {canView}
  {onReveal}
  {alt}
  {aspectRatio}
  widths={[400, 800]}
  sizes="(min-width: 768px) 50vw, 100vw"
/>
```

**Plain JavaScript**

```javascript
import "@calmlens/sdk/elements";

// aspectRatio is width divided by height, which you store when the image is uploaded.
export function renderPostPhoto(
  container,
  { projectId, assetId, status, alt, aspectRatio, canView, onReveal },
) {
  const media = document.createElement("calmlens-media");
  media.setAttribute("project-id", projectId);
  media.setAttribute("asset-id", assetId);
  media.setAttribute("status", status);
  media.setAttribute("alt", alt);
  media.setAttribute("aspect-ratio", String(aspectRatio));
  media.setAttribute("widths", "400,800");
  media.setAttribute("sizes", "(min-width: 768px) 50vw, 100vw");
  media.toggleAttribute("can-view", canView);
  media.addEventListener("calmlens-reveal", onReveal);
  container.replaceChildren(media);
}
```

| Status                                                      | What the component shows                                                                                                        |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `allowed`, `limited` or `pending`                           | The media, with the blurred placeholder until it loads                                                                          |
| `age-restricted`                                            | The placeholder with a notice and a reveal button until you pass `canView`, then the media with the `allowAgeRestricted` opt-in |
| `blocked` or `purged`                                       | An unavailable notice, without requesting the media                                                                             |
| Any status, when an `asset`'s `content.status` is `removed` | An unavailable notice, without requesting the media                                                                             |

The reveal button calls `onReveal`, where your app runs its own check before rendering again with `canView`; [Show sensitive media](/docs/guides/sensitive-media) covers that decision. A media URL that fails to load also shows the unavailable notice.

- Images come from the asset's rendered resource, so the CDN serves the best format the browser accepts without a list of formats. `widths` and `sizes` add a `srcset`, so browsers fetch the smallest image that fits. Each width is a separate image transform; see [Lenses and transforms](/docs/guides/lenses).
- `lens` and `transform` apply a named lens or a direct transform.
- `aspectRatio` (or `width` and `height`) keeps the box its final size while the image loads.
- `placeholderHash` takes the asset's `content.image.thumbhash` and draws the placeholder in the browser, with no placeholder request. It also works for private assets and signed URLs.
- `url` takes a signed URL from your server instead of building a public CDN URL, for private assets or when CalmLens must refuse age-restricted content to anyone your backend hasn't approved.
- `type` is `image` (default), `video` or `audio`. Video and audio play the original file, with no poster or placeholder.

## React Native

`CalmLensMedia` from `@calmlens/sdk/react-native` shows assets the same way with React Native's `Image`. React Native has no `srcset`, so the component measures itself and loads the smallest of `widths` that covers its width in device pixels. Give it a size with `aspectRatio`, `width` and `height`, or an `asset` that has them, or a `style`; otherwise it keeps a minimum height while the image loads.

React Native has no built-in video or audio player. Pass `renderPlayer` to play them, for example with `expo-video`; without it, video and audio show only their placeholder:

```tsx
import { CalmLensMedia, type MediaAsset, type MediaPlayerSource } from "@calmlens/sdk/react-native";
import { useVideoPlayer, VideoView } from "expo-video";
import { StyleSheet } from "react-native";

function Player({ src }: MediaPlayerSource) {
  const player = useVideoPlayer(src);
  return <VideoView player={player} style={StyleSheet.absoluteFill} nativeControls />;
}

export function PostMedia({ asset, alt }: { asset: MediaAsset; alt: string }) {
  return (
    <CalmLensMedia
      asset={asset}
      widths={[320, 640, 1080]}
      alt={alt}
      renderPlayer={(source) => <Player {...source} />}
    />
  );
}
```

`sizes`, `lazy`, `fetchPriority`, `position` and `controls` are web-only and not available in React Native.
