Skip to navigation

Styling

The parts carry no colour. Paint them with the optional stylesheet, your own CSS or Tailwind.

Add the stylesheet

Import the stylesheet once. Bundle size lists its weight:

import "@zriz/react/styles.css";
PartWhat the stylesheet sets
Viewportoverflow: clip and a grab cursor, not-allowed on it, the crop, the handles and the dial while Root is disabled
MaskThe scrim colour. It fades to 60% during a gesture
AreaA 1 px outline inside the crop and a two-tone ring on keyboard focus
HandleA corner mark of two short lines or an edge bar, and a resize cursor. Corners hide with shape="circle"
GridWhite lines that fade in during a gesture, clipped to the circle on a round crop
Dial40 px height, sideways clip with a 48 px fade at each side, a grab cursor, centre line, tick colours, the zero dot

It paints nothing on Zoom, the buttons or the aspect items. The demos on this site style those themselves. Viewport shows how to size the box.

Every rule sits in :where() inside @layer components.zriz. A class with no layer wins in any import order, and so does a class in your components or utilities layer, Tailwind included. A class in any other layer wins only when that layer is declared after the stylesheet. @zriz/core/styles.css is the same file for vanilla JS.

A page-wide transition cannot make the crop trail your finger. zriz sets transition-property inline to none on Area, DialTicks and ZoomThumb, and to opacity on Mask, so the mask keeps its fade. A transition of your own on those parts needs !important.

The first line of the file names properties, theme, base, components and utilities, so a stylesheet loaded first sets the order of those layers on your page. To keep your own order, and to let every layer of yours win, import it into a layer of its own:

@layer vendor, reset, base, components, utilities;
@import url("@zriz/react/styles.css") layer(vendor);

Style a part by its state

className takes a string, or a function that gets the part's state:

<Cropper.Grid
  className={(state) =>
    state.grid === "fine" ? "opacity-50" : undefined
  }
/>

Return undefined from the function for no class. To swap the element itself, see composition.

Style with data attributes

Every part's outer element carries data-zriz-item with its part name in kebab case, such as aspect-item or dial-ticks. The ticks and grid lines inside carry none, so select them through their part, as dial shows. Root also marks the gesture in progress with data-rotating, data-panning, data-pinching, data-resizing or data-zooming. This rule hides the handles from a press on the dial until the pointer lets go, and while the dial glides:

[data-rotating] [data-zriz-item="handle"] {
  opacity: 0;
}

Each component page lists the attributes that part sets.

Change the colours

Set the colour variables on Root, on one part or on any ancestor. This one makes the scrim white in light mode and darker in dark mode, and the accent blue:

[data-zriz-item="root"] {
  --zriz-scrim: light-dark(#ffffffd9, #000000d9);
  --zriz-accent: #0a84ff;
}
VariableLight, darkWhat it sets
--zriz-scrim#000000c7 in bothThe mask outside the crop
--zriz-frame#fff in bothCrop outline and handle marks
--zriz-line#000, #fffDial centre line, major ticks, zero dot
--zriz-grid#fff9 in bothGrid lines
--zriz-tick#949494, #666Minor ticks on the dial
--zriz-accent#fdca00 in bothDial centre line while you drag the dial

Write a value with light-dark() to keep both schemes, or a single colour to use it in both.

Open
Download
0.0
DefaultInkSoft

The demo above switches between the stylesheet's colours and two light looks of its own. Each look sets all five variables on Root. Drag the dial to see the accent.

Follow dark mode

The dial follows the page's color-scheme: the light values above apply in light mode and the dark values in dark mode. The scrim and the crop frame sit over the photo, so they look the same in both.

With this rule on the page, the parts follow the system setting:

:root {
  color-scheme: light dark;
}

To pin one part, put data-theme="dark" or the class dark on the part itself. It wins over a themed wrapper. light works on the part itself, but its children stay dark inside a dark wrapper:

<Cropper.Dial data-theme="dark" aria-label="Straighten" />

In forced colours mode, the marks the stylesheet paints switch to CanvasText, and the dial's centre line to Highlight during a drag. The browser turns the scrim into a veil of Canvas, so a dark --zriz-scrim shows in the palette's own colour. The other --zriz-* variables keep your values, on Root or on one part, so CSS of your own that reads one needs a forced-colors rule with a system colour.

Style with Tailwind

Without the stylesheet, utilities paint everything, the viewport's clip included. Tailwind's data-* variants read the state attributes:

<Cropper.Root src={file}>
  <Cropper.Viewport className="h-80 overflow-clip">
    <Cropper.Image />
    <Cropper.Mask className="bg-black/60" />
    <Cropper.Area
      aria-label="Crop area"
      className="ring-1 ring-white data-interacting:ring-2 forced-colors:border"
    />
  </Cropper.Viewport>
</Cropper.Root>

The mask turns 60% black, and the crop gets a white ring that thickens during a gesture. Forced colours mode removes the ring, because it is a box shadow, so forced-colors:border draws the frame there.