# Change your avatar

Pick a photo, crop it in a dialog, and store the result in a bucket. The crop, the transforms, the background fill and the filters are local work from image-crop, a client-only service; storing the result is file-storage's, which is why these parts ship with it. The picture's URL comes back through onChange, so the same parts serve a user's avatar, an organization's logo, or any square image.

Import: @slices.sh/file-storage/headless/avatar

Public parts: 32

## Installation

### 1. Install the service registry

services/index.ts, which `import "@/services"` loads. A provider setup writes the same file; skip this if your app has one.

```bash
npx shadcn@latest add "https://slices.sh/r/services.json"
```

### 2. Install the headless library

```bash
npm install @slices.sh/file-storage@^0.2.7
```

### 3. Install the service

Install the provider configuration via shadcn or the slices CLI. Skip this step if your app already has the service configured. Provider commands and provisioning: https://slices.sh/docs/services/file-storage.md

## Anatomy

### Composition

```text
Avatar.Root
  Avatar.Hint
  Avatar.DropZone
    Avatar.Picker
  Avatar.Image
  Avatar.Content
    Avatar.Crop shape="circle"
      Avatar.CropForm
        Avatar.Fill color="#ffffff"
        Avatar.Filter
          Avatar.FilterImage
        Avatar.Preview
        Avatar.Viewport
          Avatar.SourceImage
          Avatar.Selection
            Avatar.Grid axis="horizontal"
            Avatar.Grid axis="vertical"
        Avatar.Zoom
        Avatar.Rotate
        Avatar.Flip
        Avatar.Reset
        Avatar.Apply
        Avatar.Cancel
    Avatar.Title
    Avatar.Description
  Avatar.Feedback
  Avatar.MissingConfiguration
  Avatar.Progress
  Avatar.Retry
  Avatar.Undo
  Avatar.Local
  Avatar.Saved
  Avatar.Empty
```

## Choose & crop

```tsx
"use client"

// services/index.ts comes from https://slices.sh/r/services.json (or any provider setup)
import "@/services"
import { Avatar } from "@slices.sh/file-storage/headless/avatar"

export function Example({ onChange }: { onChange: (url: string) => void }) {
  return (
    <Avatar.Root onChange={onChange}>
      <Avatar.DropZone>
        <Avatar.Image />
        <Avatar.Picker aria-label="Choose a photo" />
      </Avatar.DropZone>
      <Avatar.Content>
        <Avatar.Crop shape="circle">
          <Avatar.CropForm>
            <Avatar.Viewport>
              <Avatar.SourceImage />
              <Avatar.Selection />
            </Avatar.Viewport>
            <Avatar.Apply>Use photo</Avatar.Apply>
          </Avatar.CropForm>
        </Avatar.Crop>
      </Avatar.Content>
    </Avatar.Root>
  )
}

```

onChange receives the stored image URL. Save it to your profile in your application handler.

## Your own element

```tsx
"use client"

// services/index.ts comes from https://slices.sh/r/services.json (or any provider setup)
import "@/services"
import { Avatar } from "@slices.sh/file-storage/headless/avatar"

export function Example({ onChange }: { onChange: (url: string) => void }) {
  return (
    <Avatar.Root onChange={onChange}>
      <Avatar.DropZone>
        <Avatar.Image />
        <Avatar.Picker aria-label="Choose a photo" />
      </Avatar.DropZone>
      <Avatar.Content>
        <Avatar.Crop shape="circle">
          <Avatar.CropForm>
            <Avatar.Viewport>
              <Avatar.SourceImage />
              <Avatar.Selection />
            </Avatar.Viewport>
            <Avatar.Apply
              render={
                <button className="rounded-full bg-black px-4 py-2 text-white" />
              }
            >
              Use photo
            </Avatar.Apply>
          </Avatar.CropForm>
        </Avatar.Crop>
      </Avatar.Content>
    </Avatar.Root>
  )
}

```

The same behavior, with your element and your styles. All other parts stay unchanged.

## Complete API reference

### Avatar.Root

scope

Image is <img alt=""> with src locked to the current picture: the stored one, the local one, or the initial URL. Empty renders its children while there is no picture, for a placeholder icon.

Import: @slices.sh/file-storage/headless/avatar

```tsx
AvatarRootProps

export type AvatarRootProps = RootProps & {
  /** The file-storage config, a slug from the registry. The nearest root's, else `"default"`, when left out. */
  config?: string
  /**
   * Left out or `true`: required, and a missing service throws. `"optional"`: the picture is
   * still picked, cropped and previewed, and kept for this page session; only what needs the
   * store (Progress, the upload) renders nothing. `false` is the same, even with a store installed.
   * A capability name requires the service with it.
   */
  enabled?: Enabled
  bucket?: string
  initialUrl?: string
  value?: string
  onChange?(url: string): void
  onUploaded?(value: Uploaded): void
}
```

Source: @slices.sh/file-storage/src/headless/avatar/root.tsx

### Avatar.Empty

gate

Image is <img alt=""> with src locked to the current picture: the stored one, the local one, or the initial URL. Empty renders its children while there is no picture, for a placeholder icon.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Empty requires Avatar.Root.

```tsx
GateProps
```

Source: @slices.sh/file-storage/src/headless/avatar/empty.tsx

### Avatar.DropZone

content · Element: label

DropZone is a <label> that takes a dropped or pasted image as well as a click. Picker is the file input inside it, accepting JPEG, PNG and WebP, and disabled while an upload runs. Hint's function child gets { limit }, the size the cropper enforces, so the words never drift from it.

Import: @slices.sh/file-storage/headless/avatar

Avatar.DropZone requires Avatar.Root.

```tsx
PartProps<"label">
```

Source: @slices.sh/file-storage/src/headless/avatar/drop-zone.tsx

### Avatar.Picker

input · Element: input

DropZone is a <label> that takes a dropped or pasted image as well as a click. Picker is the file input inside it, accepting JPEG, PNG and WebP, and disabled while an upload runs. Hint's function child gets { limit }, the size the cropper enforces, so the words never drift from it.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Picker requires Avatar.Root.

```tsx
PartProps<"input">
```

Source: @slices.sh/file-storage/src/headless/avatar/picker.tsx

### Avatar.Hint

content · Element: span

The picker's hint. Its function child receives the size limit the cropper enforces, so the words never drift from it.

Import: @slices.sh/file-storage/headless/avatar

```tsx
PartProps<"span", { limit: string }>
```

Source: @slices.sh/file-storage/src/headless/avatar/hint.tsx

### Avatar.Content

content · Element: div

Content is the dialog, open while a source is chosen; Title and Description label it. Crop is the crop primitive, with shape ("circle" | "square"), minZoom and maxZoom, and remounts for each new source so nothing leaks from the last photo. CropForm submits on Enter and handles the keyboard: arrows nudge, + and - zoom, R rotates, 0 resets. Viewport takes the photo's own proportions and paints the chosen fill behind it. SourceImage is the photo, Selection the draggable frame, and Grid a guide on one axis, shown only while panning.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Content requires Avatar.Root.

```tsx
PartProps<"div">
```

Source: @slices.sh/file-storage/src/headless/avatar/content.tsx

### Avatar.Title

content · Element: h2

Content is the dialog, open while a source is chosen; Title and Description label it. Crop is the crop primitive, with shape ("circle" | "square"), minZoom and maxZoom, and remounts for each new source so nothing leaks from the last photo. CropForm submits on Enter and handles the keyboard: arrows nudge, + and - zoom, R rotates, 0 resets. Viewport takes the photo's own proportions and paints the chosen fill behind it. SourceImage is the photo, Selection the draggable frame, and Grid a guide on one axis, shown only while panning.

Import: @slices.sh/file-storage/headless/avatar

```tsx
PartProps<"h2">
```

Source: @slices.sh/file-storage/src/headless/avatar/title.tsx

### Avatar.Description

content · Element: p

Content is the dialog, open while a source is chosen; Title and Description label it. Crop is the crop primitive, with shape ("circle" | "square"), minZoom and maxZoom, and remounts for each new source so nothing leaks from the last photo. CropForm submits on Enter and handles the keyboard: arrows nudge, + and - zoom, R rotates, 0 resets. Viewport takes the photo's own proportions and paints the chosen fill behind it. SourceImage is the photo, Selection the draggable frame, and Grid a guide on one axis, shown only while panning.

Import: @slices.sh/file-storage/headless/avatar

```tsx
PartProps<"p">
```

Source: @slices.sh/file-storage/src/headless/avatar/description.tsx

### Avatar.Crop

content · Element: div

A changed source remounts the primitive so crop, zoom, fill, filter and transforms cannot leak to the next photo.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Crop requires Avatar.Root.

```tsx
PartProps<"div"> & { shape?: CropShape; minZoom?: number; maxZoom?: number }
```

Source: @slices.sh/file-storage/src/headless/avatar/crop.tsx

### Avatar.CropForm

action · Element: form

Content is the dialog, open while a source is chosen; Title and Description label it. Crop is the crop primitive, with shape ("circle" | "square"), minZoom and maxZoom, and remounts for each new source so nothing leaks from the last photo. CropForm submits on Enter and handles the keyboard: arrows nudge, + and - zoom, R rotates, 0 resets. Viewport takes the photo's own proportions and paints the chosen fill behind it. SourceImage is the photo, Selection the draggable frame, and Grid a guide on one axis, shown only while panning.

Import: @slices.sh/file-storage/headless/avatar

Avatar.CropForm requires Avatar.Crop.

Avatar.CropForm requires Avatar.Root.

```tsx
PartProps<"form">
```

Source: @slices.sh/file-storage/src/headless/avatar/crop-form.tsx

### Avatar.Viewport

content · Element: div

The primitive pans and exports as if the photo exactly fills this box, so the box takes the
photo's own proportions. In a square box a wide photo showed past the crop but could not be
dragged there, and its edges were out of reach at every zoom. Until the photo has a size the
box has no height, so the primitive first measures it at its final shape and centres the crop.

A chosen background fill is painted here, behind the photo, which is where the export puts it.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Viewport requires Avatar.Crop.

```tsx
PartProps<"div">
```

Source: @slices.sh/file-storage/src/headless/avatar/viewport.tsx

### Avatar.SourceImage

content · Element: img

Content is the dialog, open while a source is chosen; Title and Description label it. Crop is the crop primitive, with shape ("circle" | "square"), minZoom and maxZoom, and remounts for each new source so nothing leaks from the last photo. CropForm submits on Enter and handles the keyboard: arrows nudge, + and - zoom, R rotates, 0 resets. Viewport takes the photo's own proportions and paints the chosen fill behind it. SourceImage is the photo, Selection the draggable frame, and Grid a guide on one axis, shown only while panning.

Import: @slices.sh/file-storage/headless/avatar

Avatar.SourceImage requires Avatar.Crop.

Avatar.SourceImage requires Avatar.Root.

```tsx
PartProps<"img">
```

Source: @slices.sh/file-storage/src/headless/avatar/source-image.tsx

### Avatar.Selection

content · Element: div

Content is the dialog, open while a source is chosen; Title and Description label it. Crop is the crop primitive, with shape ("circle" | "square"), minZoom and maxZoom, and remounts for each new source so nothing leaks from the last photo. CropForm submits on Enter and handles the keyboard: arrows nudge, + and - zoom, R rotates, 0 resets. Viewport takes the photo's own proportions and paints the chosen fill behind it. SourceImage is the photo, Selection the draggable frame, and Grid a guide on one axis, shown only while panning.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Selection requires Avatar.Crop.

```tsx
PartProps<"div">
```

Source: @slices.sh/file-storage/src/headless/avatar/selection.tsx

### Avatar.Grid

content · Element: div

Content is the dialog, open while a source is chosen; Title and Description label it. Crop is the crop primitive, with shape ("circle" | "square"), minZoom and maxZoom, and remounts for each new source so nothing leaks from the last photo. CropForm submits on Enter and handles the keyboard: arrows nudge, + and - zoom, R rotates, 0 resets. Viewport takes the photo's own proportions and paints the chosen fill behind it. SourceImage is the photo, Selection the draggable frame, and Grid a guide on one axis, shown only while panning.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Grid requires Avatar.Crop.

```tsx
PartProps<"div"> & { axis: "horizontal" | "vertical" }
```

Source: @slices.sh/file-storage/src/headless/avatar/grid.tsx

### Avatar.Preview

content · Element: canvas

Fill is one background choice with aria-pressed locked; without color it keeps the photo's transparency. Filter is one filter choice (value) and FilterImage shows the photo through it. Preview is a canvas at a given resolution that draws what Apply would store, fill and filter included.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Preview requires Avatar.Crop.

Avatar.Preview requires Avatar.Root.

```tsx
PartProps<"canvas"> & { resolution?: 24 | 40 | 72 }
```

Source: @slices.sh/file-storage/src/headless/avatar/preview.tsx

### Avatar.Zoom

input · Element: input

Zoom is a range input with min, max, step and value locked to the crop. Rotate turns by 90 degrees, Flip mirrors horizontally, Reset returns to the first framing.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Zoom requires Avatar.Crop.

```tsx
PartProps<"input">
```

Source: @slices.sh/file-storage/src/headless/avatar/zoom.tsx

### Avatar.Rotate

action · Element: button

Zoom is a range input with min, max, step and value locked to the crop. Rotate turns by 90 degrees, Flip mirrors horizontally, Reset returns to the first framing.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Rotate requires Avatar.Crop.

```tsx
PartProps<"button">
```

Source: @slices.sh/file-storage/src/headless/avatar/rotate.tsx

### Avatar.Flip

action · Element: button

Zoom is a range input with min, max, step and value locked to the crop. Rotate turns by 90 degrees, Flip mirrors horizontally, Reset returns to the first framing.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Flip requires Avatar.Crop.

```tsx
PartProps<"button">
```

Source: @slices.sh/file-storage/src/headless/avatar/flip.tsx

### Avatar.Fill

action · Element: button

One background choice. Without a `color` it is the photo's own transparency.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Fill requires Avatar.Crop.

```tsx
PartProps<"button", { active: boolean }> & { color?: string }
```

Source: @slices.sh/file-storage/src/headless/avatar/fill.tsx

### Avatar.Reset

action · Element: button

Zoom is a range input with min, max, step and value locked to the crop. Rotate turns by 90 degrees, Flip mirrors horizontally, Reset returns to the first framing.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Reset requires Avatar.Crop.

```tsx
PartProps<"button">
```

Source: @slices.sh/file-storage/src/headless/avatar/reset.tsx

### Avatar.Filter

action · Element: button

One filter choice; `Avatar.FilterImage` inside it shows the photo with that filter.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Filter requires Avatar.Crop.

```tsx
PartProps<"button", { active: boolean }> & { value: AvatarFilter }
```

Source: @slices.sh/file-storage/src/headless/avatar/filter-option.tsx

### Avatar.FilterImage

content · Element: img

Fill is one background choice with aria-pressed locked; without color it keeps the photo's transparency. Filter is one filter choice (value) and FilterImage shows the photo through it. Preview is a canvas at a given resolution that draws what Apply would store, fill and filter included.

Import: @slices.sh/file-storage/headless/avatar

Avatar.FilterImage requires Avatar.Root.

Avatar.FilterImage requires Avatar.Filter.

```tsx
PartProps<"img">
```

Source: @slices.sh/file-storage/src/headless/avatar/filter-image.tsx

### Avatar.Cancel

action · Element: button

Apply is the crop form's submit, with { pending } while the export runs, disabled until the photo has a size. Cancel closes the dialog and drops the source.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Cancel requires Avatar.Root.

```tsx
PartProps<"button">
```

Source: @slices.sh/file-storage/src/headless/avatar/cancel.tsx

### Avatar.Apply

action · Element: button

Apply is the crop form's submit, with { pending } while the export runs, disabled until the photo has a size. Cancel closes the dialog and drops the source.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Apply requires Avatar.Crop.

Avatar.Apply requires Avatar.Root.

```tsx
PartProps<"button", { pending: boolean }>
```

Source: @slices.sh/file-storage/src/headless/avatar/apply.tsx

### Avatar.Image

content · Element: img

Image is <img alt=""> with src locked to the current picture: the stored one, the local one, or the initial URL. Empty renders its children while there is no picture, for a placeholder icon.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Image requires Avatar.Root.

```tsx
PartProps<"img">
```

Source: @slices.sh/file-storage/src/headless/avatar/image.tsx

### Avatar.Saved

gate

After Apply. Progress is a progressbar while the upload runs. Saved opens once the picture is stored. Retry sends a failed upload again; Undo restores the picture from before, or discards a local one. Feedback is <p role="alert"> with { message } for a refused file or upload; with crop it reports the crop's own errors instead. Local opens when the picture is kept in the page session because no storage is configured. MissingConfiguration is Configuration.Missing pinned to scope="file-storage", learned when a save meets a server that lacks its configuration.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Saved requires Avatar.Root.

```tsx
GateProps
```

Source: @slices.sh/file-storage/src/headless/avatar/saved.tsx

### Avatar.Undo

action · Element: button

After Apply. Progress is a progressbar while the upload runs. Saved opens once the picture is stored. Retry sends a failed upload again; Undo restores the picture from before, or discards a local one. Feedback is <p role="alert"> with { message } for a refused file or upload; with crop it reports the crop's own errors instead. Local opens when the picture is kept in the page session because no storage is configured. MissingConfiguration is Configuration.Missing pinned to scope="file-storage", learned when a save meets a server that lacks its configuration.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Undo requires Avatar.Root.

```tsx
PartProps<"button">
```

Source: @slices.sh/file-storage/src/headless/avatar/undo.tsx

### Avatar.Local

gate

After Apply. Progress is a progressbar while the upload runs. Saved opens once the picture is stored. Retry sends a failed upload again; Undo restores the picture from before, or discards a local one. Feedback is <p role="alert"> with { message } for a refused file or upload; with crop it reports the crop's own errors instead. Local opens when the picture is kept in the page session because no storage is configured. MissingConfiguration is Configuration.Missing pinned to scope="file-storage", learned when a save meets a server that lacks its configuration.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Local requires Avatar.Root.

```tsx
GateProps
```

Source: @slices.sh/file-storage/src/headless/avatar/local.tsx

### Avatar.Progress

content · Element: div

After Apply. Progress is a progressbar while the upload runs. Saved opens once the picture is stored. Retry sends a failed upload again; Undo restores the picture from before, or discards a local one. Feedback is <p role="alert"> with { message } for a refused file or upload; with crop it reports the crop's own errors instead. Local opens when the picture is kept in the page session because no storage is configured. MissingConfiguration is Configuration.Missing pinned to scope="file-storage", learned when a save meets a server that lacks its configuration.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Progress requires Avatar.Root.

```tsx
PartProps<"div">
```

Source: @slices.sh/file-storage/src/headless/avatar/progress.tsx

### Avatar.Feedback

content · Element: p

After Apply. Progress is a progressbar while the upload runs. Saved opens once the picture is stored. Retry sends a failed upload again; Undo restores the picture from before, or discards a local one. Feedback is <p role="alert"> with { message } for a refused file or upload; with crop it reports the crop's own errors instead. Local opens when the picture is kept in the page session because no storage is configured. MissingConfiguration is Configuration.Missing pinned to scope="file-storage", learned when a save meets a server that lacks its configuration.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Feedback requires Avatar.Root.

```tsx
PartProps<"p", { message: string }> & { crop?: boolean }
```

Source: @slices.sh/file-storage/src/headless/avatar/feedback.tsx

### Avatar.Retry

action · Element: button

After Apply. Progress is a progressbar while the upload runs. Saved opens once the picture is stored. Retry sends a failed upload again; Undo restores the picture from before, or discards a local one. Feedback is <p role="alert"> with { message } for a refused file or upload; with crop it reports the crop's own errors instead. Local opens when the picture is kept in the page session because no storage is configured. MissingConfiguration is Configuration.Missing pinned to scope="file-storage", learned when a save meets a server that lacks its configuration.

Import: @slices.sh/file-storage/headless/avatar

Avatar.Retry requires Avatar.Root.

```tsx
PartProps<"button">
```

Source: @slices.sh/file-storage/src/headless/avatar/retry.tsx

### Avatar.MissingConfiguration

gate

The server this avatar is stored on answered that it lacks configuration.

Saving is the one request an avatar makes, so this is learned when somebody applies a crop,
and not before: nothing is drawn for a deployment that has not said so.
→ docs/rules/missing-configuration-is-said.md

Import: @slices.sh/file-storage/headless/avatar

```tsx
GateProps<{ scopes: readonly string[] }>
```

Source: @slices.sh/file-storage/src/headless/avatar/missing-configuration.tsx
