Oh Image is an image component library for React and Vite apps. It ships with a lightweight yet powerful Image component plus a Vite optimizer plugin that automatically optimizes static assets. Depending on whether your image is local or fetched from a CDN, you can choose the integration that fits best. The quick examples below give you a taste of the project.

Vite plugin

The Vite plugin's main purpose is to generate an Image component automatically for each static import. Suppose you want to load a hero image on the homepage from the assets folder:

import HeroImage from "./assets/hero.jpg"; // this will return image url

function App() {
  return <HeroImage />;
}

In this case the src value is just the URL for the image, so you have to manually handle every detail that makes it responsive and optimized. Now add the $oh query parameter suffix to the import and let the oh-image Vite plugin take care of the rest:

import HeroImage from "./assets/hero.jpg?$oh"; // this will return Image component

function App() {
  return <HeroImage alt="Hero image" />;
}

This returns the Image component, whose inner <img> element renders markup similar to the following:

<img class="flex" width="1920" height="1280"  loading="lazy" fetchpriority="auto"
alt="Hero image"
srcset="/@oh-images/breakpoint-16-deab89be27aab77b-d9c45d4b5795047a-hero.webp 16w,
/@oh-images/breakpoint-48-deab89be27aab77b-d9c45d4b5795047a-hero.webp 48w,
/@oh-images/breakpoint-96-deab89be27aab77b-d9c45d4b5795047a-hero.webp 96w,
/@oh-images/breakpoint-128-deab89be27aab77b-d9c45d4b5795047a-hero.webp 128w,
/@oh-images/breakpoint-384-deab89be27aab77b-d9c45d4b5795047a-hero.webp 384w,
/@oh-images/breakpoint-640-deab89be27aab77b-d9c45d4b5795047a-hero.webp 640w,
\/@oh-images/breakpoint-750-deab89be27aab77b-d9c45d4b5795047a-hero.webp 750w,
/@oh-images/breakpoint-828-deab89be27aab77b-d9c45d4b5795047a-hero.webp 828w,
/@oh-images/breakpoint-1080-deab89be27aab77b-d9c45d4b5795047a-hero.webp 1080w,
/@oh-images/breakpoint-1200-deab89be27aab77b-d9c45d4b5795047a-hero.webp 1200w,
/@oh-images/breakpoint-1920-deab89be27aab77b-d9c45d4b5795047a-hero.webp 1920w"
src="/@oh-images/main-deab89be27aab77b-d9c45d4b5795047a-hero.webp"
style="background-position: 50% 50%; background-repeat: no-repeat;
background-image: url(&quot;/@oh-images/placeholder-deab89be27aab77b-d9c45d4b5795047a-hero.webp&quot;);
background-size: cover;"
sizes="auto, 100vw"
>

It's a lot of HTML, but it gives you a feel for the benefits you get automatically:

  • width pulled from the image metadata
  • height pulled from the image metadata
  • srcset entries that cover a wide range of devices
  • a placeholder background while the image loads
  • WebP output even if the source asset is JPEG
  • sensible default attributes (e.g., sizes) to maximize performance

That's a solid payoff for adding a single suffix.

Let's adjust our HeroImage. Hero images often need to load immediately, and with oh-image you only need to set one prop:

function App() {
  return <HeroImage priority />;
}

The generated <img> is now configured to load as soon as possible.

This setup is perfect for statically loaded images. If you need to fetch assets from an image CDN—or simply want to render an <img> from the public directory—reach for the Image component itself.

Image component

The Image component is a powerful wrapper around native img element that you can use it directly. It exposes a collection of props that keep images responsive and fast. For example:

import { Image } from "@lonik/oh-image/react";

function App() {
  return (
    <Image
      src="some-image.jpg"
      width={800}
      height={600}
      breakpoints={[16, 32, 64, 128]}
    />
  );
}

We'll cover these props in later guides.

Using the component this way won't optimize a local file, but it still gives you niceties such as automatically generated srcset values and sensible performance defaults.

The component really shines when combined with loaders.

Loaders

Loaders bridge the Image component and your image CDN provider. They offer out-of-the-box integrations with popular services—just pass the appropriate loader to the loader prop.

Using Cloudinary as an example:

import { Image } from "@lonik/oh-image/react";
import { useCloudinaryLoader } from "@lonik/oh-image/cloudinary";

function App() {
  const loader = useCloudinaryLoader({
    path:"https://res.cloudinary.com/example"
    transforms: {
      rotate: 180,
    },
  });

  return (
    <Image
      width={600}
      height={400}
      placeholder
      breakpoints={[16, 32, 64, 128, 256, 512]}
      loader={loader}
      src="car.jpg"
    />
  );
}

The Image component creates all required attributes and uses the loader to build the correct URLs for the CDN. In addition to the main src, it generates srcset entries and placeholder URLs. In the example above, the src might look like https://res.cloudinary.com/example/image/upload/w_600,h_400,f_webp,r_180/car.jpg, which returns a 600 × 400 WebP image rotated by 180 degrees.

We currently support three providers, with more on the roadmap. Creating a custom loader is as simple as writing a function that accepts options and returns the desired URL.

Hopefully this gives you a sense of what the library offers. The next guides will explore these ideas in more depth.

Why?

If you're used to creating <img> tags with only src and alt, you might wonder why you need a library at all. Even with minimal requirements, you'll benefit from automatic optimization and effortless CDN fetching. The real power shows up once you understand how images work on the web. I highly recommend reading Web.dev Images—it's an excellent guide that will give you the knowledge and confidence to rely on this library instead of a plain <img> tag when you need consistent performance across devices.