Examples
Basic
Prop | Default | Type | Description |
---|---|---|---|
type | - | text , password , email , number , tel , url , search , textarea | The type of input field. |
reverse | false | boolean | Swap the position of the leading and trailing icons. |
modelValue | - | any | Value of the input. Can be a string or a number. |
id | randomId() | string | Manually set the id attribute. By default, the id attribute is generated randomly for accessibility reasons. |
readonly | false | boolean | Make the input readonly. |
disabled | false | boolean | Disable the input. |
Preview
Code
<script setup lang="ts">
const value = ref('')
</script>
<template>
<div class="flex">
<NInput
v-model="value"
type="text"
placeholder="Enter your name"
/>
</div>
</template>
Variant
Prop | Default | Type | Description |
---|---|---|---|
input | outline | {variant} | The variant of the input. |
Variant | Description |
---|---|
outline | The default variant. |
solid | The solid variant. |
~ | The unstyle or base variant |
Preview
Code
<template>
<div class="grid gap-4 sm:cols-2">
<NInput
input="outline"
placeholder="This is the outline variant"
/>
<NInput
input="solid"
placeholder="This is the solid variant"
/>
<NInput
input="~"
placeholder="This is the base input"
/>
</div>
</template>
Color
Prop | Default | Type | Description |
---|---|---|---|
input | {variant}-primary | {variant}-{color} | The color of the input. |
Preview
Code
Dynamic colors:
Static colors:
<template>
<div class="flex flex-col gap-4">
<span class="text-sm font-medium">Dynamic colors:</span>
<div class="grid gap-4 sm:cols-2">
<NInput
input="outline-primary"
placeholder="This is the primary color (default)"
/>
<NInput
input="outline-indigo"
placeholder="This is the indigo color"
/>
<NInput
input="outline-rose"
placeholder="This is the rose color"
/>
<NInput
input="outline-lime"
placeholder="This is the lime color"
/>
</div>
<NSeparator />
<span class="text-sm font-medium">Static colors:</span>
<div class="flex flex-col gap-4">
<NInput
input="outline-gray"
placeholder="This is the gray color"
/>
<NInput
input="outline-black"
placeholder="This is the black color"
/>
</div>
</div>
</template>
Size
Prop | Default | Type | Description |
---|---|---|---|
size | sm | string | Allows you to change the size of the input. |
🚀 Adjust input size freely using any size, breakpoints (e.g.,
sm:sm, xs:lg
), or states (e.g.,hover:lg, focus:3xl
).
The padding, icons, and text-size of the input scale are dynamically adjusted based on the size property. To customize the text-size and padding simultaneously, you can use utility classes.
Preview
Code
<template>
<div class="grid cols-1 gap-4 sm:cols-2">
<NInput
size="xs"
placeholder="This is extra small size"
/>
<NInput
size="sm"
placeholder="This is small size"
/>
<NInput
size="md"
placeholder="This is base or md size (default)"
/>
<NInput
size="2xl"
placeholder="This is 2xl size"
/>
<NInput
size="lg"
class="rounded-full"
placeholder="This is custom"
/>
<NInput
size="26px"
placeholder="This is custom"
class="rounded-none"
/>
<NInput
:una="{
inputWrapper: 'sm:col-span-2',
}"
size="6rem"
placeholder="This is 6rem size"
/>
</div>
</template>
Leading and Trailing Icons
Prop | Default | Type | Description |
---|---|---|---|
leading | - | string | Display leading icon. |
trailing | - | string | Display trailing icon. |
Preview
Code
<template>
<div class="grid cols-1 gap-4 sm:cols-2">
<div class="sm:col-span-1">
<NInput
leading="i-heroicons-magnifying-glass-20-solid"
placeholder="This is leading icon"
/>
</div>
<div class="sm:col-span-1">
<NInput
trailing="i-heroicons-question-mark-circle-20-solid text-primary"
placeholder="This is trailing icon with custom class"
/>
</div>
<div class="sm:col-span-2">
<NInput
input="outline-purple"
size="1.3rem"
leading="i-heroicons-paper-clip-20-solid"
trailing="i-heroicons-chat-bubble-left-ellipsis-20-solid"
:una="{
inputLeading: 'text-yellow',
inputTrailing: 'text-blue',
}"
placeholder="You can also use una to add custom class"
/>
</div>
</div>
</template>
Read more in Icon component
Loading
Prop | Default | Type | Description |
---|---|---|---|
loading | - | boolean | Display loading state. |
Preview
Code
<template>
<div class="grid cols-1 gap-4 sm:cols-2">
<NInput
disabled
loading
placeholder="This is the disabled variant with loading indicator"
/>
<NInput
:una="{
inputLoading: 'text-lime',
inputLoadingIcon: 'i-tabler-fidget-spinner',
}"
loading
placeholder="Custom color loading icon"
/>
<NInput
:una="{
inputLoading: 'text-rose animate-none',
inputLoadingIcon: 'i-svg-spinners-blocks-shuffle-3',
}"
loading
reverse
placeholder="Loading icon is on the left side"
/>
<NInput
:una="{
inputLoading: 'animate-pulse text-yellow',
inputLoadingIcon: 'i-heroicons-ellipsis-horizontal-20-solid',
}"
loading
placeholder="This is possible too"
/>
</div>
</template>
Status
Prop | Default | Type | Description |
---|---|---|---|
status | - | info , success , warning , error | Update the input status. Useful for validations. |
Preview
Code
<template>
<div class="grid gap-4 sm:cols-2">
<NInput
status="error"
placeholder="This is the outline variant with error status"
/>
<NInput
type="email"
status="success"
placeholder="This is the outline variant with success status"
/>
<NInput
type="email"
status="warning"
placeholder="This is the outline variant with warning status"
/>
<NInput
type="email"
status="info"
placeholder="This is the outline variant with info status"
/>
</div>
</template>
Read more in Icon component
Events
Event | Description |
---|---|
@leading | Emit an event when the leading icon is clicked. |
@trailing | Emit an event when the trailing icon is clicked. |
Leading and trailing icons are wrapped in pointer-events-none by default. Use pointer-events-auto to remove this behavior.
Preview
Code
<script setup lang="ts">
function click(description: string) {
// eslint-disable-next-line no-alert
alert(description)
}
const isPasswordVisible = ref(false)
</script>
<template>
<div class="grid cols-1 gap-4 sm:cols-2">
<NInput
:type="isPasswordVisible ? 'text' : 'password'"
:trailing="isPasswordVisible ? 'i-heroicons-eye-20-solid' : 'i-heroicons-eye-slash-20-solid'"
:una="{
inputTrailing: 'pointer-events-auto cursor-pointer',
}"
model-value="Password"
@trailing="isPasswordVisible = !isPasswordVisible"
/>
<NInput
input="outline-purple"
leading="i-heroicons-hand-thumb-up-20-solid"
trailing="i-heroicons-arrow-down-tray-20-solid "
:una="{
inputLeading: 'active:scale-120 text-blue pointer-events-auto cursor-pointer active:text-green',
inputTrailing: 'active:scale-90 text-yellow pointer-events-auto cursor-pointer active:text-lime',
}"
placeholder="Leading and trailing icons are clickable"
@leading="click('leading icon is clicked')"
@trailing="click('trailing icon is clicked')"
/>
</div>
</template>
Slots
Name | Props | Description |
---|---|---|
default | - | The content slot. |
leading | - | The leading slot. |
trailing | - | The trailing slot. |
Leading
Add a leading slot to the input.
Preview
Code
<template>
<div class="flex">
<NInput
placeholder="Search"
trailing="i-heroicons-chat-bubble-left-right-20-solid"
class="pl-12"
>
<template #leading>
<NAvatar
avatar="~"
square="6"
src="https://avatars.githubusercontent.com/u/33350692?s=400&u=49395c835e8197ae2ee42ca02c95e828d8f64239&v=4"
/>
</template>
</NInput>
</div>
</template>
Trailing
Aad a trailing slot to the input.
Preview
Code
USD
<template>
<div class="flex">
<NInput
leading="i-heroicons-currency-dollar-20-solid"
placeholder="Search"
class="pr-12"
>
<template #trailing>
<span class="text-sm">
USD
</span>
</template>
</NInput>
</div>
</template>
Presets
shortcuts/input.ts
type InputPrefix = 'input'
export const staticInput: Record<`${InputPrefix}-${string}` | InputPrefix, string> = {
// config
'input-default-variant': 'input-outline',
'input-loading-icon': 'i-loading',
'input-info-icon': 'i-info',
'input-error-icon': 'i-error',
'input-success-icon': 'i-success',
'input-warning-icon': 'i-warning',
'input-leading-padding': 'pl-2.9em',
'input-trailing-padding': 'pr-2.9em',
// base
'input': 'text-0.875em leading-6 px-0.8571428571428571em py-0.5em w-full input-disabled ring-base ring-inset placeholder:text-$c-gray-400 block outline-none rounded-md border-0 shadow-sm bg-transparent',
'input-input': 'h-9', // role='input'
'input-textarea': '', // role='textarea'
'input-disabled': 'disabled:(n-disabled)',
'input-status-ring': 'ring-opacity-50 dark:ring-opacity-40',
'input-status-icon-base': 'text-1.042em',
'input-leading': 'text-1.042em',
'input-trailing': 'text-1.042em',
'input-loading': 'animate-spin text-1.042em',
// wrappers
'input-wrapper': 'relative flex items-center',
'input-leading-wrapper': 'pointer-events-none absolute inset-y-0 left-0 flex items-center pl-0.75em text-$c-gray-400',
'input-trailing-wrapper': 'pointer-events-none absolute inset-y-0 right-0 flex items-center pr-0.75em text-$c-gray-400',
// variants
'input-outline-gray': 'focus:ring-2 ring-1',
'input-outline-black': 'ring-1 focus:ring-$c-foreground',
}
export const dynamicInput: [RegExp, (params: RegExpExecArray) => string][] = [
// config
[/^input-focus(-(\S+))?$/, ([, , c = 'primary']) => `focus:ring-2 focus:ring-${c}-500 dark:focus:ring-${c}-400`],
[/^input-status(-(\S+))?$/, ([, , c = 'info']) => `text-${c}-700 dark:text-${c}-200 placeholder-${c}-400/70 dark:placeholder-${c}-300/70`],
// variants
[/^input-outline(-(\S+))?$/, ([, , c = 'primary']) => `ring-1 input-focus-${c}`],
[/^input-solid(-(\S+))?$/, ([, , c = 'primary']) => ` ring-1 input-focus-${c} ring-${c}-500 dark:ring-${c}-400`],
]
export const input = [
...dynamicInput,
staticInput,
]
Props
types/input.ts
export interface NInputProps {
/**
*
* @default null
*/
type?: 'text' | 'password' | 'email' | 'number' | 'tel' | 'url' | 'search' | 'textarea' | ''
/**
* Update the input status.
* Useful for validations.
*
* @default null
*/
status?: 'info' | 'success' | 'warning' | 'error'
/**
* Add loading state to the input.
*
* @default false
*/
loading?: boolean
/**
* Swap the position of the leading and trailing icons.
*
* @default false
*/
reverse?: boolean
/**
* Value of the input.
*
* @default null
*/
modelValue?: string | number
/**
* Display leading icon.
*
* @default null
*/
leading?: string
/**
* Display trailing icon.
*
* @default null
*/
trailing?: string
/**
* Allows you to add `UnaUI` input preset properties,
* Think of it as a shortcut for adding options or variants to the preset if available.
*
* @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/input.ts
* @example
* input="solid-green"
*/
input?: string
/**
* Allows you to change the size of the input.
*
* @default sm
*
* @example
* size="sm" | size="2cm" | size="2rem" | size="2px"
*/
size?: string
/**
* Manually set the id attribute.
*
* By default, the id attribute is generated randomly for accessibility reasons.
*
* @default randomId
* @example
* id="email"
*/
id?: string
/**
* Automatically resize the textarea to fit the content.
* This property only works with the `textarea` type.
*
* @default false
*/
autoresize?: boolean | number
/**
* This property only works with the `textarea` type.
* You can add your own resize preset or use the default one.
*
* @default none
*
* @example
* resize="x" | resize="y" | resize="none" | null
*/
resize?: string | null
/**
* This property only works with the `textarea` type.
*
* @default 3
*/
rows?: number
/**
* This property only works with the `textarea` type.
*
* @default 3
*/
cols?: number
/**
* `UnaUI` preset configuration
*
* @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/input.ts
*/
una?: {
// base
input?: string
inputLoading?: string
inputTrailing?: string
inputLeading?: string
// wrappers
inputWrapper?: string
inputLeadingWrapper?: string
inputTrailingWrapper?: string
// icons
inputWarningIcon?: string
inputErrorIcon?: string
inputSuccessIcon?: string
inputInfoIcon?: string
inputLoadingIcon?: string
}
}
Components
Input.vue
<script setup lang="ts">
import type { NInputProps } from '../../types'
import { computed, onMounted, ref } from 'vue'
import { cn, randomId } from '../../utils'
import NIcon from '../elements/Icon.vue'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<NInputProps>(), {
type: 'text',
resize: 'none',
rows: 3,
})
const emit = defineEmits(['leading', 'trailing', 'update:modelValue'])
const slots = defineSlots<{
leading?: any
trailing?: any
}>()
const id = computed(() => props.id ?? randomId('input'))
const isLeading = computed(() => props.leading || slots.leading)
const isTrailing = computed(() => props.trailing || slots.trailing || props.status || props.loading)
const inputVariants = ['outline', 'solid'] as const
const hasVariant = computed(() => inputVariants.some(inputVariants => props.input?.includes(inputVariants)))
const isBaseVariant = computed(() => props.input?.includes('~'))
const statusClassVariants = computed(() => {
const input = {
info: 'input-status-info input-solid-info input-status-ring',
success: 'input-status-success input-solid-success input-status-ring',
warning: 'input-status-warning input-solid-warning input-status-ring',
error: 'input-status-error input-solid-error input-status-ring',
default: !hasVariant.value && !isBaseVariant.value ? 'input-default-variant' : '',
}
const text = {
info: 'text-info',
success: 'text-success',
warning: 'text-warning',
error: 'text-error',
default: '',
}
const icon = {
info: props.una?.inputWarningIcon ?? 'input-info-icon',
success: props.una?.inputSuccessIcon ?? 'input-success-icon',
warning: props.una?.inputWarningIcon ?? 'input-warning-icon',
error: props.una?.inputErrorIcon ?? 'input-error-icon',
default: '',
}
return {
input: input[props.status ?? 'default'],
text: text[props.status ?? 'default'],
icon: icon[props.status ?? 'default'],
}
})
const reverseClassVariants = computed(() => {
const input = {
false: [{ 'input-leading-padding': isLeading.value }, { 'input-trailing-padding': isTrailing.value }],
true: [{ 'input-trailing-padding': isLeading.value }, { 'input-leading-padding': isTrailing.value }],
}
return {
input: input[props.reverse ? 'true' : 'false'],
leadingWrapper: props.reverse ? 'input-trailing-wrapper' : 'input-leading-wrapper',
trailingWrapper: props.reverse ? 'input-leading-wrapper' : 'input-trailing-wrapper',
}
})
// html refs
const textarea = ref<HTMLTextAreaElement>()
function resizeTextarea(): void {
if (!(props.type === 'textarea' && props.autoresize) || !textarea.value)
return
textarea.value.rows = props.rows
const styles = window.getComputedStyle(textarea.value)
const paddingTop = Number.parseInt(styles.paddingTop)
const paddingBottom = Number.parseInt(styles.paddingBottom)
const padding = paddingTop + paddingBottom
const lineHeight = Number.parseInt(styles.lineHeight)
const { scrollHeight } = textarea.value
const newRows = (scrollHeight - padding) / lineHeight
if (newRows > props.rows)
textarea.value.rows = newRows
const maxAutoresizeRows = typeof props.autoresize === 'number' ? props.autoresize : Number.POSITIVE_INFINITY
if (textarea.value.rows > maxAutoresizeRows)
textarea.value.rows = maxAutoresizeRows
}
function onInput(event: Event): void {
emit('update:modelValue', (event.target as HTMLInputElement).value)
resizeTextarea()
}
onMounted(() => {
resizeTextarea()
})
</script>
<template>
<div
input="wrapper"
:size="size"
:class="una?.inputWrapper"
>
<div
v-if="isLeading"
:class="[
una?.inputLeadingWrapper,
reverseClassVariants.leadingWrapper,
statusClassVariants.text,
]"
>
<slot name="leading">
<NIcon
v-if="leading"
:name="leading"
input="leading"
:class="una?.inputLeading"
@click="emit('leading')"
/>
</slot>
</div>
<Component
:is="props.type !== 'textarea' ? 'input' : 'textarea'"
:id="id"
ref="textarea"
:value="modelValue"
:type="props.type !== 'textarea' ? props.type : undefined"
class="input"
:class="cn(
type === 'textarea' ? 'input-textarea' : 'input-input',
statusClassVariants.input,
reverseClassVariants.input,
una?.input,
)"
:input="input"
:resize="type === 'textarea' ? resize : undefined"
:rows="type === 'textarea' ? rows : undefined"
:cols="type === 'textarea' ? cols : undefined"
v-bind="$attrs"
@input="onInput"
/>
<div
v-if="isTrailing"
:class="cn(
una?.inputTrailingWrapper,
reverseClassVariants.trailingWrapper,
statusClassVariants.text,
)"
>
<NIcon
v-if="loading"
input="loading"
:name="una?.inputLoadingIcon ?? 'input-loading-icon'"
:class="una?.inputLoading"
/>
<NIcon
v-else-if="status"
input="status-icon-base"
:name="statusClassVariants.icon"
/>
<slot v-else name="trailing">
<NIcon
v-if="trailing"
input="trailing"
:class="una?.inputTrailing"
:name="trailing"
@click="emit('trailing')"
/>
</slot>
</div>
</div>
</template>