@mad-core/react
v0.5.2
Published
React components for serving responsive, format-optimized images, videos, Lottie animations, and Rive animations from a MAD server
Maintainers
Readme
@mad-core/react
React components for serving responsive, format-optimized images, videos, Lottie, and Rive animations from a MAD (Media Assets Distributor) server.
Features
<MadImage>— renders a<picture>element with automatic<source>generation for modern formats (avif, webp)<MadVideo>— renders a<video>element with<source>elements for format negotiation (webm, mp4) and optional poster screenshots<MadLottie>— renders a Lottie animation (.json/.lottie) via canvas, with autoplay/loop control<MadRive>— renders a Rive animation (.riv) via canvas, with artboard/state-machine/fit control<MadGallery>— fetches a published gallery and renders its items with a render-propuseMadImage/useMadVideo/useMadAnimation/useMadGallery— hooks for building memoized asset URLs or fetching a gallerybuildMadUrl/buildMadVideoUrl/buildMadAnimationUrl/buildMadGalleryUrl— low-level utilities for constructing MAD URLs- Art direction — breakpoint-aware responsive sources via the
sourcesprop - Path-based URLs — DAM-compatible URL format (
/image/w/800/f/webp/my-slug-a7f3b2,/video/f/webm/w/1280/my-slug-a7f3b2,/animation/my-slug-a7f3b2) - Tree-shakeable — ESM-only,
sideEffects: false - TypeScript-first — full type definitions with JSDoc documentation
- Zero required runtime dependencies — only
reactas a required peer;@lottiefiles/dotlottie-weband@rive-app/canvasare optional peers (needed only if you use<MadLottie>/<MadRive>respectively)
Installation
# npm
npm install @mad-core/react
# pnpm
pnpm add @mad-core/react
# yarn
yarn add @mad-core/reactreact 18 or 19 is required as a peer dependency.
Build output
The published dist is ES modules targeting ES2018, with the sources embedded in the sourcemaps (inlineSources). It is not transpiled any further.
Object rest and object spread therefore reach your bundle as ES2018 syntax. Optional chaining and nullish coalescing are downlevelled, so the output still parses under bundlers and Babel configs that predate ES2020.
If your bundler excludes node_modules from transpilation — a common default — that ES2018 syntax ships as-is to your users. Anything that runs React 18 supports it (Chrome 60+, Firefox 55+, Safari 11.1+, Edge 79+). To target lower, put this package through your own transpilation step.
Quick Start
import { MadImage } from "@mad-core/react";
function Hero() {
return (
<MadImage
slug="hero-banner-a7f3b2"
baseUrl="https://cdn.example.com"
widths={[400, 800, 1200, 1600]}
sizes="(max-width: 768px) 100vw, 50vw"
alt="Hero banner"
loading="lazy"
/>
);
}URL Format
MAD uses path-based transform parameters (DAM-compatible):
https://cdn.example.com/image/w/800/h/600/f/webp/q/80/c/cover/hero-banner-a7f3b2
https://cdn.example.com/video/w/1280/h/720/f/webm/demo-video-a7f3b2
──────────────────────────────── ─────────────────
key/value pairs (any order) slug (always last)API Reference
<MadImage>
Renders a responsive <picture> element with optimized format sources.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| slug | string | — | Required. MAD asset slug |
| baseUrl | string | — | Required. MAD server base URL |
| params | MadTransformParams | {} | Base transform params applied to all URLs |
| widths | number[] | — | Responsive srcset widths |
| sizes | string | — | HTML sizes attribute |
| formats | Array<"avif" \| "webp" \| "jpg" \| "png"> | ["avif", "webp"] | Format priority list |
| sources | MadSource[] | — | Art direction sources |
| ...rest | ImgHTMLAttributes | — | Passed to the fallback <img> (alt, className, loading, etc.) |
Art direction example
<MadImage
slug="hero-banner-a7f3b2"
baseUrl="https://cdn.example.com"
widths={[400, 800]}
sizes="100vw"
sources={[
{
media: "(min-width: 1024px)",
params: { w: 1200, p: "center" },
widths: [1200, 1800, 2400],
sizes: "80vw",
},
]}
alt="Responsive hero"
/>A ref is forwarded to the wrapping <picture> element.
useMadPicture(input: MadPictureInput): MadPictureResult
The headless layer: everything <MadImage> knows about URLs, srcset, formats and art direction,
as plain props. <MadImage> is a thin render on top of it, so a fix here reaches both it and any
markup you write yourself.
const { sources, img } = useMadPicture({
slug: "hero-a7f3b2",
baseUrl: "https://cdn.example.com",
widths: [400, 800, 1200],
sizes: "(max-width: 768px) 100vw, 50vw",
params: { fp: "auto" },
});
return (
<picture ref={myRef}>
{sources.map(({ key, ...props }) => (
<source key={key} {...props} />
))}
<img {...img} alt="Hero" className="rounded" />
</picture>
);sources comes in render order and that order matters: art-direction sources (the ones with a
media) first, then one per format — the browser takes the first <source> that matches. The
fallback img.src carries no forced format, so the server negotiates it.
A source may also carry its own slug, for multi-asset art direction — when the mobile version is a
different photograph, not a crop of the desktop one:
sources={[
{ media: "(max-width: 640px)", params: { c: "cover" }, slug: "hero-mobile-b2c3d4" },
]}The fallback <img> always stays on the base asset: it is what a browser without <picture> support
gets. A source still needs widths — its own or the global ones — or it is skipped.
buildMadPicture is the same function without React, for SSR or a non-React renderer. The hook is
buildMadPicture memoized on the value of its input, so passing object literals inline — the
usual call site — does not hand consumers a new result on every parent render.
useMadImage(params: MadUrlParams): UseMadImageResult
Hook that returns a memoized image URL and a srcSet builder function. Use this when you need full control over the rendered markup.
import { useMadImage } from "@mad-core/react";
function Avatar({ slug }: { slug: string }) {
const { url, srcSet } = useMadImage({
slug,
baseUrl: "https://cdn.example.com",
w: 128,
f: "webp",
});
return (
<img
src={url}
srcSet={srcSet([128, 256, 384])}
sizes="128px"
alt="User avatar"
/>
);
}| Return field | Type | Description |
|-------------|------|-------------|
| url | string | Fully-qualified MAD image URL |
| srcSet | (widths: number[]) => string | Generates a srcset string for the given widths |
buildMadUrl(params: MadUrlParams): string
Low-level utility to build a MAD image URL with path-based transform parameters.
import { buildMadUrl } from "@mad-core/react";
const url = buildMadUrl({
slug: "hero-banner-a7f3b2",
baseUrl: "https://cdn.example.com",
w: 800,
f: "webp",
q: 80,
});
// => "https://cdn.example.com/image/w/800/f/webp/q/80/hero-banner-a7f3b2"<MadVideo>
Renders a <video> element with <source> elements for format negotiation, serving optimized video from a MAD server.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| slug | string | — | Required. MAD asset slug |
| baseUrl | string | — | Required. MAD server base URL |
| params | MadVideoTransformParams | {} | Base transform params applied to all URLs |
| formats | Array<"webm" \| "mp4" \| "ogg"> | ["webm", "mp4"] | Format priority list |
| poster | number \| false | — | Time in seconds for a poster screenshot (omit or false to disable) |
| ...rest | VideoHTMLAttributes | — | Passed to <video> (autoPlay, muted, loop, controls, className, etc.) |
<MadVideo
slug="demo-video-a7f3b2"
baseUrl="https://cdn.example.com"
params={{ w: 1280, h: 720 }}
poster={2}
autoPlay muted loop
/>useMadVideo(params: MadVideoUrlParams): UseMadVideoResult
Hook that returns a memoized video URL and a posterUrl builder function. Use this when you need full control over the rendered markup.
import { useMadVideo } from "@mad-core/react";
function Player({ slug }: { slug: string }) {
const { url, posterUrl } = useMadVideo({
slug,
baseUrl: "https://cdn.example.com",
w: 1280,
f: "webm",
});
return <video src={url} poster={posterUrl(2)} controls />;
}| Return field | Type | Description |
|-------------|------|-------------|
| url | string | Fully-qualified MAD video URL |
| posterUrl | (time: number) => string | Generates a poster screenshot URL at the given seek time |
buildMadVideoUrl(params: MadVideoUrlParams): string
Low-level utility to build a MAD video URL with path-based transform parameters.
import { buildMadVideoUrl } from "@mad-core/react";
const url = buildMadVideoUrl({
slug: "demo-video-a7f3b2",
baseUrl: "https://cdn.example.com",
w: 1280,
f: "webm",
dur: 30,
});
// => "https://cdn.example.com/video/w/1280/f/webm/dur/30/demo-video-a7f3b2"<MadLottie>
Renders a Lottie animation (.json Bodymovin or .lottie dotLottie) served by MAD. The animation is drawn onto a <canvas> element by @lottiefiles/dotlottie-web, which is dynamically imported on mount.
Requires the optional peer dependency:
npm install @lottiefiles/dotlottie-web| Prop | Type | Default | Description |
|------|------|---------|-------------|
| slug | string | — | Required. MAD asset slug |
| baseUrl | string | — | Required. MAD server base URL |
| autoplay | boolean | false | Play immediately after load |
| loop | boolean | false | Loop playback |
| className | string | — | Class applied to the canvas |
| style | CSSProperties | — | Inline style for the canvas |
import { MadLottie } from "@mad-core/react";
<MadLottie
slug="hero-animation-a7f3b2"
baseUrl="https://cdn.example.com"
autoplay
loop
className="h-64 w-64"
/>useMadAnimation(params: MadAnimationUrlParams): UseMadAnimationResult
Hook that returns a memoized animation URL. Use this to wire up your own Lottie player.
import { useMadAnimation } from "@mad-core/react";
function MyPlayer({ slug }: { slug: string }) {
const { url } = useMadAnimation({
slug,
baseUrl: "https://cdn.example.com",
});
// feed `url` into lottie-web, dotlottie-react, etc.
}| Return field | Type | Description |
|-------------|------|-------------|
| url | string | Fully-qualified MAD animation URL |
buildMadAnimationUrl(params: MadAnimationUrlParams): string
Low-level utility to build a MAD animation URL. Works for both Lottie and Rive assets — the URL shape is the same; the asset's type field (from the management API) tells you which player to use.
import { buildMadAnimationUrl } from "@mad-core/react";
const url = buildMadAnimationUrl({
slug: "hero-animation-a7f3b2",
baseUrl: "https://cdn.example.com",
});
// => "https://cdn.example.com/animation/hero-animation-a7f3b2"<MadRive>
Renders a Rive animation (.riv) served by MAD. The animation is drawn onto a <canvas> element by @rive-app/canvas, which is dynamically imported on mount.
Requires the optional peer dependency:
npm install @rive-app/canvas| Prop | Type | Default | Description |
|------|------|---------|-------------|
| slug | string | — | Required. MAD asset slug |
| baseUrl | string | — | Required. MAD server base URL |
| artboard | string | first artboard | Artboard to activate |
| stateMachine | string | — | State machine to activate. Omit to play the default timeline |
| fit | "cover" \| "contain" \| "fill" \| "fitWidth" \| "fitHeight" \| "none" | "contain" | Layout fit mode |
| alignment | "center" \| "topLeft" \| "topCenter" \| … | "center" | Alignment within the fitted bounds |
| autoplay | boolean | false | Play immediately after load |
| className | string | — | Class applied to the canvas |
| style | CSSProperties | — | Inline style for the canvas |
Rive's loop behavior is configured inside the
.rivfile at export time, so there is noloopprop — the file's authored setting is authoritative.
import { MadRive } from "@mad-core/react";
<MadRive
slug="hero-animation-a7f3b2"
baseUrl="https://cdn.example.com"
stateMachine="main"
fit="cover"
autoplay
className="h-64 w-full"
/>Types
MadTransformParams
| Field | Type | Description |
|-------|------|-------------|
| w | number | Target width in pixels (clamped server-side to 3000) |
| h | number | Target height in pixels (clamped server-side to 3000) |
| f | MadImageFormat | Output format |
| q | number | Output quality (1–100) |
| c | MadFit | Resize fit mode |
| p | MadPosition | Crop position anchor |
| r | [x, y, w, h] | Region crop (extract) |
| t | MadTransform | Post-processing: "grayscale", "negate" or "normalize" |
| ro | number | Rotation in degrees, any finite angle (positive = clockwise); 0 opts out of a stored rotation |
| sh | number | Sharpen sigma, 0.3–10 |
| bl | number | Blur sigma, 0.3–100 |
| fl | MadFlip | Flip: "v" (vertical) or "h" (horizontal) |
| bg | string | Background hex color (3, 6, or 8 chars, no #) |
| br | number | Brightness, 0.1–10 (1 = neutral) |
| co | number | Contrast, 0–3 (1 = neutral) |
| sa | number | Saturation, 0–10 (1 = neutral) |
| filter | string | LUT filter by name, or "none" to override the one stored on the asset |
| fs | number | Filter strength, 0–100 |
| fpx | number | Focal point override, 0–1 from the left (needs c: "cover" + w + h + no p) |
| fpy | number | Focal point override, 0–1 from the top |
| src | MadDownscaleSource | Downscaled pipeline source for thumbnails: "medium" (≤1024) or "small" (≤512). Ignored when the request does not fit the bound |
| wm | MadTransformParams["wm"] | Watermark position "br" \| "bl" \| "tr" \| "tl" \| "center", or "pattern" to tile it diagonally (requires a watermark uploaded via admin) |
| wmo | number | Watermark opacity, 0–100 (default 50) |
| wms | number | Watermark scale as % of image width, 1–100 (default 20) |
| wmp | number | Pattern only: gap between repetitions as % of logo, 0–300 (default 70) |
| wmf | boolean | Pattern only: flip the diagonal to -45° |
Every key the server's /image route accepts is in this table — a test
(tests/url-contract.test.ts) reads them off the server's validator and fails if the two
ever drift apart.
MadUrlParams
Extends MadTransformParams with:
| Field | Type | Description |
|-------|------|-------------|
| slug | string | MAD asset slug |
| baseUrl | string | MAD server base URL |
MadSource
| Field | Type | Description |
|-------|------|-------------|
| media | string | CSS media query, e.g. "(min-width: 1024px)" |
| params | MadTransformParams | Transform overrides for this breakpoint |
| widths | number[] | srcset widths (overrides global) |
| sizes | string | HTML sizes attribute for this source |
MadPosition
"center" | "top" | "bottom" | "left" | "right"
MadFit
"cover" | "contain" | "fill" | "inside" | "outside"
MadImageFormat
"webp" | "avif" | "jpg" | "png" | "gif" | "auto"
"auto"enables content negotiation: the server inspects the client'sAcceptheader and picks the best format (avif > webp > jpg).
MadFlip
"v" | "h"
MadVideoTransformParams
| Field | Type | Description |
|-------|------|-------------|
| w | number | Target width in pixels |
| h | number | Target height in pixels |
| f | MadVideoFormat | Output format |
| time | number | Seek position in seconds (alone = JPEG screenshot) |
| dur | number | Duration in seconds (trim video) |
MadVideoUrlParams
Extends MadVideoTransformParams with:
| Field | Type | Description |
|-------|------|-------------|
| slug | string | MAD asset slug |
| baseUrl | string | MAD server base URL |
MadVideoFormat
"mp4" | "webm" | "ogg"
<MadGallery>
Fetches a published gallery from /gallery/:slug and renders its items with a render-prop. Uses plain fetch under the hood (no extra runtime deps); you can inject a custom fetcher to integrate with react-query, SWR, or SSR frameworks.
import { MadGallery } from "@mad-core/react";
function GalleryPage({ slug }: { slug: string }) {
return (
<MadGallery
slug={slug}
baseUrl="https://cdn.example.com"
className="grid grid-cols-3 gap-4"
renderLoading={() => <Spinner />}
renderError={(err) => <p>{err.message}</p>}
renderEmpty={() => <p>No items yet</p>}
renderItem={(item) =>
item.type === "image" ? (
<img src={item.thumbnailUrl ?? item.url} alt={item.altText ?? ""} />
) : item.type === "video" ? (
<video src={item.url} poster={item.posterUrl} controls />
) : null
}
/>
);
}| Prop | Type | Default | Description |
|------|------|---------|-------------|
| slug | string | — | Required. Gallery slug |
| baseUrl | string | — | Required. MAD server base URL |
| renderItem | (item, index) => ReactNode | — | Required. Maps each item to JSX |
| renderLoading | () => ReactNode | — | Shown while fetching |
| renderError | (err) => ReactNode | — | Shown on fetch error |
| renderEmpty | (gallery) => ReactNode | — | Shown when items is empty |
| fetcher | MadGalleryFetcher | fetch | Custom fetcher — e.g. react-query |
| className | string | — | Class applied to the wrapping <div> |
useMadGallery(params: UseMadGalleryParams): UseMadGalleryResult
Lower-level hook that fetches on mount and exposes { data, isLoading, error, refetch }. Unlike the other SDK hooks (which are pure URL builders), this one performs a network request because gallery payloads only exist server-side.
import { useMadGallery } from "@mad-core/react";
function List({ slug }: { slug: string }) {
const { data, isLoading, error, refetch } = useMadGallery({
slug,
baseUrl: "https://cdn.example.com",
});
// …render or invoke refetch()…
}| Return field | Type | Description |
|--------------|------|-------------|
| data | MadGalleryResponse \| null | Parsed payload (null while loading or on error) |
| isLoading | boolean | True while the fetch is in flight |
| error | Error \| null | Non-null when the fetch failed |
| refetch | () => void | Re-runs the fetch (aborts any in-flight request) |
buildMadGalleryUrl(params: MadGalleryUrlParams): string
Pure URL builder for SSR or query libs that want to own the fetch.
import { buildMadGalleryUrl } from "@mad-core/react";
const url = buildMadGalleryUrl({
slug: "summer-trip-a7f3b2",
baseUrl: "https://cdn.example.com",
});
// => "https://cdn.example.com/gallery/summer-trip-a7f3b2"MadGalleryResponse
| Field | Type | Description |
|-------|------|-------------|
| slug | string | Gallery slug |
| name | string | Display name |
| description | string \| null | — |
| namespace | string \| null | Namespace or null for global |
| coverUrl | string \| null | Thumbnail of the cover image (explicit or first image-type item) |
| publishedAt | string \| null | ISO 8601 timestamp; null = draft |
| items | MadGalleryItem[] | Ordered items |
MadGalleryItem
| Field | Type | Description |
|-------|------|-------------|
| type | "image" \| "video" \| "lottie" \| "rive" | Asset type |
| slug | string | MAD asset slug |
| url | string | Full delivery URL (filter/adjustments baked-in for images) |
| thumbnailUrl? | string | w/400/f/auto thumbnail (images only) |
| posterUrl? | string | Video poster JPEG at time/0 |
| duration? | number \| null | Video duration in seconds (if known) |
| altText | string \| null | Alt text for images / a11y |
| width | number \| null | Intrinsic width in px |
| height | number \| null | Intrinsic height in px |
| tags | string[] | Tags attached to the asset |
How It Works
<MadImage> renders a standard HTML <picture> element:
- For each art direction source (
sourcesprop), it generates<source>elements for every format in theformatslist, each with the appropriatemedia,type,srcSet, andsizesattributes. - For global responsive widths (
widthsprop), it generates<source>elements for each format (nomediaattribute). - A fallback
<img>element is rendered last with the base URL (no forced format), letting the browser negotiate the best option.
The browser evaluates sources top-to-bottom and picks the first match, giving you automatic format negotiation (avif > webp > fallback) with art-direction breakpoints.
<MadVideo> renders a standard HTML <video> element:
- For each format in the
formatslist, it generates a<source>element with the correspondingsrcandtypeattributes. - When
posteris set, it generates a poster URL using the MAD video screenshot endpoint (/video/time/{seconds}/slug), which returns a JPEG frame at the given seek position. - All remaining props (
autoPlay,muted,loop,controls, etc.) are passed through to the<video>element.
License
MIT
