Skip to content
Velocity X Docs
← All components

Login Form

Free · MIT

Email/password sign-in card with a show/hide password toggle driven by a scoped inline script.

Category
Forms
Framework
astro
Version
1.0.0
Peer deps
none

Install

bash
velocityx add login-form

Free and MIT licensed — no account needed. The file lands under src/components/velocity/login-form/ by default (override with --dest), and it's yours to edit from the moment it's written.

Demo

View the live demo on the showcase site ↗

Source

Pulled live from the registry — this is the exact file velocityx add login-form writes into your project right now.

components/velocity/login-form/LoginForm.astro

astro
---
//    VELOCITY COMPONENT · theming
// 
//    Colours here are Tailwind `primary-*` / `accent-*`, which resolve through
//    the Velocity preset. Out of the box they are a neutral indigo — NOT the
//    Aidxn brand — so this drops into your project without dragging our look in.
// 
//    ⚡ NOT USING VELOCITY? Run this once, in your project root:
// 
//          velocityx theme
// 
//    It writes velocity-preset.cjs + velocity-theme.css. Add the preset to your
//    tailwind.config, import the css, then set your whole brand in ONE place:
// 
//          :root {
//            --vx-brand: custom;
//            --vx-primary-600: 220 38 38;   <- your action colour
//            --vx-primary-700: 185 28 28;
//          }
// 
//    ⚠️  Raw RGB channels, space separated — NOT hex. `#DC2626` here silently
//        breaks every /opacity variant in the library (bg-primary-600/20 etc).
// 
//    Prefer not to install anything? Find-and-replace `primary-` with your own
//    scale name. Nothing else in this file depends on Velocity.
/**
 * LoginForm — centred email/password sign-in card with a show/hide password
 * toggle (vanilla inline script, no framework). Presentational only — wire
 * `action` to your auth endpoint.
 */
export interface Props {
  action?: string;
  method?: string;
  heading?: string;
  subheading?: string;
  submitLabel?: string;
  forgotHref?: string;
  signupHref?: string;
  className?: string;
}

const {
  action = '#',
  method = 'POST',
  heading = 'Welcome back',
  subheading = 'Sign in to pick up where you left off.',
  submitLabel = 'Sign in',
  forgotHref = '#',
  signupHref = '#',
  className = '',
} = Astro.props;

const inputClass =
  'w-full rounded-xl border-2 border-primary-200 dark:border-primary-800 bg-white dark:bg-primary-950/60 px-4 py-3 text-sm sm:text-base font-medium text-primary-950 dark:text-white placeholder:text-primary-950/40 dark:placeholder:text-primary-300/40 outline-none transition focus:border-primary-500 focus:ring-4 focus:ring-primary-500/15';

const labelClass = 'block text-sm font-semibold text-primary-950 dark:text-white mb-1.5';
---

<div class:list={['w-full max-w-sm rounded-3xl border border-primary-500/15 bg-white/95 dark:bg-primary-950/60 backdrop-blur-sm p-6 sm:p-8 shadow-[0_8px_50px_rgba(0,0,0,0.08)]', className]}>
  <h2 class="h-sub font-bold text-primary-950 dark:text-white">{heading}</h2>
  <p class="mt-1.5 body-md text-primary-950/60 dark:text-primary-200/60">{subheading}</p>

  <form action={action} method={method} class="mt-6 flex flex-col gap-4">
    <div>
      <label for="lf-email" class={labelClass}>Email</label>
      <input id="lf-email" name="email" type="email" autocomplete="email" required class={inputClass} placeholder="you@company.com" />
    </div>

    <div>
      <div class="flex items-center justify-between mb-1.5">
        <label for="lf-password" class="block text-sm font-semibold text-primary-950 dark:text-white">Password</label>
        <a href={forgotHref} class="text-xs font-semibold text-primary-600 dark:text-primary-400 hover:underline">Forgot password?</a>
      </div>
      <div class="relative">
        <input id="lf-password" name="password" type="password" autocomplete="current-password" required class={`${inputClass} pr-11`} placeholder="••••••••" />
        <button
          type="button"
          data-pw-toggle="lf-password"
          aria-label="Show password"
          aria-pressed="false"
          class="absolute right-2 top-1/2 -translate-y-1/2 inline-flex h-8 w-8 items-center justify-center rounded-lg text-primary-500 hover:bg-primary-500/10 transition"
        >
          <svg data-pw-icon-show class="h-4.5 w-4.5" width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
            <path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
            <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
          </svg>
          <svg data-pw-icon-hide class="hidden h-4.5 w-4.5" width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
            <path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12c1.292 4.338 5.31 7.5 10.066 7.5.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.52 10.52 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.243L9.88 9.88" />
          </svg>
        </button>
      </div>
    </div>

    <label class="flex items-center gap-2.5 text-sm text-primary-950/70 dark:text-primary-200/70 select-none">
      <input type="checkbox" name="remember" class="h-4 w-4 rounded border-2 border-primary-300 dark:border-primary-700 text-primary-600 focus:ring-primary-500/30 focus:ring-4" />
      Remember me
    </label>

    <button type="submit" class="btn btn-primary btn-block mt-1">{submitLabel}</button>
  </form>

  <p class="mt-6 text-center body-sm text-primary-950/55 dark:text-primary-200/55">
    Don't have an account? <a href={signupHref} class="font-semibold text-primary-600 dark:text-primary-400 hover:underline">Sign up</a>
  </p>
</div>

<script>
  // Password show/hide — scoped to this component instance, no framework.
  document.querySelectorAll<HTMLButtonElement>('[data-pw-toggle]').forEach((btn) => {
    if (btn.dataset.wired) return;
    btn.dataset.wired = '1';
    btn.addEventListener('click', () => {
      const id = btn.getAttribute('data-pw-toggle');
      const input = id ? document.getElementById(id) as HTMLInputElement | null : null;
      if (!input) return;
      const show = input.type === 'password';
      input.type = show ? 'text' : 'password';
      btn.setAttribute('aria-pressed', String(show));
      btn.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
      btn.querySelector('[data-pw-icon-show]')?.classList.toggle('hidden', show);
      btn.querySelector('[data-pw-icon-hide]')?.classList.toggle('hidden', !show);
    });
  });
</script>

Theming

Uses the shared primary-* / accent-* scale — see Theming if colours look unstyled after install.