@joycostudio/iris
v0.0.1
Published
Analyze images and deliver the right version for each screen
Keywords
Readme
Iris
Iris turns image sources and usage context into a loading plan. It has three parts:
- Prepare assets: Find image files and hardcoded URLs in your code, inspect their metadata, then generate a catalog and small placeholders.
- Define a policy: Recipes describe loading stages. Rules choose a recipe using image metadata and how the image is used.
- Render the plan: The Next.js adapter applies the policy with Next Image. The core policy can also drive other renderers.
Next.js remains responsible for responsive sizes, optimization, and the final image request.
Get started
Requires Node.js 22.18+, Next.js 16+, and Sharp for image generation.
1. Install and initialize
npm i @joycostudio/iris
npm i -D sharp
npx iris init2. Set up iris.config.ts
import { createIris } from "@joycostudio/iris"
export default createIris({
assets: ["public/images/**/*.{png,jpg,jpeg,webp,avif,gif}"],
recipes: {
direct: { preview: "none" },
blur: { placeholder: "blur" },
staged: { placeholder: "blur", preview: { ratio: 0.5 } },
},
rules: [
{ when: { priority: true }, recipe: "direct" },
{ include: "/images/gallery/**", recipe: "staged" },
],
fallback: "blur",
})
export const generate = {
output: "src/generated/iris",
scan: ["src/**/*.{js,jsx,ts,tsx}", "app/**/*.{js,jsx,ts,tsx}"],
}assets catalogs files in public/images, including images no page uses yet. scan finds image URLs already written in your JS/TS files, including JSX like <Image src="/images/hero.png" />. The first matching rule chooses a recipe; fallback covers everything else.
3. Generate and use it
npx iris generateimport { IrisImage } from "@/generated/iris/image"
<IrisImage src="/images/hero.png" alt="Hero" fill sizes="100vw" priority />
<IrisImage src="/images/gallery/photo.jpg" alt="Photo" width={1200} height={800} />Use regular Next Image props. Run iris generate again when image sources or placeholder encoding settings change. An image missing from the catalog renders as ordinary Next Image.
Recipe and rules
Recipes
A recipe is a named loading behavior. It controls what appears while the final image loads; Next.js still chooses and requests the final image.
| Recipe in the example | Definition | Result |
| --------------------- | -------------------------------------------------- | ------------------------------------------------------- |
| direct | { preview: "none" } | Final image, without an Iris preview |
| blur | { placeholder: "blur" } | Tiny blurred placeholder, then final image |
| staged | { placeholder: "blur", preview: { ratio: 0.5 } } | Placeholder, optional smaller preview, then final image |
Define each recipe under recipes and refer to it by name in a rule or fallback. A recipe can also be a synchronous function when its behavior depends on the image:
recipes: {
adaptive: ({ asset, usage }) => ({
placeholder: "blur",
preview: asset.bytes > 500_000 && !usage.priority ? { ratio: 0.5 } : "none",
}),
}placeholder accepts "none", "blur", or blur settings. preview accepts "none" or settings such as ratio, maxWidth, and quality.
Rules
A rule says when to use a named recipe. When IrisImage renders, Iris tests rules using catalog metadata and image props. It checks from top to bottom and uses the first match. If none match, it uses fallback. The recipe prop on one IrisImage overrides this selection.
rules: [
{ when: { priority: true }, recipe: "direct" },
{
include: "/images/gallery/**",
when: { sourceBytes: { min: 500_000 } },
recipe: "staged",
},
{ when: ({ asset }) => asset.alpha === true, recipe: "direct" },
]Here, a priority gallery image uses direct because that rule comes first. A nonpriority gallery image over 500 KB uses staged; an image matching no rule uses fallback.
Each rule can have include and exclude filters for the source URL, plus a when condition. They must all pass for the rule to match. when can be an object of conditions, as above, or a synchronous function. include and exclude accept strings/globs, regular expressions, functions, and arrays. Recipe callbacks and rule predicates receive the same context:
| Field | What it contains |
| ---------- | ------------------------------------------------------------------------------------------- |
| src | The image URL |
| asset | Catalog metadata for the original image, such as width, bytes, format, and alpha |
| variants | Known image variants plus the original |
| usage | Render context, such as width in CSS pixels, dpr, priority, tags, and custom data |
Object conditions include priority, sourceBytes, sourceWidth, renderedWidth, targetWidth, format, and alpha. Size conditions use inclusive { min?, max? } ranges. renderedWidth comes from usage.width (or a fixed width prop); targetWidth is that width multiplied by usage.dpr (default 1). For a fill image, pass usage={{ width: 800 }} if a rule needs its rendered width.
Common project needs
| Need | Use |
| -------------------------------------------------------- | ---------------------------------------------------- |
| Find hardcoded image URLs in app code | generate.scan |
| Include all images in a folder | assets with a public/** glob |
| Skip tests, fixtures, or specific URLs | generate.exclude |
| Add CMS or remote image URLs | generate.sources or an async generate() function |
| Choose a recipe by priority, image size, format, or path | rules with when or include |
| Inspect generated assets and policy results | generate.report: true |
scan recognizes literal PNG, JPEG, WebP, AVIF, and GIF URLs, including full remote URLs. It cannot evaluate variables, imported assets, or interpolated paths; add those through assets or generate.sources. Remote images must be reachable when you run iris generate. Scanning does not rewrite your code: use IrisImage where you want the policy applied.
Reference
See the full reference for configuration options, loading behavior, other renderers, and debugging.
