Login Form
Free · MITEmail/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
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
---
// 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.