Skip to content
touchcn
Esc
navigateopen⌘Jpreview
On this page

Input

A text input field with platform-adaptive styling.

A text input — a floating label on Material, an inline label on iOS.

Installation

npx touchcn add input

Usage

<tcn-input label="Email" type="email" [(value)]="email" />
import { TcnInput } from '@/components/ui/input';

<TcnInput label="Email" type="email" onValueChange={setEmail} />
<script setup lang="ts">
import { TcnInput } from '@/components/ui/input';
</script>

<template>
  <TcnInput v-model="email" label="Email" type="email" />
</template>

Props

Prop Type Default Description
label string Floating (MD) / inline (iOS) field label.
value string (model, Angular) '' Two-way bound value.
placeholder string Placeholder text.
type string 'text' Native input type.
error boolean false Tint the outline / border destructive.
disabled boolean false Disable the field.
onValueChange (value: string) => void Convenience callback with the raw value (React).
Source · Angular
export * from './tcn-input';
import { booleanAttribute, Component, input, model } from '@angular/core';

@Component({
  selector: 'tcn-input',
  template: `
    <label
      class="tcn-input relative flex items-center gap-3 px-3 py-2.5"
      [class.tcn-input-labeled]="!!label()"
      [attr.data-error]="error() || null"
    >
      @if (label()) {
        <span class="tcn-input-label">{{ label() }}</span>
      }
      <input
        class="w-full bg-transparent text-[var(--color-on-surface)] outline-none"
        [type]="type()"
        [placeholder]="placeholder()"
        [disabled]="disabled()"
        [value]="value()"
        (input)="value.set($any($event.target).value)"
      />
      <fieldset class="tcn-input-outline" aria-hidden="true">
        @if (label()) {
          <legend class="tcn-input-notch">{{ label() }}</legend>
        }
      </fieldset>
    </label>
  `,
  host: { class: 'block' },
})
export class TcnInput {
  readonly value = model('');
  readonly label = input('');
  readonly placeholder = input('');
  readonly type = input('text');
  readonly disabled = input(false, { transform: booleanAttribute });
  readonly error = input(false, { transform: booleanAttribute });
}
Source · React
export * from './tcn-input';
import { forwardRef } from 'react';
import type { ChangeEvent, ComponentPropsWithoutRef } from 'react';
import { cn } from '@touchcn/core';

export interface TcnInputProps extends ComponentPropsWithoutRef<'input'> {
  /** Floating (MD) / inline (iOS) field label. */
  label?: string;
  /** Error state — tints the outline / border destructive. */
  error?: boolean;
  /** Wrapper class applied to the field host. */
  wrapperClassName?: string;
  /** Convenience callback with the raw string value. */
  onValueChange?(value: string): void;
}

export const TcnInput = forwardRef<HTMLInputElement, TcnInputProps>(function TcnInput(
  { label, error, className, wrapperClassName, onValueChange, onChange, ...props },
  ref,
) {
  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    onChange?.(event);
    onValueChange?.(event.target.value);
  };

  return (
    <div className={cn('block', wrapperClassName)}>
      <label
        className={cn('tcn-input relative flex items-center gap-3 px-3 py-2.5', label && 'tcn-input-labeled')}
        data-error={error || undefined}
      >
        {label && <span className="tcn-input-label">{label}</span>}
        <input
          ref={ref}
          className={cn('w-full bg-transparent text-[var(--color-on-surface)] outline-none', className)}
          onChange={handleChange}
          {...props}
        />
        <fieldset className="tcn-input-outline" aria-hidden="true">
          {label && <legend className="tcn-input-notch">{label}</legend>}
        </fieldset>
      </label>
    </div>
  );
});
Source · Vue
<script setup lang="ts">
import { cn } from '@touchcn/core';

defineOptions({ inheritAttrs: false });

const props = defineProps<{
  /** Floating (MD) / inline (iOS) field label. */
  label?: string;
  /** Error state — tints the outline / border destructive. */
  error?: boolean;
  /** Wrapper class applied to the field host. */
  wrapperClass?: string;
}>();

/** Two-way bound raw string value. */
const model = defineModel<string>();
</script>

<template>
  <div :class="cn('block', props.wrapperClass)">
    <label
      :class="cn('tcn-input relative flex items-center gap-3 px-3 py-2.5', label && 'tcn-input-labeled')"
      :data-error="error || undefined"
    >
      <span v-if="label" class="tcn-input-label">{{ label }}</span>
      <input
        v-model="model"
        class="w-full bg-transparent text-[var(--color-on-surface)] outline-none"
        v-bind="$attrs"
      />
      <fieldset class="tcn-input-outline" aria-hidden="true">
        <legend v-if="label" class="tcn-input-notch">{{ label }}</legend>
      </fieldset>
    </label>
  </div>
</template>
export { default as TcnInput } from './TcnInput.vue';

Last updated on July 24, 2026

Was this page helpful?