2026.05.27 リリースしました

確認する

Image Uploader

ImageUploaderは、画像ファイルをアップロードする際に使用されるコンポーネントです。
選択済みの画像プレビューを表示し、スロットで未選択時と選択後のUIを切り替えられます。

プロパティ

ImageUploaderは、以下のプロパティをサポートしています。

名前 デフォルト値 説明
disabled boolean false 入力を無効化します。操作できません。
isError boolean false エラー状態を視覚的に示します。バリデーション用など。
src string | null null 選択された画像のURLをバインドします。
readonly boolean false 表示専用の状態にします。操作できません。
clearable boolean false 選択された画像をクリアできるかどうかを指定します。true に設定すると、選択された画像をクリアできるようになります。

インストールの手順

以下のコンポーネントのコードを、使いたいプロジェクトにコピー&ペーストします。
パスは実際のプロジェクトの構成にあわせて更新してください。

atoms/ImageUploader.svelte
        <!--
@component
## 概要
- 画像ファイルを選択・プレビューするコンポーネントです

## 機能
- ユーザーが画像ファイルを端末から選択できます
- 選択された画像のプレビューを表示します
- スロットを用いて未選択時と選択後の表示を切り替えできます

## Props
- isError: true の場合エラー時のスタイルを適用します
- disabled: 指定するとグレーアウトされ、クリック不可になります
- src: 選択済み画像のURLをバインドできます
- readonly: 画像の選択はできませんがプレビューは表示されます
- clearable: true の場合、選択済み画像をクリアするボタンが表示されます

## Usage
```svelte
<ImageUploader bind:src>
  {#if src}
    <img src={src} alt="preview" class="w-32 h-32 rounded-md object-cover" />
  {:else}
    <span>画像を選択</span>
  {/if}
</ImageUploader>
```
-->

<script module lang="ts">
  import type { Snippet } from 'svelte';
  import type { ClassValue, HTMLInputAttributes } from 'svelte/elements';
  import { cva, type VariantProps } from 'class-variance-authority';

  export const imageUploaderVariants = cva('relative flex items-center justify-center min-h-4 text-muted-foreground text-sm group overflow-hidden outline-ring outline-offset-2 focus-visible:outline-2 transition-colors cursor-pointer', {
    variants: {
      /** 操作できるかどうか */
      disabled: {
        true: ['opacity-50 pointer-events-none'],
        false: ['active:bg-accent hover:bg-accent/90'],
      },
      /** エラーかどうか */
      isError: {
        true: ['border-destructive'],
        false: [],
      },
      /** 画像が選択されているかどうか */
      filled: {
        true: ['!border-none text-foreground'],
        false: [],
      },
      /** 読み取り専用かどうか */
      readonly: {
        true: ['pointer-events-none'],
        false: [],
      },
      /** 編集可能かどうか */
      isEditable: {
        true: ['hover:bg-muted-foreground/50'],
        false: [],
      },
    },
    defaultVariants: {
      disabled: false,
      isError: false,
      filled: false,
      readonly: false,
      isEditable: false,
    },
  });

  export type ImageUploaderVariants = VariantProps<typeof imageUploaderVariants>;

  export interface ImageUploaderProps extends ImageUploaderVariants, HTMLInputAttributes {
    /** クラス */
    class?: ClassValue;
    /** 選択された画像のURL */
    src: string | null;
    /** 選択したファイル */
    files?: FileList | null;
    /** エラー状態のスタイルを適用するか */
    isError?: boolean;
    /** 読み取り専用モード */
    readonly?: boolean;
    /** 値をリセットできるか */
    clearable?: boolean;
    /** 画像が変更されたときのコールバック */
    onChangeImage?: (src: string | null) => void;
    children?: Snippet<[]>;
  }
</script>

<script lang="ts">
  import ImagePlus from '@lucide/svelte/icons/image-plus';
  import Pen from '@lucide/svelte/icons/pen';
  import X from '@lucide/svelte/icons/x';

  let { isError = false, readonly = false, class: className, src = $bindable(null), files = $bindable(null), onChangeImage, clearable = false, children, ...inputAttributes }: ImageUploaderProps = $props();

  /** 画像をクリアする関数。srcとfilesをnullにリセットし、onChangeコールバックを呼び出します。 */
  export function clear() {
    src = null;
    files = null;
    onChangeImage?.(null);
  }

  let hasSrc = $derived(!!src);
  let isNotEditable = readonly || inputAttributes.disabled;
  let isEditable = $derived(hasSrc && !isNotEditable);
  let fileInputElement: HTMLInputElement;

  let imageUploaderVariantClass = $derived(imageUploaderVariants({ disabled: inputAttributes.disabled, isError, readonly, filled: !!src, class: className, isEditable }));

  function handleChange(event: Event) {
    const input = event.target;
    if (!(input instanceof HTMLInputElement)) return;
    handleFiles(input.files);
  }

  // ファイルリストから最初のファイルを取得し、URLを生成してsrcに設定
  function handleFiles(fileList: FileList | null) {
    const file = fileList?.[0];

    // ファイルが存在しない = 選択がキャンセルされた場合は何もしない
    if (!file) return;
    src = URL.createObjectURL(file);
    // 選択状態をクリア
    if (fileInputElement) fileInputElement.value = '';
    onChangeImage?.(src);
  }

  // ドラッグ&ドロップイベントのハンドラ
  function handleDrop(event: DragEvent) {
    event.preventDefault();
    if (readonly || inputAttributes.disabled) return;
    handleFiles(event.dataTransfer?.files ?? null);
  }

  // ドラッグオーバーイベントのハンドラ
  function handleDragOver(event: DragEvent) {
    event.preventDefault();
  }

  // ドラッグリーブイベントのハンドラ
  function handleDragLeave(event: DragEvent) {
    event.preventDefault();
  }

  // クリアボタン押下時の処理
  function handleClear(event: MouseEvent) {
    event.stopPropagation();

    clear();
  }
</script>

<div class="relative inline-block" data-rabee-ui="image-uploader">
  <button class={[imageUploaderVariantClass, !children && 'rounded-full']} type="button" onclick={() => fileInputElement?.click()} ondrop={handleDrop} ondragover={handleDragOver} ondragleave={handleDragLeave} disabled={inputAttributes.disabled}>
    <input class="sr-only cursor-pointer" tabindex="-1" aria-hidden="true" type="file" accept="image/*" bind:this={fileInputElement} bind:files onchange={handleChange} {...inputAttributes} />
    <div class="relative grid place-content-center">
      {#if isEditable}
        <div class="absolute inset-0 flex items-center justify-center size-full bg-overlay-dark/10 transition-colors duration-200 group-hover:bg-overlay-dark/20">
          <Pen class="text-bright-foreground" />
        </div>
      {/if}
      {#if children}
        {@render children?.()}
      {:else if src}
        <img class="size-20 rounded-full object-cover" {src} alt="preview" />
      {:else}
        <div class={['flex items-center justify-center size-20 border border-dashed rounded-full', isError ? 'border-destructive' : 'border-input']}>
          <ImagePlus class="object-contain text-subtle-foreground" size="1.5rem" />
        </div>
      {/if}
    </div>
  </button>

  <!-- 画像が選択されていて、クリア可能で、読み取り専用でなく、入力が無効でない場合にクリアボタンを表示 -->
  {#if src && clearable && !readonly && !inputAttributes.disabled}
    <button class="absolute top-0.5 right-0.5 flex items-center justify-center size-5.5 bg-surface border border-border rounded-full outline-ring outline-offset-2 focus-visible:outline-2 cursor-pointer" onclick={handleClear} aria-label="画像をクリア">
      <X size="0.875rem" />
    </button>
  {/if}
</div>

      

使い方


サンプル

Default

特に操作が行われていない、デフォルトの状態です。

Filled

画像が選択され、プレビューが表示されている状態です。

Error

入力内容に問題があり、エラーが表示されている状態です。

Disabled

操作不可の状態です。

Readonly

写真の選択ができない、表示のみの状態です。

Clearable

clearableプロパティをtrueに設定すると、選択済み画像をクリアするボタンが表示されます。

Slot

任意の見た目やレイアウトをスロットで上書きできます。