Skip to content

Responsive images #101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## Version 5.1.0

1. **New helper** `getResponsiveImageAttributes()`
Generates ready‑to‑use `src`, `srcSet`, and `sizes` for responsive `<img>` tags (breakpoint pruning, DPR 1×/2×, custom breakpoints, no up‑scaling).
2. Added exports:
`getResponsiveImageAttributes`, `GetImageAttributesOptions`, `ResponsiveImageAttributes`.

_No breaking changes from 5.0.x._

## Version 5.0.0

This version introduces major breaking changes, for usage examples, refer to the [official documentation](https://imagekit.io/docs/integration/javascript).
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@imagekit/javascript",
"version": "5.0.0",
"version": "5.1.0-beta.1",
"description": "ImageKit Javascript SDK",
"main": "dist/imagekit.cjs.js",
"module": "dist/imagekit.esm.js",
Expand Down
164 changes: 164 additions & 0 deletions src/getResponsiveImageAttributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import type { SrcOptions } from './interfaces'
import { buildSrc } from './url'

/* Default break‑point pools */
const DEFAULT_DEVICE_BREAKPOINTS = [640, 750, 828, 1080, 1200, 1920, 2048, 3840] as const
const DEFAULT_IMAGE_BREAKPOINTS = [16, 32, 48, 64, 96, 128, 256, 384] as const

export interface GetImageAttributesOptions extends SrcOptions {
/**
* The intended display width of the image in pixels,
* used **only when the `sizes` attribute is not provided**.
*
* Triggers a DPR-based strategy (1x and 2x variants) and generates `x` descriptors in `srcSet`.
*
* Ignored if `sizes` is present.
*/
width?: number

/**
* The value for the HTML `sizes` attribute
* (e.g., `"100vw"` or `"(min-width:768px) 50vw, 100vw"`).
*
* - If it includes one or more `vw` units, breakpoints smaller than the corresponding percentage of the smallest device width are excluded.
* - If it contains no `vw` units, the full breakpoint list is used.
*
* Enables a width-based strategy and generates `w` descriptors in `srcSet`.
*/
sizes?: string

/**
* Custom list of **device-width breakpoints** in pixels.
* These define common screen widths for responsive image generation.
*
* Defaults to `[640, 750, 828, 1080, 1200, 1920, 2048, 3840]`.
* Sorted automatically.
*/
deviceBreakpoints?: number[]

/**
* Custom list of **image-specific breakpoints** in pixels.
* Useful for generating small variants (e.g., placeholders or thumbnails).
*
* Merged with `deviceBreakpoints` before calculating `srcSet`.
* Defaults to `[16, 32, 48, 64, 96, 128, 256, 384]`.
* Sorted automatically.
*/
imageBreakpoints?: number[]
}

/**
* Resulting set of attributes suitable for an HTML `<img>` element.
* Useful for enabling responsive image loading.
*/
export interface ResponsiveImageAttributes {
/** URL for the *largest* candidate (assigned to plain `src`). */
src: string
/** Candidate set with `w` or `x` descriptors. */
srcSet?: string
/** `sizes` returned (or synthesised as `100vw`). */
sizes?: string
/** Width as a number (if `width` was provided). */
width?: number
}

/**
* Generates a responsive image URL, `srcSet`, and `sizes` attributes
* based on the input options such as `width`, `sizes`, and breakpoints.
*/
export function getResponsiveImageAttributes(
opts: GetImageAttributesOptions
): ResponsiveImageAttributes {
const {
src,
urlEndpoint,
transformation = [],
queryParameters,
transformationPosition,
sizes,
width,
deviceBreakpoints = DEFAULT_DEVICE_BREAKPOINTS as unknown as number[],
imageBreakpoints = DEFAULT_IMAGE_BREAKPOINTS as unknown as number[],
} = opts

const sortedDeviceBreakpoints = [...deviceBreakpoints].sort((a, b) => a - b);
const sortedImageBreakpoints = [...imageBreakpoints].sort((a, b) => a - b);
const allBreakpoints = [...sortedImageBreakpoints, ...sortedDeviceBreakpoints].sort((a, b) => a - b);

const { candidates, descriptorKind } = computeCandidateWidths({
allBreakpoints,
deviceBreakpoints: sortedDeviceBreakpoints,
explicitWidth: width,
sizesAttr: sizes,
})

/* helper to build a single ImageKit URL */
const buildURL = (w: number) =>
buildSrc({
src,
urlEndpoint,
queryParameters,
transformationPosition,
transformation: [
...transformation,
{ width: w, crop: 'at_max' }, // never upscale beyond original
],
})

/* build srcSet */
const srcSet =
candidates
.map((w, i) => `${buildURL(w)} ${descriptorKind === 'w' ? w : i + 1}${descriptorKind}`)
.join(', ') || undefined

const finalSizes = sizes ?? (descriptorKind === 'w' ? '100vw' : undefined)

return {
src: buildURL(candidates[candidates.length - 1]), // largest candidate
srcSet,
...(finalSizes ? { sizes: finalSizes } : {}), // include only when defined
...(width !== undefined ? { width } : {}), // include only when defined
}
}

function computeCandidateWidths(params: {
allBreakpoints: number[]
deviceBreakpoints: number[]
explicitWidth?: number
sizesAttr?: string
}): { candidates: number[]; descriptorKind: 'w' | 'x' } {
const { allBreakpoints, deviceBreakpoints, explicitWidth, sizesAttr } = params

// Strategy 1: Width-based srcSet (`w`) using viewport `vw` hints
if (sizesAttr) {
const vwTokens = sizesAttr.match(/(^|\s)(1?\d{1,2})vw/g) || []
const vwPercents = vwTokens.map((t) => parseInt(t, 10))

if (vwPercents.length) {
const smallestRatio = Math.min(...vwPercents) / 100
const minRequiredPx = deviceBreakpoints[0] * smallestRatio
return {
candidates: allBreakpoints.filter((w) => w >= minRequiredPx),
descriptorKind: 'w',
}
}

// No usable `vw` found: fallback to all breakpoints
return { candidates: allBreakpoints, descriptorKind: 'w' }
}

// Strategy 2: Fallback using explicit image width using device breakpoints
if (typeof explicitWidth !== 'number') {
return { candidates: deviceBreakpoints, descriptorKind: 'w' }
}

// Strategy 3: Use 1x and 2x nearest breakpoints for `x` descriptor
const nearest = (t: number) =>
allBreakpoints.find((n) => n >= t) || allBreakpoints[allBreakpoints.length - 1]

const unique = Array.from(
new Set([nearest(explicitWidth), nearest(explicitWidth * 2)]),
)

return { candidates: unique, descriptorKind: 'x' }
}
9 changes: 5 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { SrcOptions, Transformation, UploadOptions, UploadResponse } from "./interfaces";
import { ImageKitAbortError, ImageKitInvalidRequestError, ImageKitServerError, ImageKitUploadNetworkError, upload } from "./upload";
import { buildSrc, buildTransformationString } from "./url";
import { getResponsiveImageAttributes } from "./getResponsiveImageAttributes";
import type { GetImageAttributesOptions, ResponsiveImageAttributes } from "./getResponsiveImageAttributes";

export { buildSrc, buildTransformationString, upload, ImageKitInvalidRequestError, ImageKitAbortError, ImageKitServerError, ImageKitUploadNetworkError };
export { buildSrc, buildTransformationString, upload, getResponsiveImageAttributes, ImageKitInvalidRequestError, ImageKitAbortError, ImageKitServerError, ImageKitUploadNetworkError };
export type {
Transformation,
SrcOptions,
UploadOptions,
UploadResponse
UploadResponse,
GetImageAttributesOptions, ResponsiveImageAttributes
};


15 changes: 15 additions & 0 deletions src/interfaces/Transformation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -720,4 +720,19 @@ export type SolidColorOverlayTransformation = Pick<Transformation, "width" | "he
* Specifies the transparency level of the overlaid solid color layer. Supports integers from `1` to `9`.
*/
alpha?: number;

/**
* Specifies the background color of the solid color overlay.
* Accepts an RGB hex code, an RGBA code, or a color name.
*/
background?: string;

/**
* Only works if base asset is an image.
*
* Creates a linear gradient with two colors. Pass `true` for a default gradient, or provide a string for a custom gradient.
*
* [Effects and Enhancements - Gradient](https://imagekit.io/docs/effects-and-enhancements#gradient---e-gradient)
*/
gradient?: Transformation["gradient"]
}
Loading