Skip to main content
Nexus Design System
GitHub

Input

Input collects one line of text. Give every input a visible label or an aria-label. For several lines of text, use Textarea.

'use client';

import { Input } from '@/components/nxds/components/input/input';

export default function InputTypeEmail() {
  return (
    <div className="nx:w-[400px]">
      <Input type="email" placeholder="email@example.com" />
    </div>
  );
}

Installation

npx nxds-cli@latest add input

Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.

Usage

import { Input } from '@/components/nxds/components/input/input';

Examples

With label

Link a Label to the input with htmlFor and a matching id.

'use client';

import * as React from 'react';

import { Input } from '@/components/nxds/components/input/input';
import { Label } from '@/components/nxds/components/label/label';

export default function InputWithLabel() {
  const inputId = React.useId();

  return (
    <div className="nx:w-[400px]">
      <div className="nx:grid nx:gap-1.5">
        <Label htmlFor={inputId}>Full name</Label>
        <Input id={inputId} placeholder="Ada Lovelace" />
      </div>
    </div>
  );
}

This example also needs: Label.

Disabled

Set disabled to block editing and dim the field.

'use client';

import { Input } from '@/components/nxds/components/input/input';

export default function InputDisabled() {
  return (
    <div className="nx:w-[400px]">
      <Input placeholder="Disabled input" disabled />
    </div>
  );
}

Invalid

Set aria-invalid to show the error border.

'use client';

import { Input } from '@/components/nxds/components/input/input';

export default function InputInvalid() {
  return (
    <div className="nx:w-[400px]">
      <Input defaultValue="invalid@" aria-invalid aria-label="Invalid email" />
    </div>
  );
}

All variants

Compare every size, variant, state and type side by side.

Sizes

sm
default
lg

Variants

bordered
borderless
ghost

States

empty
filled
disabled

Types

text
email
password
number
file
'use client';

import { Input } from '@/components/nxds/components/input/input';

export default function InputAllVariants() {
  return (
    <div className="nx:w-[400px]">
      <div className="nx:flex nx:flex-col nx:gap-8 nx:w-[400px]">
        <div>
          <h3 className="nx:text-foreground nx:mb-4 nx:typography-label-default">
            Sizes
          </h3>
          <div className="nx:flex nx:flex-col nx:gap-3">
            <div className="nx:flex nx:items-center nx:gap-4">
              <span className="nx:typography-label-small nx:text-muted-foreground nx:w-16">
                sm
              </span>
              <Input size="sm" placeholder="Small input" />
            </div>
            <div className="nx:flex nx:items-center nx:gap-4">
              <span className="nx:typography-label-small nx:text-muted-foreground nx:w-16">
                default
              </span>
              <Input size="default" placeholder="Default input" />
            </div>
            <div className="nx:flex nx:items-center nx:gap-4">
              <span className="nx:typography-label-small nx:text-muted-foreground nx:w-16">
                lg
              </span>
              <Input size="lg" placeholder="Large input" />
            </div>
          </div>
        </div>

        <div>
          <h3 className="nx:text-foreground nx:mb-4 nx:typography-label-default">
            Variants
          </h3>
          <div className="nx:flex nx:flex-col nx:gap-3">
            <div className="nx:flex nx:items-center nx:gap-4">
              <span className="nx:typography-label-small nx:text-muted-foreground nx:w-20">
                bordered
              </span>
              <Input placeholder="Bordered input" />
            </div>
            <div className="nx:flex nx:items-center nx:gap-4">
              <span className="nx:typography-label-small nx:text-muted-foreground nx:w-20">
                borderless
              </span>
              <Input variant="borderless" placeholder="Borderless input" />
            </div>
            <div className="nx:flex nx:items-center nx:gap-4">
              <span className="nx:typography-label-small nx:text-muted-foreground nx:w-20">
                ghost
              </span>
              <Input variant="ghost" placeholder="Ghost input" />
            </div>
          </div>
        </div>

        <div>
          <h3 className="nx:text-foreground nx:mb-4 nx:typography-label-default">
            States
          </h3>
          <div className="nx:flex nx:flex-col nx:gap-3">
            <div className="nx:flex nx:items-center nx:gap-4">
              <span className="nx:typography-label-small nx:text-muted-foreground nx:w-16">
                empty
              </span>
              <Input placeholder="Placeholder text" />
            </div>
            <div className="nx:flex nx:items-center nx:gap-4">
              <span className="nx:typography-label-small nx:text-muted-foreground nx:w-16">
                filled
              </span>
              <Input defaultValue="Filled value" aria-label="Filled input" />
            </div>
            <div className="nx:flex nx:items-center nx:gap-4">
              <span className="nx:typography-label-small nx:text-muted-foreground nx:w-16">
                disabled
              </span>
              <Input placeholder="Disabled" disabled />
            </div>
          </div>
        </div>

        <div>
          <h3 className="nx:text-foreground nx:mb-4 nx:typography-label-default">
            Types
          </h3>
          <div className="nx:flex nx:flex-col nx:gap-3">
            <div className="nx:flex nx:items-center nx:gap-4">
              <span className="nx:typography-label-small nx:text-muted-foreground nx:w-16">
                text
              </span>
              <Input type="text" placeholder="Text input" />
            </div>
            <div className="nx:flex nx:items-center nx:gap-4">
              <span className="nx:typography-label-small nx:text-muted-foreground nx:w-16">
                email
              </span>
              <Input type="email" placeholder="email@example.com" />
            </div>
            <div className="nx:flex nx:items-center nx:gap-4">
              <span className="nx:typography-label-small nx:text-muted-foreground nx:w-16">
                password
              </span>
              <Input type="password" placeholder="••••••••" />
            </div>
            <div className="nx:flex nx:items-center nx:gap-4">
              <span className="nx:typography-label-small nx:text-muted-foreground nx:w-16">
                number
              </span>
              <Input type="number" placeholder="0" />
            </div>
            <div className="nx:flex nx:items-center nx:gap-4">
              <span className="nx:typography-label-small nx:text-muted-foreground nx:w-16">
                file
              </span>
              <Input type="file" aria-label="File upload" />
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

Props

Input

Input props
PropTypeDefaultDescription
size"default" | "sm" | "lg" | null"default"No description
variant"ghost" | "borderless" | "bordered" | null"bordered"No description

Input collects one line of text. Give every input a visible label or an aria-label. For several lines of text, use Textarea.

<script setup lang="ts">
import { Input } from '@/components/input';
</script>

<template>
  <Input type="email" placeholder="email@example.com" />
</template>

Installation

  1. Install the following dependencies:

    npm install @vueuse/core@^14.1.0 class-variance-authority@^0.7.1
    
  2. Copy and paste the following code into your project.

    components/input/Input.vue
    <script setup lang="ts">
    import { useVModel } from '@vueuse/core';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    import { type InputVariants, inputVariants } from '.';
    
    export interface InputProps {
      size?: InputVariants['size'];
      variant?: InputVariants['variant'];
      /** The value, for `v-model`. */
      modelValue?: string | number;
      /** The starting value when `v-model` isn't bound. */
      defaultValue?: string | number;
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<InputProps>(), {
      size: 'default',
      variant: 'bordered',
    });
    const emits = defineEmits<{
      'update:modelValue': [value: string | number];
    }>();
    
    const model = useVModel(props, 'modelValue', emits, {
      passive: true,
      defaultValue: props.defaultValue,
    });
    </script>
    
    <template>
      <input
        :value="model"
        data-slot="input"
        :data-size="size"
        :data-variant="variant"
        :class="cn(inputVariants({ size, variant }), props.class)"
        @input="model = ($event.target as HTMLInputElement).value"
      />
    </template>
    
    components/input/index.ts
    import { cva, type VariantProps } from 'class-variance-authority';
    
    import { fieldSizeVariants } from '../../lib/field-size';
    
    export { default as Input, type InputProps } from './Input.vue';
    
    export const inputVariants = cva(
      [
        'nx:flex nx:box-border nx:w-full nx:rounded-md nx:border-default',
        'nx:text-foreground nx:autofill-text-foreground nx:transition-field',
        'nx:file:border-0 nx:file:bg-transparent nx:file:typography-label-default nx:file:text-foreground nx:disabled:file:text-disabled-foreground',
        'nx:placeholder:text-muted-foreground',
        'nx:focus-visible:outline-default nx:focus-visible:outline-focus-default nx:focus-visible:border-focus-default',
        'nx:aria-invalid:border-error-border nx:aria-invalid:focus-visible:outline-focus-error nx:aria-invalid:focus-visible:border-focus-error',
        'nx:disabled:cursor-not-allowed nx:disabled:bg-disabled nx:disabled:autofill-bg-disabled nx:disabled:text-disabled-foreground nx:disabled:autofill-text-disabled-foreground nx:disabled:placeholder:text-disabled-foreground',
      ],
      {
        variants: {
          size: fieldSizeVariants,
          variant: {
            bordered:
              'nx:border-border-default nx:bg-container nx:autofill-bg-container nx:enabled:hover:bg-container-hover nx:enabled:hover:autofill-bg-container-hover nx:disabled:border-border-disabled',
            borderless:
              'nx:border-transparent nx:bg-control-background nx:autofill-bg-control-background nx:enabled:hover:bg-control-background-hover nx:enabled:hover:autofill-bg-control-background-hover',
            ghost:
              'nx:border-transparent nx:bg-transparent nx:autofill-bg-transparent nx:enabled:hover:not-aria-invalid:not-focus-visible:border-border-default',
          },
        },
        defaultVariants: {
          size: 'default',
          variant: 'bordered',
        },
      }
    );
    
    export type InputVariants = VariantProps<typeof inputVariants>;
    
    lib/field-size.ts
    export const fieldSizeVariants = {
      default: 'nx:h-10 nx:px-3 nx:py-0 nx:typography-body-default',
      sm: 'nx:h-8 nx:px-2.5 nx:py-0 nx:typography-body-small',
      lg: 'nx:h-12 nx:px-3.5 nx:py-0 nx:typography-body-default',
    };
    
  3. Keep each file at the path shown above it.

    Paths are relative to packages/vue/src/. Keep the same folders under your @/ root, and the imports between files work without edits.

Usage

import { Input } from '@/components/input';

Examples

With label

Link a Label to the input with for and a matching id.

<script setup lang="ts">
import { useId } from 'vue';

import { Input } from '@/components/input';
import { Label } from '@/components/label';

const inputId = useId();
</script>

<template>
  <div class="nx:grid nx:gap-1.5">
    <Label :for="inputId">Full name</Label>
    <Input :id="inputId" placeholder="Ada Lovelace" />
  </div>
</template>

This example also needs: Label.

Disabled

Set disabled to block editing and dim the field.

<script setup lang="ts">
import { Input } from '@/components/input';
</script>

<template>
  <Input placeholder="Disabled input" disabled />
</template>

Invalid

Set aria-invalid to show the error border.

<script setup lang="ts">
import { Input } from '@/components/input';
</script>

<template>
  <Input
    default-value="invalid@"
    aria-invalid="true"
    aria-label="Invalid email"
  />
</template>

All variants

Compare every size, variant, state and type side by side.

<script setup lang="ts">
import { Input } from '@/components/input';
</script>

<template>
  <div class="nx:flex nx:flex-col nx:gap-8 nx:w-[400px]">
    <div>
      <h3 class="nx:text-foreground nx:mb-4 nx:typography-label-default">
        Sizes
      </h3>
      <div class="nx:flex nx:flex-col nx:gap-3">
        <div class="nx:flex nx:items-center nx:gap-4">
          <span
            class="nx:typography-label-small nx:text-muted-foreground nx:w-16"
          >
            sm
          </span>
          <Input size="sm" placeholder="Small input" />
        </div>
        <div class="nx:flex nx:items-center nx:gap-4">
          <span
            class="nx:typography-label-small nx:text-muted-foreground nx:w-16"
          >
            default
          </span>
          <Input size="default" placeholder="Default input" />
        </div>
        <div class="nx:flex nx:items-center nx:gap-4">
          <span
            class="nx:typography-label-small nx:text-muted-foreground nx:w-16"
          >
            lg
          </span>
          <Input size="lg" placeholder="Large input" />
        </div>
      </div>
    </div>

    <div>
      <h3 class="nx:text-foreground nx:mb-4 nx:typography-label-default">
        Variants
      </h3>
      <div class="nx:flex nx:flex-col nx:gap-3">
        <div class="nx:flex nx:items-center nx:gap-4">
          <span
            class="nx:typography-label-small nx:text-muted-foreground nx:w-20"
          >
            bordered
          </span>
          <Input placeholder="Bordered input" />
        </div>
        <div class="nx:flex nx:items-center nx:gap-4">
          <span
            class="nx:typography-label-small nx:text-muted-foreground nx:w-20"
          >
            borderless
          </span>
          <Input variant="borderless" placeholder="Borderless input" />
        </div>
        <div class="nx:flex nx:items-center nx:gap-4">
          <span
            class="nx:typography-label-small nx:text-muted-foreground nx:w-20"
          >
            ghost
          </span>
          <Input variant="ghost" placeholder="Ghost input" />
        </div>
      </div>
    </div>

    <div>
      <h3 class="nx:text-foreground nx:mb-4 nx:typography-label-default">
        States
      </h3>
      <div class="nx:flex nx:flex-col nx:gap-3">
        <div class="nx:flex nx:items-center nx:gap-4">
          <span
            class="nx:typography-label-small nx:text-muted-foreground nx:w-16"
          >
            empty
          </span>
          <Input placeholder="Placeholder text" />
        </div>
        <div class="nx:flex nx:items-center nx:gap-4">
          <span
            class="nx:typography-label-small nx:text-muted-foreground nx:w-16"
          >
            filled
          </span>
          <Input default-value="Filled value" aria-label="Filled input" />
        </div>
        <div class="nx:flex nx:items-center nx:gap-4">
          <span
            class="nx:typography-label-small nx:text-muted-foreground nx:w-16"
          >
            disabled
          </span>
          <Input placeholder="Disabled" disabled />
        </div>
      </div>
    </div>

    <div>
      <h3 class="nx:text-foreground nx:mb-4 nx:typography-label-default">
        Types
      </h3>
      <div class="nx:flex nx:flex-col nx:gap-3">
        <div class="nx:flex nx:items-center nx:gap-4">
          <span
            class="nx:typography-label-small nx:text-muted-foreground nx:w-16"
          >
            text
          </span>
          <Input type="text" placeholder="Text input" />
        </div>
        <div class="nx:flex nx:items-center nx:gap-4">
          <span
            class="nx:typography-label-small nx:text-muted-foreground nx:w-16"
          >
            email
          </span>
          <Input type="email" placeholder="email@example.com" />
        </div>
        <div class="nx:flex nx:items-center nx:gap-4">
          <span
            class="nx:typography-label-small nx:text-muted-foreground nx:w-16"
          >
            password
          </span>
          <Input type="password" placeholder="••••••••" />
        </div>
        <div class="nx:flex nx:items-center nx:gap-4">
          <span
            class="nx:typography-label-small nx:text-muted-foreground nx:w-16"
          >
            number
          </span>
          <Input type="number" placeholder="0" />
        </div>
        <div class="nx:flex nx:items-center nx:gap-4">
          <span
            class="nx:typography-label-small nx:text-muted-foreground nx:w-16"
          >
            file
          </span>
          <Input type="file" aria-label="File upload" />
        </div>
      </div>
    </div>
  </div>
</template>

Props

Input

Input props
PropTypeDefaultDescription
defaultValuestring | numberNo default documented

The starting value when v-model isn't bound.

modelValuestring | numberNo default documented

The value, for v-model.

size"default" | "sm" | "lg" | null"default"No description
variant"ghost" | "bordered" | "borderless" | null"bordered"No description