Default

Standard text input with label.

<label class="label" for="email">Email</label>
<input class="input" type="email" id="email"
       placeholder="you@example.com">

With description

Use .field-description below the input for help text.

Choose a unique username for your account.

<label class="label" for="username">Username</label>
<input class="input" type="text" id="username"
       placeholder="codylindley"
       aria-describedby="username-desc">
<p class="field-description" id="username-desc">
  Choose a unique username for your account.
</p>

Sizes

Small, default, and large via data-size.

<input class="input" data-size="sm" type="text" placeholder="Small">
<input class="input" type="text" placeholder="Default">
<input class="input" data-size="lg" type="text" placeholder="Large">

Disabled

Non-interactive at 50% opacity.

This field is currently disabled.

<input class="input" type="text" disabled placeholder="Disabled">

Invalid

Use aria-invalid="true" with a .field-error message.

Please enter a valid email address.

<label class="label" for="email">Email</label>
<input class="input" type="email" id="email"
       aria-invalid="true" value="not-an-email"
       aria-describedby="email-err">
<p class="field-error" id="email-err">
  Please enter a valid email address.
</p>

Required

Add required attribute and a visual indicator in the label.

This field is required.

<label class="label" for="name">
  Name <span class="text-destructive">*</span>
</label>
<input class="input" type="text" id="name"
       required placeholder="Jane Doe">

With icon

Wrap in a relative container and position the Lucide icon absolutely.

<div style="position:relative;">
  <i data-lucide="search"
     class="text-muted-foreground"
     style="position:absolute;left:0.75rem;top:50%;transform:translateY(-50%);width:1rem;height:1rem;"></i>
  <input class="input" style="padding-left:2.25rem;" type="search"
         placeholder="Search...">
</div>

File

Use type="file" — the file selector button is styled automatically.

Select a picture to upload.

<label class="label" for="avatar">Picture</label>
<input class="input" type="file" id="avatar">
<p class="field-description">Select a picture to upload.</p>

Textarea

Use <textarea> with the .input class. Auto-grows with content via field-sizing: content — zero JS.

<label class="label" for="message">Message</label>
<textarea class="input" id="message"
          placeholder="Your message..."></textarea>

Readonly

Non-editable value the user can still select and copy.

Your API key is read-only.

<label class="label" for="api-key">API Key</label>
<input class="input" type="text" id="api-key"
       readonly value="sk-1234567890abcdef">

Password

Use type="password" for secure text entry.

<label class="label" for="password">Password</label>
<input class="input" type="password" id="password"
       placeholder="Enter your password">

With button

Pair an input with a button for inline actions like search.

<div style="display:flex;gap:0.5rem;">
  <input class="input" type="search"
         placeholder="Search...">
  <button class="btn" data-variant="default">Search</button>
</div>

Form composition

Multiple inputs with labels, descriptions, and a submit button.

We'll never share your email with anyone.

<form>
  <div>
    <label class="label" for="name">
      Name <span class="text-destructive">*</span>
    </label>
    <input class="input" type="text" id="name"
           required placeholder="Jane Doe">
  </div>
  <div>
    <label class="label" for="email">
      Email <span class="text-destructive">*</span>
    </label>
    <input class="input" type="email" id="email"
           required placeholder="jane@example.com">
    <p class="field-description">
      We'll never share your email with anyone.
    </p>
  </div>
  <div>
    <label class="label" for="bio">Bio</label>
    <textarea class="input" id="bio"
              placeholder="Tell us about yourself..."></textarea>
  </div>
  <button class="btn" data-variant="default" type="submit">Submit</button>
</form>

CSS view file

/* -- Input component ------------------------------------------- */

@layer components {
  .input {
    height: 2.5rem;
    width: 100%;
    border: 1px solid var(--input);
    border-radius: var(--radius-md);
    background: var(--background);
    padding: 0 0.75rem;
    font-size: 0.875rem;
    font-family: var(--font-sans);
    color: var(--foreground);
    outline: none;
    box-shadow: var(--shadow-xs);
    transition: border-color 150ms, box-shadow 150ms;

    &:focus {
      border-color: var(--ring);
      box-shadow: 0 0 0 2px oklch(from var(--ring) l c h / 0.2);
    }

    &::placeholder { color: var(--muted-foreground); }

    &:disabled {
      opacity: 0.5;
      cursor: not-allowed;
    }

    /* -- Readonly state -------------------------------------- */
    &:read-only:not([type="file"]) {
      background: var(--muted);
      cursor: default;
      opacity: 0.7;

      &:focus {
        border-color: var(--input);
        box-shadow: var(--shadow-xs);
      }
    }

    /* -- Invalid state --------------------------------------- */
    &:is([aria-invalid="true"], :user-invalid) {
      border-color: var(--destructive);

      &:focus {
        border-color: var(--destructive);
        box-shadow: 0 0 0 2px oklch(from var(--destructive) l c h / 0.2);
      }
    }

    /* -- File input ------------------------------------------ */
    &[type="file"] {
      padding: 0;
      font-size: 0.875rem;

      &::file-selector-button {
        height: 100%;
        border: none;
        border-right: 1px solid var(--input);
        background: var(--muted);
        color: var(--foreground);
        font-size: 0.875rem;
        font-weight: 500;
        font-family: var(--font-sans);
        padding: 0 0.75rem;
        margin-right: 0.75rem;
        cursor: pointer;
        transition: background 150ms;

        &:hover { background: var(--accent); }
      }
    }

    /* -- Sizes ----------------------------------------------- */
    &[data-size="sm"] { height: 2rem;    padding: 0 0.625rem; font-size: 0.8125rem; }
    &[data-size="lg"] { height: 2.75rem; padding: 0 1rem;     font-size: 1rem; }
  }

  /* -- Auto-growing textarea --------------------------------- */
  textarea.input {
    field-sizing: content;
    min-height: 5rem;
    padding-top: 0.5rem;
    padding-bottom: 0.5rem;
  }

  /* -- Field description ------------------------------------- */
  .field-description {
    font-size: 0.8125rem;
    color: var(--muted-foreground);
    margin: 0.375rem 0 0;
  }

  /* -- Field error ------------------------------------------- */
  .field-error {
    font-size: 0.8125rem;
    color: var(--destructive);
    margin: 0.375rem 0 0;
  }

  /* -- Accessibility ---------------------------------------- */
  @media (prefers-reduced-motion: reduce) {
    .input {
      transition: none;
    }

    .input[type="file"]::file-selector-button {
      transition: none;
    }
  }

  @media (prefers-contrast: more) {
    .input {
      border-width: 2px;
    }

    .input:disabled {
      opacity: 0.7;
    }
  }

  @media (forced-colors: active) {
    .input {
      border-color: ButtonBorder;
      color: ButtonText;
      background: Field;
    }

    .input:focus {
      outline: 2px solid Highlight;
      outline-offset: 1px;
    }

    .input:disabled {
      opacity: 1;
      color: GrayText;
      border-color: GrayText;
    }

    .input:is([aria-invalid="true"], :user-invalid) {
      border-color: LinkText;
    }

    .field-error {
      color: LinkText;
    }
  }
}