Input
Inputは、1行のテキストやファイルなどを入力するためのコンポーネントです。
プレースホルダーやラベル、ボタンとの組み合わせなど、さまざまなユースケースに対応しています。
<script lang="ts">
import Input from '$lib/components/ui/atoms/Input.svelte';
let value = $state('');
</script>
<Input type="text" placeholder="プレースホルダー" bind:value />
プロパティ
Inputは、以下のプロパティをサポートしています。
| 名前 | 型 | デフォルト値 | 説明 |
|---|---|---|---|
type |
string |
text |
入力の種類を指定します。 |
placeholder |
string |
入力欄に表示されるヒントテキストです。 | |
readonly |
boolean |
false |
読み取り専用の状態にします。編集はできません。 |
disabled |
boolean |
false |
入力を無効化します。操作できません。 |
isError |
boolean |
false |
エラー状態を視覚的に示します。バリデーション用など。 |
インストールの手順
以下のコンポーネントのコードを、使いたいプロジェクトにコピー&ペーストします。
パスは実際のプロジェクトの構成にあわせて更新してください。
atoms/Input.svelte
<!--
@component
## 概要
- 1行のテキストを入力する際に使用されるコンポーネントです
## 機能
- 見た目を変更するためのいくつかのスタイル用Propsが追加されています(詳細はPropsセクションを参照)
## Props
- type: inputのtypeを指定できます
- placeholder: プレースホルダーの文言を指定できます
- isError: true の場合エラー時のスタイルを適用します
- readonly: 指定すると読み取り専用の状態です
- disabled: 指定するとグレーアウトされ、クリック不可になります
## Usage
```svelte
<Input type="text" placeholder="プレースホルダー" bind:value />
```
-->
<script module lang="ts">
import type { Snippet } from 'svelte';
import type { ClassValue, HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements';
import { cva, type VariantProps } from 'class-variance-authority';
export const inputVariants = cva('relative w-full min-h-10 px-2.75 py-1.75 bg-base-container-default border border-base-stroke-default rounded-md text-base-foreground-default text-sm outline-primary transition-colors [&:not(:disabled):not([readonly])]:hover:bg-base-container-accent/90 active:border-base-stroke-default placeholder:text-base-foreground-muted focus-visible:outline-[0.125rem] focus-visible:outline-offset-[0.125rem] focus-visible:outline-primary', {
variants: {
/** type属性 */
type: {
text: [],
file: ['text-base-foreground-muted file:px-2 file:text-base-foreground-default file:mr-0'],
} as Partial<Record<HTMLInputTypeAttribute, string[]>>,
/** 操作できるかどうか */
disabled: {
true: ['opacity-50 cursor-not-allowed'],
false: [],
},
/** 読み取り専用かどうか */
readonly: {
true: ['opacity-50'],
false: [],
},
/** エラーかどうか */
isError: {
true: ['border-destructive'],
false: [],
},
},
compoundVariants: [
{
type: 'file',
readonly: true,
class: 'cursor-not-allowed',
},
],
defaultVariants: {
type: 'text',
disabled: false,
readonly: false,
},
});
export const startContentVariants = cva('absolute inset-y-0 left-0 z-10 flex items-center pr-1 pl-3 pointer-events-none m-auto align-middle', {
variants: {
disabled: {
true: ['cursor-not-allowed'],
false: [],
},
},
defaultVariants: {
disabled: false,
},
});
export const endContentVariants = cva('absolute inset-y-0 right-0 z-10 flex items-center pr-3 pl-1 pointer-events-none m-auto align-middle', {
variants: {
disabled: {
true: ['cursor-not-allowed'],
false: [],
},
},
defaultVariants: {
disabled: false,
},
});
export type InputVariants = VariantProps<typeof inputVariants>;
export interface InputProps extends InputVariants, HTMLInputAttributes {
// inputのクラスを制御する値
inputClass?: ClassValue;
startContent?: Snippet;
endContent?: Snippet;
/** クラス */
class?: ClassValue;
}
let inputElement = $state<HTMLElement>();
export function focus() {
if (inputElement) {
inputElement?.focus();
}
}
</script>
<script lang="ts">
let { isError = false, class: className, inputClass, value = $bindable(''), startContent, endContent, ...inputAttributes }: InputProps = $props();
let startContentElement = $state<HTMLElement>();
let endContentElement = $state<HTMLElement>();
// 左側にコンテンツが来る場合の余白
let inputLeftPadding = $derived.by(() => {
if (startContentElement) {
return startContentElement.clientWidth;
}
});
// 右側にコンテンツが来る場合の余白
let inputRightPadding = $derived.by(() => {
if (endContentElement) {
return endContentElement.clientWidth;
}
});
let inputVariantClass = $derived(inputVariants({ type: inputAttributes.type, disabled: inputAttributes.disabled, readonly: inputAttributes.readonly, class: inputClass, isError }));
let startContentVariantClass = $derived(startContentVariants({ disabled: inputAttributes.disabled }));
let endContentVariantClass = $derived(endContentVariants({ disabled: inputAttributes.disabled }));
</script>
<div class={[className, 'relative z-0 inline-block w-full']}>
{#if startContent}
<div class={startContentVariantClass} bind:this={startContentElement}>
<div class="contents pointer-events-auto">{@render startContent()}</div>
</div>
{/if}
<input class={inputVariantClass} {...inputAttributes} bind:value bind:this={inputElement} style="padding-left: {inputLeftPadding}px; padding-right: {inputRightPadding}px" />
{#if endContent}
<div class={endContentVariantClass} bind:this={endContentElement}>
<div class="contents pointer-events-auto">{@render endContent()}</div>
</div>
{/if}
</div>
<style>
/* autofill用の考慮 */
input:-webkit-autofill {
box-shadow: 0 0 0 1000px rgb(255, 255, 255) inset !important;
-webkit-text-fill-color: #18181b !important;
}
.dark input:-webkit-autofill {
box-shadow: 0 0 0 1000px rgb(0, 9, 11) inset !important;
-webkit-text-fill-color: #fafafa !important;
}
</style>
使い方
<script lang="ts">
import Input from '$lib/components/ui/atoms/Input.svelte';
let value = $state('');
</script>
<Input type="text" placeholder="プレースホルダー" bind:value />
サンプル
Text
基本的なテキスト入力の状態です。
<script lang="ts">
import Input from '$lib/components/ui/atoms/Input.svelte';
let value = $state('');
</script>
<Input type="text" placeholder="プレースホルダー" bind:value />
Readonly
内容を編集できない、読み取り専用の状態です。
<script lang="ts">
import Input from '$lib/components/ui/atoms/Input.svelte';
let value = $state('入力済みのテキスト');
</script>
<Input type="text" readonly placeholder="プレースホルダー" bind:value />
Error
入力内容に問題があり、エラーが表示されている状態です。
<script lang="ts">
import Input from '$lib/components/ui/atoms/Input.svelte';
let value = $state('');
let isError = $derived(value === '');
</script>
<div class="flex flex-col w-full">
<Input type="text" placeholder="プレースホルダー" required {isError} bind:value />
{#if isError}
<p class="text-destructive text-sm mt-2">ここにエラーメッセージが入ります。</p>
{/if}
</div>
Disabled
利用不可の状態です。
<script lang="ts">
import Input from '$lib/components/ui/atoms/Input.svelte';
let value = $state('');
</script>
<Input type="text" disabled placeholder="プレースホルダー" bind:value />
With Label
入力フィールドとラベルを組み合わせた例です。
<script lang="ts">
import Input from '$lib/components/ui/atoms/Input.svelte';
import Label from '$lib/components/ui/atoms/Label.svelte';
let value = $state('');
</script>
<div class="flex flex-col w-full">
<Label class="mb-1.5" for="input">ラベル</Label>
<p class="text-base-foreground-muted text-sm mb-2">ここに補足文が入ります。</p>
<Input id="input" type="text" placeholder="プレースホルダー" bind:value />
</div>
With Button
入力フィールドとボタンを組み合わせた例です。
<script lang="ts">
import Button from '$lib/components/ui/atoms/Button.svelte';
import Input from '$lib/components/ui/atoms/Input.svelte';
let value = $state('');
</script>
<div class="flex items-center max-w-sm gap-2 w-full">
<Input id="input" type="text" placeholder="プレースホルダー" bind:value />
<Button class="shrink-0">ボタン</Button>
</div>
With Icon
入力フィールドとアイコンを組み合わせた例です。
<script lang="ts">
import Input from '$lib/components/ui/atoms/Input.svelte';
import { Mic, Search } from '@lucide/svelte';
let firstInputValue = $state('');
let secondInputValue = $state('');
</script>
<div class="flex flex-col gap-4 w-full">
<Input type="text" placeholder="プレースホルダー" bind:value={firstInputValue}>
{#snippet startContent()}
<Search class="pointer-events-none text-base-foreground-muted" size="1rem" />
{/snippet}
</Input>
<Input type="text" placeholder="プレースホルダー" bind:value={secondInputValue}>
{#snippet endContent()}
<Mic class="cursor-pointer pointer-events-none text-base-foreground-muted" size="1rem" />
{/snippet}
</Input>
</div>
Input Password
入力フィールドをパスワード入力欄として利用した例です。
<script lang="ts">
import Input from '$lib/components/ui/atoms/Input.svelte';
import { Eye, EyeClosed } from '@lucide/svelte';
let value = $state('');
let type = $state('password');
function toggleInputType(e) {
e.preventDefault();
e.stopPropagation();
if (type === 'text') {
type = 'password';
}
else {
type = 'text';
}
}
</script>
<Input {type} placeholder="プレースホルダー" bind:value>
{#snippet endContent()}
<button class="cursor-pointer p-1" type="button" onclick={toggleInputType}>
{#if type === 'text'}
<Eye class="text-base-foreground-muted" size="1rem" />
{:else}
<EyeClosed class="text-base-foreground-muted" size="1rem" />
{/if}
</button>
{/snippet}
</Input>
Max Length
入力可能な最大文字数を設定し、超えるとエラーが表示されます。
<script lang="ts">
import Input from '$lib/components/ui/atoms/Input.svelte';
import { Mic, Search } from '@lucide/svelte';
let value = $state('');
let isError = $derived(value.length > 10);
let maxlength = 10;
let overTexts = $derived(maxlength - value.length);
</script>
<div class="flex flex-col w-full gap-4">
<div>
<Input type="text" placeholder="プレースホルダー" {isError} bind:value>
{#snippet startContent()}
<Search class="text-base-foreground-muted pointer-events-none" size="1rem" />
{/snippet}
</Input>
<div class="flex items-start justify-between w-full gap-2 mt-2">
<div>
{#if isError}
<p class="text-destructive text-sm">ここにエラーメッセージが入ります。</p>
{/if}
</div>
<p class="shrink-0 text-sm/normal">
<span class={[isError && 'text-destructive', 'mr-0.5']}>{value.length}</span><span class="text-base-foreground-subtle">/{maxlength}</span>
</p>
</div>
</div>
<div>
<Input type="text" placeholder="プレースホルダー" {isError} bind:value>
{#snippet endContent()}
<Mic class="text-base-foreground-muted pointer-events-none cursor-pointer" size="1rem" />
{/snippet}
</Input>
<div class="flex items-start justify-between w-full gap-2 mt-2">
<div>
{#if isError}
<p class="text-destructive text-sm">ここにエラーメッセージが入ります。</p>
{/if}
</div>
<p class="shrink-0 text-sm/normal">
{#if isError}
<span class="text-destructive mr-0.5">{overTexts}</span>
{:else}
<span class="mr-0.5">{value.length}</span><span class="text-base-foreground-subtle">/{maxlength}</span>
{/if}
</p>
</div>
</div>
</div>