Overlays

Cropper

A composable image-crop primitive. The Cropper root manages pan/zoom/rotation state in context; CropperCanvas renders the interactive canvas; CropperOverlay draws the crop mask. The consumer wires their own Dialog, Slider, and Buttons around it.

Preview
01 Installation

Install

Pull Cropper from the barrel for everyday use, or the granular path when you want a tighter bundle.

tsxBarrel1 line
import { Cropper } from "@brika/clay";
tsxGranular1 line
import { Cropper } from "@brika/clay/components/cropper";
02 Usage

A minimal example

Drop this into a page. Native HTML attributes pass through to the underlying primitive.

tsxCropper.tsx96 lines
'use client';

import * as React from 'react';
import { useState } from 'react';
import { Card, CardContent } from '@brika/clay/components/card';
import {
  Cropper,
  CropperApply,
  CropperCancel,
  CropperFallback,
  CropperFlip,
  CropperInput,
  CropperReset,
  CropperRotate,
  CropperViewport,
  CropperZoom,
} from '@brika/clay/components/cropper';
import {
  FlipHorizontal2,
  FlipVertical2,
  ImageUp,
  RotateCcw,
  RotateCcwSquare,
  RotateCw,
  Upload,
  ZoomIn,
  ZoomOut,
} from 'lucide-react';

/**
 * Full-featured Cropper in uncontrolled mode: viewport with drag-drop fallback, zoom slider, rotate/flip/reset toolbar, and apply/cancel actions, all composed inside a Card.
 */
export default function CropperDefaultDemo() {
  const [preview, setPreview] = useState<string | null>(null);

  function onCrop(blob: Blob) {
    if (preview != null) URL.revokeObjectURL(preview);
    setPreview(URL.createObjectURL(blob));
  }

  return (
    <div className="flex flex-col items-center gap-4">
      {preview != null && (
        <img src={preview} alt="Cropped result" className="size-20 rounded-full object-cover" />
      )}

      <Card>
        <CardContent className="flex flex-col items-center gap-3 pt-4">
          <Cropper>
            <CropperViewport>
              <CropperFallback>
                <ImageUp className="size-8 opacity-60" />
                <span className="text-xs font-medium">Drop a photo or click upload</span>
              </CropperFallback>
            </CropperViewport>

            {/* Zoom row: minus icon, slider, plus icon */}
            <div className="flex w-full items-center gap-3">
              <ZoomOut className="size-4 shrink-0 text-muted-foreground" />
              <CropperZoom className="flex-1" />
              <ZoomIn className="size-4 shrink-0 text-muted-foreground" />
            </div>

            {/* Toolbar: rotate, flip, reset, upload */}
            <div className="flex w-full items-center gap-2">
              <CropperRotate direction="left" aria-label="Rotate left" className="flex-1">
                <RotateCcw className="size-4" />
              </CropperRotate>
              <CropperRotate direction="right" aria-label="Rotate right" className="flex-1">
                <RotateCw className="size-4" />
              </CropperRotate>
              <CropperFlip axis="h" aria-label="Flip horizontal" className="flex-1">
                <FlipHorizontal2 className="size-4" />
              </CropperFlip>
              <CropperFlip axis="v" aria-label="Flip vertical" className="flex-1">
                <FlipVertical2 className="size-4" />
              </CropperFlip>
              <CropperReset aria-label="Reset" className="flex-1">
                <RotateCcwSquare className="size-4" />
              </CropperReset>
              <CropperInput variant="outline" size="icon-sm" aria-label="Upload image" className="flex-1">
                <Upload className="size-4" />
              </CropperInput>
            </div>

            {/* Action row */}
            <div className="flex w-full gap-2">
              <CropperCancel className="flex-1">Cancel</CropperCancel>
              <CropperApply onCrop={onCrop} className="flex-1">Apply</CropperApply>
            </div>
          </Cropper>
        </CardContent>
      </Card>
    </div>
  );
}
03 Examples

Cropper, every way

Inline

Minimal inline Cropper (no modal) with a rounded crop shape: choose a file, drag to reposition, zoom, then apply.

Avatar-upload flow: clicking the avatar opens the file browser, then the crop UI appears inside a Dialog composed from public Cropper parts with a full rotate/flip/zoom toolbar.

04 Accessibility

Accessibility

  • The crop canvas is pointer-only; keyboard-accessible zoom and apply actions must be provided by the consuming layout (e.g. a Slider and Button).
  • Wrap the entire crop UI with an accessible dialog so screen readers can announce the modal context.
05 Tokens 5 theme-overridable

Theme tokens

Every CSS variable Cropper reads, with its default and the dotted path you'd write in a ThemeConfig JSON to override it. Set any of these in your theme to retune Cropper without touching component code.

Cropper tokens

5 tokens
ColorFill, border, and text colors.5
  • --cropper-stage-bgcolorvar(--accent)
    components.cropper.stageBg

    Background fill of the crop canvas stage, visible as letterboxing when the image does not cover the full square.

  • --cropper-overlay-colorcoloroklch(0.08 0.01 60 / 0.42)
    components.cropper.overlayColor

    Dim color applied outside the crop area via a large box-shadow on the crop mask. Defaults to a near-black at ~42% opacity so the outside of the circle is visibly darkened.

  • --cropper-overlay-bordercoloroklch(1 0 0 / 0.45)
    components.cropper.overlayBorder

    Border drawn on the inner edge of the crop mask to delineate the crop area. Defaults to 45% white.

  • --cropper-drop-active-ringcolorvar(--primary)
    components.cropper.dropActiveRing

    Outline ring color applied to CropperViewport while an image file is dragged over it. Uses the brand primary so it matches the theme accent.

  • --cropper-placeholder-colorcolorvar(--muted-foreground)
    components.cropper.placeholderColor

    Icon and text color for the empty-state placeholder shown inside CropperViewport when no image has been loaded yet.

Override in a theme

Authoring a theme is plain JSON. Drop overrides under components.cropper, the names are camelCase versions of the variable suffix.

jsonmy-theme.json12 lines
{
  "id": "my-theme",
  "name": "My Theme",
  "description": "...",
  "accentSwatches": ["#000"],

  "components": {
    "cropper": {
      "stageBg": "var(--accent)"
    }
  }
}

Then apply with applyTheme(myTheme) (see the theming guide), scope it to a subtree with themeToCssVars(myTheme, mode), or consume any token directly in your own JSX with the matching Tailwind utility (e.g. bg-cropper-…) — see the chips on each token row.

06 API reference

API reference

Props specific to Cropper. Native HTML attributes pass through to the underlying primitive, see the component source on GitHub for the full type signature.

  • File | null
    Seed file for uncontrolled mode. Only read once on mount; ignored when image is provided (controlled mode). Use this to preload a sample image in demos or to restore a previously saved file; subsequent picks and drag-drops still work without any additional wiring.
  • File | null
    The file to display in the cropper (controlled mode). When omitted the cropper manages its own file state internally (uncontrolled mode) and CropperInput / drag-drop on CropperViewport load files without any consumer state. Pass image when you need to control the loaded file from outside (e.g. a Dialog that clears on cancel).
  • number
    Maximum zoom level. Defaults to 3. CropperZoom's Slider max defaults to this value.
  • number
    Minimum zoom level. Defaults to 1 (image just covers the stage). CropperZoom's Slider min defaults to this value.
  • (file: File) => void
    Called whenever a new file is loaded via CropperInput pick or drag-drop on CropperViewport, in both controlled and uncontrolled mode. In controlled mode (image prop provided) this is the primary wiring point: wire onImageChange={setFile} so picks and drops actually replace the displayed image (without it, controlled mode ignores new files because the consumer owns image). In uncontrolled mode this is an optional notification callback; the cropper already updates its internal state automatically.
  • 'circle' | 'rounded'
    Mask shape rendered by CropperOverlay: 'circle' or 'rounded'. Defaults to 'circle'.
  • number
    On-screen stage size in px. Defaults to 288.