FewTone UI
ComponentsDocs

Getting started

  • Introduction
  • Installation
  • Theming

Components4

  • ButtonNew
  • InputNew
  • SelectNew
  • SwitchNew
© 2026 FewTone · MIT licenceGitHub
  1. Components
  2. Input

InputNew

pnpm dlx shadcn@latest add …/r/input.json

An accessible React text input with three sizes and clear invalid, disabled and file states, built on a native input.

import { Input } from "@/components/ui/input"

export function EmailField() {
  return (
    <>
      <label htmlFor="email">Email</label>
      <Input id="email" type="email" placeholder="you@example.com" />
    </>
  )
}
default.tsx
import { Input } from "@/components/ui/input";
import { useId } from "react";

export default function InputDefault() {
  const id = useId();
  return (
    <div className="grid w-full max-w-xs gap-2">
      <label htmlFor={id} className="text-sm font-medium">
        Email
      </label>
      <Input id={id} type="email" placeholder="you@example.com" autoComplete="email" />
    </div>
  );
}

Installation

  1. 1

    Install the dependencies.

    Package manager
    $ pnpm add class-variance-authority
  2. 2

    Add the FewTone UI tokens to your global CSS file. Skip any variable your project already defines.

    app/globals.css
    :root {
      --size-control-sm: 2rem;
      --size-control-md: 2.25rem;
      --size-control-lg: 2.5rem;
      --size-icon-sm: 0.875rem;
      --size-icon-md: 1rem;
      --size-icon-lg: 1.25rem;
      --duration-instant: 80ms;
      --duration-fast: 150ms;
      --duration-base: 220ms;
      --duration-slow: 350ms;
      --duration-slower: 500ms;
      --z-popover: 1400;
      --focus-ring-width: 2px;
      --focus-ring-offset: 2px;
      --opacity-disabled: 0.5;
      --scale-press: 0.97;
      --ease-spring: linear(0, 0.157, 0.48, 0.77, 0.97, 1.07, 1.1, 1.09, 1.05, 1.02, 1, 0.995, 1);
      --destructive-foreground: oklch(1 0 0);
    }
    
    .dark {
      --destructive-foreground: oklch(1 0 0);
    }
    
    @theme inline {
      --animate-popover-in: popover-in var(--duration-fast) cubic-bezier(0.22, 1, 0.36, 1) both;
      --animate-popover-out: popover-out var(--duration-instant) cubic-bezier(0.55, 0, 1, 0.45) both;
      --color-destructive-foreground: var(--destructive-foreground);
      @keyframes popover-in {
        from {
          opacity: 0;
          transform: scale(0.96);
        }
        to {
          opacity: 1;
          transform: scale(1);
        }
      }
      @keyframes popover-out {
        from {
          opacity: 1;
          transform: scale(1);
        }
        to {
          opacity: 0;
          transform: scale(0.96);
        }
      }
    }
    
    @media (prefers-reduced-motion: reduce) {
      :root {
        --duration-instant: 1ms;
        --duration-fast: 1ms;
        --duration-base: 1ms;
        --duration-slow: 1ms;
        --duration-slower: 1ms;
      }
    }
  3. 3

    Copy the source into your project.

    components/ui/input.tsx
    import { cn } from "@/lib/utils";
    import { cva, type VariantProps } from "class-variance-authority";
    import type { ComponentProps } from "react";
    
    const inputVariants = cva(
      [
        "flex w-full min-w-0 rounded-lg border border-input bg-transparent px-3 text-sm text-foreground shadow-xs",
        "transition duration-(--duration-fast) ease-out",
        "placeholder:text-muted-foreground",
        "outline-ring focus-visible:border-ring focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-offset-(--focus-ring-offset)",
        "aria-invalid:border-destructive aria-invalid:outline-destructive",
        "disabled:cursor-not-allowed disabled:opacity-(--opacity-disabled)",
        "file:me-3 file:h-full file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground",
      ],
      {
        variants: {
          size: {
            sm: "h-(--size-control-sm) rounded-md px-2.5",
            md: "h-(--size-control-md)",
            lg: "h-(--size-control-lg)",
          },
        },
        defaultVariants: { size: "md" },
      },
    );
    
    /** `size` is the height variant here, so the native numeric `size` attribute is left out */
    export type InputProps = Omit<ComponentProps<"input">, "size"> & VariantProps<typeof inputVariants>;
    
    /**
     * A text field. Pair it with a visible label (`<label htmlFor>`), and set `aria-invalid` with an
     * error message linked through `aria-describedby` when the value is wrong.
     */
    function Input({ className, size, type = "text", ...props }: InputProps) {
      return (
        <input
          data-slot="input"
          data-size={size ?? "md"}
          type={type}
          className={cn(inputVariants({ size }), className)}
          {...props}
        />
      );
    }
    
    export { Input, inputVariants };
  4. 4

    Update the import paths to match your project.

Examples

Invalid

Set aria-invalid and link the error message with aria-describedby, so screen readers announce it.

Use at least 3 characters.

invalid.tsx
import { Input } from "@/components/ui/input";
import { useId } from "react";

export default function InputInvalid() {
  const id = useId();
  return (
    <div className="grid w-full max-w-xs gap-2">
      <label htmlFor={id} className="text-sm font-medium">
        Username
      </label>
      <Input id={id} defaultValue="f" aria-invalid="true" aria-describedby={`${id}-error`} />
      <p id={`${id}-error`} className="text-sm text-destructive">
        Use at least 3 characters.
      </p>
    </div>
  );
}

Sizes

Small, medium and large, matching Button's heights.

sizes.tsx
import { Input } from "@/components/ui/input";

export default function InputSizes() {
  return (
    <div className="grid w-full max-w-xs gap-3">
      <Input size="sm" placeholder="Small" aria-label="Small input" />
      <Input placeholder="Medium" aria-label="Medium input" />
      <Input size="lg" placeholder="Large" aria-label="Large input" />
    </div>
  );
}

Disabled

disabled.tsx
import { Input } from "@/components/ui/input";
import { useId } from "react";

export default function InputDisabled() {
  const id = useId();
  return (
    <div className="grid w-full max-w-xs gap-2">
      <label htmlFor={id} className="text-sm font-medium">
        Workspace
      </label>
      <Input id={id} defaultValue="fewtone" disabled />
    </div>
  );
}

File

The file picker button takes the input's styles.

file.tsx
import { Input } from "@/components/ui/input";
import { useId } from "react";

export default function InputFile() {
  const id = useId();
  return (
    <div className="grid w-full max-w-xs gap-2">
      <label htmlFor={id} className="text-sm font-medium">
        Avatar
      </label>
      <Input id={id} type="file" accept="image/*" />
    </div>
  );
}

API reference

Also accepts every native input prop except the numeric size attribute. Exports inputVariants.

PropTypeDefaultDescription
size?"sm" | "md" | "lg""md"The height.
type?string"text"The native input type.
aria-invalid?boolean–Marks the value as wrong: the border and focus ring turn destructive.
disabled?booleanfalsePrevents editing.

Related components

  • SelectNew

    An accessible React select with a gliding highlight, groups, labels and a scrollable list, built on Radix Select.

  • SwitchNew

    An accessible React switch for on/off settings, with a spring-animated thumb, built on Radix Switch.

  • ButtonNew

    An accessible React button with six variants, three sizes, icon support and a spring press, built on a native button.

Last updated 13 Oct 2026Edit on GitHub