๐ข Input
Basic
use the NInput
tag to create a basic input.
id
for the input for accessibility purposes. If you want to override this behavior, you can define the id
attribute manually.Variants
input="{variant}"
- change the variant of the input outline.
Variant | Description |
---|---|
outline | The default variant. |
solid | The solid variant. |
~ | The unstyle or base variant |
Color
input="{variant}-{color}"
- change the color of the input outline.
primary
. You can also add your own colors to the palette through the Configuration section.Size
size="{size}"
- change the size of the input.
๐ You can freely adjust the size of the input using any size imaginable. No limits exist, and you can use
breakpoints
such assm:sm, xs:lg
to change size based on screen size orstates
such ashover:lg, focus:3xl
to change size based on input state and more.
padding
, icons
, and text-size
of the input scale depends on the size
. If you want to change the text-size
and padding
simultaneously, you can always customize it using utility classes.Icon
trailing="{icon}"
- add a trailing icon to the input outline.
leading="{icon}"
- add a leading icon to the input outline.
heroicons
and tabler
for the icons, you can use any icon provided by Iconify
through icones, refer to configuration for more information.Loading
loading
- add a loading icon to the input outline.
Status
status="{status}"
- change the status of the input outline.
disabled
- disable the input.
readonly
- make the input readonly.
Events
@leading
- emit an event when the leading icon is clicked.
@trailing
- emit an event when the trailing icon is clicked.
leading
and trailing
are wrapped around pointer-events-none
class, if you want to remove this behavior, you can use pointer-events-auto
class.Slots
Leading
#leading
- add a leading slot to the input.
Trailing
#trailing
- add a trailing slot to the input.
Props
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
}
}
Presets
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-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,
]
Component
<script setup lang="ts">
import type { NInputProps } from '../../types'
import { computed, onMounted, ref } from 'vue'
import { 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="[
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="[
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>