๐ข Textarea
Basic
You can use NInput with the textarea"
type to create a basic textarea.
This component shares the same API as the NInput component. So you can use all the same props, variants, slots, events, etc. as the input component.
Rows and Cols
rows={value}
- Set the number of rows for the textarea.
cols={value}
- Set the number of columns for the textarea.
Autosizing
autoresize={value}
- Enable autosizing of the textarea, you can also pass a number to set the maximum number of rows.
Value | Description |
---|---|
true | The textarea will automatically adjust its height to fit the content. |
false | The textarea will not automatically adjust its height to fit the content. |
number | The textarea will automatically adjust its height to fit the content, but will not exceed the specified number of rows. |
Resizing
resize="{value}"
- change the resize behavior of the textarea.
Value | Description |
---|---|
none | The textarea will not be resizable. (Default) |
null | The textarea will be resizable vertically and horizontally. |
y | The textarea will be resizable vertically. |
x | The textarea will be resizable horizontally. |
The examples below utilize
NFormGroup
. Refer to the FormGroup section for more details.Props
export interface NInputProps {
/**
*
* @default null
*/
type?: 'text' | 'password' | 'email' | 'number' | 'tel' | 'url' | '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.42857142857142855em 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 { computed, onMounted, ref } from 'vue'
import NIcon from '../elements/Icon.vue'
import type { NInputProps } from '../../types'
import { randomId } from '../../utils'
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() {
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) {
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>
Table of Contents