{"id":18905030,"url":"https://github.com/coreyward/sanity-image","last_synced_at":"2025-05-16T10:05:36.783Z","repository":{"id":48123228,"uuid":"218824395","full_name":"coreyward/sanity-image","owner":"coreyward","description":"React component for displaying well-optimized images from Sanity.io easily","archived":false,"fork":false,"pushed_at":"2025-02-27T18:23:30.000Z","size":970,"stargazers_count":125,"open_issues_count":1,"forks_count":13,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-13T02:17:12.955Z","etag":null,"topics":["image","images","react","sanity","sanity-io","srcset"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/coreyward.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-10-31T17:41:24.000Z","updated_at":"2025-05-03T08:23:15.000Z","dependencies_parsed_at":"2024-11-08T09:10:26.803Z","dependency_job_id":"01ec8590-fad6-4807-86e9-1e24e6c67ed3","html_url":"https://github.com/coreyward/sanity-image","commit_stats":{"total_commits":38,"total_committers":4,"mean_commits":9.5,"dds":0.07894736842105265,"last_synced_commit":"4ec0f3eff77863ba1656bfa3f22bdb55f1b88de7"},"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coreyward%2Fsanity-image","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coreyward%2Fsanity-image/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coreyward%2Fsanity-image/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coreyward%2Fsanity-image/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/coreyward","download_url":"https://codeload.github.com/coreyward/sanity-image/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254509477,"owners_count":22082891,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["image","images","react","sanity","sanity-io","srcset"],"created_at":"2024-11-08T09:10:23.076Z","updated_at":"2025-05-16T10:05:36.763Z","avatar_url":"https://github.com/coreyward.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# sanity-image\n\n[![Latest version](https://img.shields.io/npm/v/sanity-image?label=version\u0026color=brightGreen\u0026logo=npm)](https://www.npmjs.com/package/sanity-image)\n[![Open issues](https://img.shields.io/github/issues/coreyward/sanity-image)](https://github.com/coreyward/sanity-image/issues)\n![React version compatibility](https://img.shields.io/badge/dynamic/json?color=blue\u0026label=react%20versions\u0026query=peerDependencies.react\u0026url=https%3A%2F%2Fraw.githubusercontent.com%2Fcoreyward%2Fsanity-image%2Fmain%2Fpackage.json)\n\nA well-considered React component for displaying images from Sanity. At a\nglance:\n\n- Outputs a single `\u003cimg\u003e` tag, no nested DOM structure to mess with\n- Zero styling included so you can style it however you want…it's just an `img`\n  tag!\n- Supports low-quality image previews out of the box, without build-time\n  penalties (native lazy loading)\n- Generates a `srcSet` automatically based on the `width` you specify\n- Dynamic `srcSet` factor based on image output width\n- Knows _exactly_ what size the image will be and sets `width` and `height`\n  attributes accordingly\n- Supports `crop` and `hotspot` values from the Sanity Studio\n- Automatically crops to the most “interesting” part of the image if the aspect\n  ratio changes and no `hotspot` is provided\n- Images are _never_ scaled up\n- Tiny 4kb bundle size (2kb gzipped)\n- No dependencies\n- TypeScript support\n- Works with Gatsby, Next.js, and any other React-based framework\n- Polymorphic component (supports `as` prop to render as a custom component)\n\n## Quick Start\n\n### Install it:\n\n```sh\nyarn add sanity-image\n# or\nnpm install sanity-image\n```\n\n### Use it:\n\nYou can find the full writeup on getting going below, but in the interest of\nmaking it easy to see if this is the thing you are looking for, here’s a quick\nexample of most of what you’ll need to know:\n\n**Simplest Case**:\n\nThis will render the image out assuming it will be displayed at half its\noriginal width with a srcSet included (multiplies vary based on original image\nsize):\n\n```tsx\nimport { SanityImage } from \"sanity-image\"\n\nconst YourSweetComponent = ({ image }: ComponentProps) =\u003e (\n  \u003cSanityImage\n    // Pass the Sanity Image ID (`_id`) (e.g., `image-abcde12345-1200x800-jpg`)\n    id={image._id}\n    baseUrl=\"https://cdn.sanity.io/images/abcd1234/production/\"\n    alt=\"Demo image\"\n  /\u003e\n)\n```\n\n**More full-featured example**:\n\n```tsx\nimport { SanityImage } from \"sanity-image\"\n\nconst YourSweetComponent = ({ image }: ComponentProps) =\u003e (\n  \u003cSanityImage\n    // Pass the Sanity Image ID (`_id`) (e.g., `image-abcde12345-1200x800-jpg`)\n    id={image._id}\n    //\n    // You can set the base URL manually, or let it be constructed by passing\n    // `projectId` and `dataset` props.\n    baseUrl=\"https://cdn.sanity.io/images/abcd1234/production/\"\n    //\n    // Specify how big it is expected to render so a reasonable srcSet can be\n    // generated using `width`, `height`, or both\n    width={500}\n    height={250}\n    //\n    // Choose whether you want it to act like `object-fit: cover` or\n    // `object-fit: contain`, or leave it out to use the default (contain)\n    mode=\"cover\"\n    //\n    // Have hotspot or crop data from Sanity? Pass it in!\n    hotspot={image.hotspot}\n    crop={image.crop}\n    //\n    // Want low-quality image previews? Fetch them from Sanity and pass them in too.\n    preview={image.asset.metadata.lqip}\n    //\n    // Have a burning desire to have Sanity change the format or something?\n    // Most of the visual effects from the Sanity Image API are available:\n    queryParams={{ sharpen: 30, q: 80 }}\n    //\n    // Anything else you want to pass through to the img tag? Go for it!\n    alt=\"Sweet Christmas!\"\n    className=\"big-ol-image\"\n    sizes=\"(min-width: 500px) 500px, 100vw\"\n  /\u003e\n)\n\nexport default YourSweetComponent\n```\n\nThat’s the gist. Read on for more. 👇\n\n## Details\n\nHow it works at a glance:\n\n- The image ID is parsed to determine the source image dimensions and format\n- SVG images get special treatment from the Sanity Image API (they don't support\n  params), so they're handled a bit differently (check `SanityImage.ts` for\n  details)\n- All other images have `src` and `srcSet` props generated based on the `width`\n  and `height` props you pass in (or the image dimensions if you don't pass in a\n  width or height)\n- The `srcSet` widths depend on the size of the output image and the original\n  image; there's some logic to avoid wasteful tiny images or giant jumps in size\n  between large entries (see `dynamicMultipliers` in `urlBuilder.ts`)\n- Values in the `srcSet` are never duplicated and never upscale the image\n- Since we can compute the output dimensions of the image in all cases, the\n  `width` and `height` attributes are set automatically to avoid layout shifts\n- A few image params are applied by default:\n  - `auto=format` - Sanity will use AVIF images if they're supported by the\n    browser (https://www.sanity.io/help/avif) (note: if you specify `fm`\n    manually, this won't be set)\n  - `fit` - if the image aspect ratio isn't changed, this will be set to `max`;\n    if the aspect ratio will change it's set to `crop`; you don't really need to\n    worry about this though\n  - `q` - the quality is set to 75 by default, but you can override it with the\n    `queryParams` prop\n- The `loading` attribute will be set to `lazy` if it isn't supplied; use\n  `loading=\"eager\"` for images above the fold\n- The `alt` attribute will be set to an empty string if it isn't supplied; set\n  it if it isn't a decorative image!\n- By default it renders an `img` tag (two if you pass in a `preview`), but you\n  can pass in a custom component to render as using the `as` prop (see the\n  `SanityImage.test.tsx` file for an example of this)\n- If you wanna get weird you can also import the `buildSrc` and `buildSrcSet`\n  exports to do your own thing with. You get a lot of the magic still this way\n  with a skosh more control.\n- Similarly, the `parseImageId` function is available as a named export; it\n  takes an image ID and returns an object with the image id, dimensions, and\n  format.\n- Query params passed to Sanity are all sorted and minimized like heck for\n  improved caching and smaller URLs. Pass in a `height` only? Don't be alarmed,\n  but it'll be converted to a `w` param without altering what you're asking\n  Sanity for. Ask for `mode=\"cover\"` but the aspect ratio matches the source?\n  It'll be ignored and fall back to `fit=max` with just a `w` param. You get the\n  idea (I hope, or at least, I'm pretending, but no judgement if you don't, it's\n  definitely 11:09pm and I'm on fumes)\n\n## Props\n\nThis is mostly copied and reformatted from the `types.ts` file; if you're\ncomfortable with TypeScript, that might give you more detail.\n\n- `id` (string) — Required - The Sanity Image ID (`_id` or `_ref` field value)\n- `mode` (\"cover\" | \"contain\") — Optional - Use `cover` to crop the image to\n  match the requested aspect ratio (based on `width` and `height`). Use\n  `contain` to fit the image to the boundaries provided without altering the\n  aspect ratio. Defaults to `\"contain\"`. See the image below for a comparison.\n- `width` (number) — Optional - The target width of the image in pixels. Only\n  used for determining the dimensions of the generated assets, not for layout.\n  Use CSS to specify how the browser should render the image instead.\n- `height` (number) — Optional - The target height of the image in pixels. Only\n  used for determining the dimensions of the generated assets, not for layout.\n  Use CSS to specify how the browser should render the image instead.\n- `hotspot` (`{ x: number, y: number }`) — Optional - The hotspot coordinates to\n  use for the image. Note: hotspot `width` and `height` are not used.\n- `crop` (`{ top: number, bottom: number, left: number, right: number }`) —\n  Optional - The crop coordinates to use for the image.\n- `preview` (string) — Optional - A low-quality image preview to use while the\n  full-size image is loading. This should be a base64-encoded image string.\n- `as` (React.ElementType) — Optional - The component to render as. Defaults to\n  `\"img\"`.\n- `baseUrl` (string) — Optional - The base URL to use for the image. If not\n  specified, the `projectId` and `dataset` props will be used to construct the\n  URL.\n- `projectId` (string) — Optional - The Sanity project ID to use for the image.\n  Only used if `baseUrl` is not specified.\n- `dataset` (string) — Optional - The Sanity dataset to use for the image. Only\n  used if `baseUrl` is not specified.\n- `queryParams` (object) — Optional - An object of query parameters to pass to\n  the Sanity Image API. See the\n  [Sanity Image API documentation](https://www.sanity.io/docs/image-urls) for a\n  list of available options.\n\nThat's the gist. There's a ton more in the inline comments and types and such,\nand I'll add more details as I think of them. Feel free to open an issue or\nstart a discussion if you have questions or suggestions, or find me on the\nSanity Slack!\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003e⚠️ “Help! My full-size image isn’t loading!\"\u003c/strong\u003e\u003c/summary\u003e\n\n`SanityImage` is relying on browser-native deferred image loading. This relies\non the `\u003cimg\u003e` element being visible on the page to trigger loading\nautomatically. If the image is hidden or covered by another element, it may\nnever be loaded.\n\nFor example, if you are using `overflow: hidden` on a parent element, the image\nmight be positioned in a way that it is not treated as visible by the browser.\nThis can also happen if you have `display: none` on the image or a parent\nelement, or if the image is positioned off-screen (e.g., in a carousel, or\nwaiting for an animation to reveal it).\n\nInspecting the DOM in your browser’s dev tools and checking the position of the\n`\u003cimg\u003e` element with the `data-loading` attribute can help you identify the\nissue usually. Once you can identify the cause of the image being hidden, you\ncan adjust the CSS styles on the `img[data-loading]` element to make it visible.\n\nFor reference, the full-size image sits immediately adjacent to the preview\nimage and has the following default styles _while loading_:\n\n```css\nposition: absolute;\nwidth: 10px !important; /* must be \u003e 4px to be lazy loaded */\nheight: 10px !important; /* must be \u003e 4px to be lazy loaded */\nopacity: 0;\nzindex: -10;\npointerevents: none;\nuserselect: none;\n```\n\n\u003c/details\u003e\n\n## Tips\n\n### Choosing the right `mode`\n\nIf you are providing only one dimension (`width` or `height`, but not both), it\ndoesn't matter since the behavior will be the same.\n\n- **Contain** mode will treat the dimensions you provide as boundaries, resizing\n  the image to fit inside of them. The output image will match the aspect ratio\n  of the original image (i.e., no cropping will occur).\n- **Cover** mode will treat the dimensions you provide as a container, resizing\n  the image to completely fill the dimensions. The output image will match the\n  aspect ratio of the dimensions you provide.\n\nHere's a visual of this in action:\n\n![Sanity Image Mode Explanation](https://github.com/user-attachments/assets/82125edb-2081-448e-9f06-c90d4f0bbf34)\n\n### Wrap it internally\n\nTo improve the DX of using `sanity-image`, create a wrapper component in your\napp that sets the `baseUrl` prop (or `projectId` and `dataset`). This keeps the\nconfiguration in one place and gives you an entry point to add any other logic\nyou might need. There's a\n[full `ImageWrapper` example](https://github.com/coreyward/sanity-image/blob/main/examples/ImageWrapper.tsx)\nin the examples folder including comments. Here's a simplified version of that\nexample for quick reference:\n\n```tsx\nimport * as React from \"react\"\nimport { SanityImage, type WrapperPRops } from \"sanity-image\"\n\nexport const Image = \u003cT extends React.ElementType = \"img\"\u003e(\n  props: WrapperProps\u003cT\u003e\n) =\u003e \u003cSanityImage baseUrl=\"\u003cyour-baseurl-here\u003e\" {...props} /\u003e\n```\n\n\u003cdetails\u003e\n  \u003csummary\u003e👩‍🎤 Using Emotion’s `jsxImportSource`? Read this.\u003c/summary\u003e\n\nWhen you set the `jsxImportSource` to `@emotion/react` it replaces some core\nReact types with those of its own. This allows Emotion’s polymorphic components\nto work, but it also makes typing a polymorphic component like `\u003cSanityImage\u003e` a\nbit harder. This is okay when it's used directly, but the wrapper approach winds\nup breaking it—the use of `Omit` to remove configuration props breaks the\npolymorphism and prompts TS to complain about unexpected props.\n\nI am not a TS wizard, alas, and despite lots of reading and attempting to make\nsomething that works out of the box for Emotion, I have not managed to do so.\nInstead I recommend using `@ts-expect-error` on the `\u003cSanityImage\u003e` line in your\nwrapper. This will tell TS to ignore the props we're passing in, but it will\nstill ensure your in-app `\u003cImage\u003e` component works as expected with full\npolymorphic type support.\n\n```tsx\nexport const Image = \u003cT extends React.ElementType = \"img\"\u003e(\n  props: WrapperProps\u003cT\u003e\n) =\u003e (\n  /* @ts-expect-error Emotion types are incompatible with polymorphic component */\n  \u003cSanityImage baseUrl=\"\u003cyour-baseurl-here\u003e\" {...props} /\u003e\n)\n```\n\n\u003c/details\u003e\n\n### Styling your images\n\nI recommend setting something like the following CSS for images in your project,\nthen overriding styles as needed. This will ensure images act like block-level\nelements with infinitely scalable contents even with the `width` and `height`\nattributes set. It also makes it easier to handle responsiveness—if your\ncontainer gets smaller, the image gets smaller.\n\n```css\nimg {\n  display: block;\n  max-width: 100%;\n  width: 100%;\n  height: auto;\n}\n```\n\nHere's an example of how that works when using, for example, a 3-column grid\nthat fills the viewport until it is a maximum of 1,200px wide (plus padding).\nThis produces columns that are 390px at most on desktop:\n\n```jsx\n\u003cdiv\n  css={{\n    display: \"grid\",\n    gridTemplateColumns: \"repeat(3, 1fr)\",\n    gap: 15,\n    maxWidth: 1240,\n    paddingInline: 20,\n    marginInline: \"auto\",\n  }}\n\u003e\n  {[\"image-a\", \"image-b\", \"image-c\"].map((imageId) =\u003e (\n    \u003cdiv key={imageId}\u003e\n      \u003cSanityImage\n        id={imageId}\n        baseUrl=\"...\"\n        width={390}\n        sizes=\"(min-width: 1240px) 390px, calc((100vw - 40px - 30px) / 3)\"\n      /\u003e\n    \u003c/div\u003e\n  ))}\n\u003c/div\u003e\n```\n\nIf you need these images to all match in height, it's a good idea to switch to\n`cover` mode. With the height set to 260px and `mode=\"cover\"`, this will produce\nimages with a 3:2 aspect ratio that fill the column width even if the source\nimage is too small:\n\n```jsx\n\u003cSanityImage\n  id={imageId}\n  baseUrl=\"...\"\n  width={390}\n  height={260}\n  mode=\"cover\"\n  sizes=\"(min-width: 1240px) 390px, calc((100vw - 40px - 30px) / 3)\"\n/\u003e\n```\n\nIn this example we don't pass a `hotspot` value, so the image will be cropped\nbased on what Sanity thinks is the most interesting part of the image since\n`SanityImage` automatically sets `crop=entropy` in these cases. If you want to\noverride that, you can pass a `hotspot` value.\n\n### Background images\n\nUsing `SanityImage` for background images is easy, you just style the image to\nmatch the expectations of your mockup. In most cases that means setting\n`position: relative` on the container you want to fill, then using absolute\npositioning for the image. Here’s an example:\n\n```jsx\n\u003csection\n  css={{\n    position: \"relative\",\n    paddingBlock: 100,\n  }}\n\u003e\n  \u003cSanityImage\n    id=\"...\"\n    baseUrl=\"...\"\n    width={1440}\n    css={{\n      position: \"absolute\",\n      top: 0,\n      left: 0,\n      width: \"100%\",\n      height: \"100%\",\n      objectFit: \"cover\",\n      userSelect: \"none\",\n      zIndex: 1,\n    }}\n    alt=\"\"\n  /\u003e\n\n  \u003cdiv css={{ position: \"relative\", zIndex: 2 }}\u003e\n    \u003ch1\u003eYour big hero copy\u003c/h1\u003e\n    \u003cLinkButton to=\"/signup/\"\u003eGet started\u003c/LinkButton\u003e\n  \u003c/div\u003e\n\u003c/section\u003e\n```\n\nThis will cause the `section` to be sized based on the content inside of the\n`div`, and the image will be sized to fill the entire section. The aspect ratio\nof the image will be maintained due to the use of `object-fit: cover`. Note that\nwe are still using `mode=\"contain\"` for `SanityImage` here. If you have a rough\nidea of the height your section, you can set `height` and `mode=\"cover\"` which\nwill prevent, for example, a portrait orientation image from being retrieved and\ncropped by the browser.\n\nSince the z-index is set higher on the `div` containing the content, it will\nshow above the image. This example also sets `user-select: none` on the image to\nprevent the image from being selected when the user clicks and drags on the page\nto make it behave more like a traditional background image.\n\n### Fetching data from Sanity via GROQ\n\nIf you're using Sanity's GROQ query language to fetch data, here is how I\nrecommend fetching the fields you need from a typical image with the hotspot,\ncrop, and low-quality image preview included:\n\n```groq\n\"id\": asset._ref,\n\"preview\": asset-\u003emetadata.lqip,\nhotspot { x, y },\ncrop {\n  bottom,\n  left,\n  right,\n  top,\n}\n```\n\n## License\n\nCopyright ©2023-2025 Corey Ward. Available under the\n[MIT License](https://github.com/coreyward/sanity-image/blob/main/LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcoreyward%2Fsanity-image","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcoreyward%2Fsanity-image","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcoreyward%2Fsanity-image/lists"}