How to build a split-screen sign-in with an overlay card using Tailwind CSS and Alpine.js
Build a responsive split-screen sign-in layout with Tailwind CSS and Alpine.js, including an absolute overlay card on top of the hero image.
Published on November 18, 2025 by Michael Andreuzza · 10 min readHello everyone! In this tutorial we’ll build a split‑screen sign‑in section with Tailwind CSS and a small Alpine.js enhancement for password visibility. The layout places the form on the left and a full‑height image on the right. On top of that image, we render an elevated card using position: absolute.
We’ll cover:
- The split layout and responsive widths
- Form fields with left icons and polished focus states
- A password field with a show/hide toggle using Alpine.js
- The image panel and a floating absolute card on top
- Accessibility considerations (sr‑only, labels, aria attributes)
Prerequisites: Tailwind CSS v3+ (or v4) and Alpine.js v3 available on the page. Alpine is only used for the password toggle in this example.
1 — Split layout shell
Use a responsive flex container. On mobile, the form stacks; on large viewports (lg:) we show a two‑column split.
<!-- Page section wrapper -->
<section class="flex flex-col lg:flex-row min-h-screen">
<!-- Left (form) and Right (image) go here as siblings -->
</section>
The two children use w-full lg:w-1/2 and hidden lg:block lg:w-1/2 to split the space only on large screens.
2 — Left panel: logo, heading, intro
The left side centers content, constrains width, and provides vertical padding.
<div class="flex items-center justify-center w-full lg:w-1/2">
<div class="w-full max-w-md p-8 py-24 lg:py-32">
<a href="#_" class="inline-flex items-center" aria-label="Back to home">
<span class="sr-only">Back to home</span>
<!-- brand svg -->
<svg class="h-12 text-zinc-900" aria-hidden="true" viewBox="0 0 2895 2895" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- ...paths -->
</svg>
</a>
<h1 class="text-2xl md:text-3xl lg:text-4xl mt-12 font-semibold tracking-tight text-zinc-900">Sign in</h1>
<p class="text-base mt-4 text-zinc-500 text-balance">Welcome back. You know the drill, email, password, maybe a tear or two.</p>
</div>
</div>
3 — Form controls with icons
Inputs use a relative container so we can place an icon absolutely on the left via absolute inset-y-0 left-0 pl-3 with pointer-events-none.
<form class="mt-10 space-y-4">
<!-- Email -->
<div class="w-full">
<label for="email" class="block mb-1 text-sm font-medium text-zinc-500">Email Address</label>
<div class="relative z-0 focus-within:z-10">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<!-- email icon -->
</div>
<input
id="email"
name="email"
type="email"
inputmode="email"
placeholder="you@example.com"
class="w-full h-10 pl-10 px-4 py-2 text-sm rounded-md bg-white border border-transparent text-zinc-500 ring-1 ring-zinc-200 placeholder-zinc-400 focus:border-blue-500 focus:ring-blue-100 focus:ring-2 focus:outline-none shadow-sm"
/>
</div>
</div>
<!-- more fields ... -->
</form>
4 — Password field with Alpine.js toggle
We use a small x-data object to hold value and showPassword. The eye icon flips with x-show.
<div class="w-full">
<label for="password" class="block mb-1 text-sm font-medium text-zinc-500">Password</label>
<div class="relative z-0 focus-within:z-10" x-data="{ value: '', showPassword: false }">
<input
id="password"
name="password"
x-model="value"
:type="showPassword ? 'text' : 'password'"
placeholder="••••••••"
class="w-full h-10 px-4 py-2 text-sm rounded-md bg-white border border-transparent text-zinc-500 ring-1 ring-zinc-200 placeholder-zinc-400 focus:border-blue-500 focus:ring-blue-100 focus:ring-2 focus:outline-none shadow-sm"
aria-required="true"
/>
<div class="absolute inset-y-0 right-0 flex items-center pr-3 gap-2">
<button type="button" @click="showPassword = !showPassword" class="text-zinc-500" aria-label="Toggle password visibility">
<!-- eye on/eye off icons; use x-show to switch -->
</button>
<button x-show="value" type="button" @click="value = ''" class="text-zinc-400 hover:text-zinc-600" aria-label="Clear input">
<!-- clear (X) icon -->
</button>
</div>
</div>
<div class="mt-4 flex items-center">
<input type="checkbox" id="login-remember" class="rounded border-zinc-300 text-blue-600 focus:ring-blue-500 focus:ring-offset-2 focus:ring-2 shadow size-4" />
<label for="login-remember" class="ml-2 text-sm font-medium text-zinc-500">Remember me for 30 days</label>
</div>
</div>
5 — Primary and social actions
Standard filled button for email sign‑in and a bordered social button with an icon and a small notification dot via absolute.
<button class="w-full h-10 px-5 text-sm rounded-md font-medium text-white bg-zinc-900 outline outline-zinc-900 hover:bg-zinc-950 focus-visible:outline-2 focus-visible:outline-zinc-950">Sign in with email</button>
<button class="relative w-full h-10 px-5 text-sm rounded-md font-medium text-zinc-700 bg-white outline outline-zinc-200 hover:bg-zinc-50 hover:shadow-sm focus-visible:outline-2 focus-visible:outline-zinc-900 flex items-center gap-2.5">
<!-- google icon -->
Sign in with Google
<span class="absolute right-3 top-1/2 -translate-y-1/2 w-2 h-2 bg-green-500 rounded-full"></span>
<span class="sr-only">Continue with Google</span>
</button>
6 — Right panel: image
Make the right panel relative so the floating card can anchor to it. Hide it on small screens and reveal on lg:.
<div class="relative hidden lg:block lg:w-1/2">
<img
src="/path/to/hero.jpg"
alt=""
aria-hidden="true"
loading="lazy"
decoding="async"
class="w-full h-full object-cover object-left bg-zinc-50 grayscale"
/>
<!-- overlay card goes here -->
</div>
7 — The floating overlay card (absolute)
The card is positioned with absolute inside the relative container. We use bottom-20 left-10 right-10 and utility styles for the lifted look: a subtle gradient background, outline, rounded corners, and shadow.
<div class="absolute bottom-20 left-10 right-10 p-8 rounded-xl shadow bg-linear-180 from-zinc-50 to-zinc-100 outline outline-zinc-100">
<div class="flex flex-col gap-2">
<h3 class="text-base font-medium text-zinc-900">“Short testimonial or callout sits here.”</h3>
<div class="mt-2 flex items-center gap-4">
<img src="/path/to/avatar.jpg" alt="" aria-hidden="true" class="size-10 rounded-full object-cover object-top" />
<div>
<p class="text-base font-semibold text-zinc-900">Jane Doe</p>
<p class="text-xs text-zinc-500">Software Engineer</p>
</div>
</div>
</div>
</div>
8 — Accessibility and polish
- Use a visible text label or
aria-labelfor icon-only elements, andsr-onlytext when needed. - Always connect labels with inputs via
for/id. - Add
aria-hidden="true"for purely decorative images and icons. - Provide clear focus styles (
focus-visible:outline-2,focus:ring-*).
<a href="#_" aria-label="Back to home"><span class="sr-only">Back to home</span><!-- icon --></a>
<input id="email" aria-required="true" />
Full snippet
Paste this full example into a page with Tailwind CSS and Alpine.js loaded:
<div class="flex items-center justify-center w-full lg:w-1/2">
<div class="w-full max-w-md p-8 py-24 lg:py-32">
<div>
<a href="#_">
<span class="sr-only">Back to home</span>
<svg
class="h-12 text-zinc-900"
aria-hidden="true"
viewBox="0 0 2895 2895"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1936.72 1316.69V1677.28L1700.54 1540.97V1452.99L1537.73 1359.06L1461.65 1315.07V1042.46L1536.29 1085.55L1773.92 1222.76L1936.72 1316.69Z"
stroke="#17181B"
stroke-width="18.0294"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M988.017 1041.56V1315.79L913.191 1359.06L753.454 1451.19V1537.55L516.003 1674.75V1314.16L675.746 1221.85L911.752 1085.55L988.017 1041.56Z"
stroke="#17181B"
stroke-width="18.0294"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M2408.73 1044.08L2096.46 1224.38L1936.72 1316.69L1773.92 1222.75L1536.29 1085.55L1461.65 1042.46L1621.57 950.15L1696.21 907.06L1933.65 769.856L2408.73 1044.08Z"
fill="#17181B"
stroke="#17181B"
stroke-width="18.0294"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M1696.23 632.653L1383.96 812.947L1224.03 905.257L1061.23 811.324L748.96 631.03L1221.15 358.425L1696.23 632.653Z"
fill="#17181B"
stroke="#17181B"
stroke-width="18.0294"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M988.023 1041.56L911.761 1085.55L675.754 1221.85L516.012 1314.16L353.21 1220.05L40.9387 1039.76L512.947 767.333L748.954 903.635L825.218 947.627L988.023 1041.56Z"
fill="#17181B"
stroke="#17181B"
stroke-width="18.0294"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M2408.73 1044.08V1404.67L1936.72 1677.28V1316.69L2096.46 1224.38L2408.73 1044.08Z"
stroke="#17181B"
stroke-width="18.0294"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M1700.55 1452.99V1813.58L1228.54 2086.18V1725.59L1388.28 1633.28L1624.47 1496.98L1700.55 1452.99Z"
stroke="#17181B"
stroke-width="18.0294"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M1700.55 1452.99L1624.47 1496.98L1388.28 1633.28L1228.54 1725.59L1065.74 1631.48L828.288 1494.46L753.468 1451.19L913.207 1359.06L988.031 1315.79L1149.39 1222.75L1224.03 1179.66L1225.48 1178.76L1300.12 1221.85L1461.66 1315.07L1537.75 1359.06L1700.55 1452.99Z"
fill="#17181B"
stroke="#17181B"
stroke-width="18.0294"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M1228.54 1725.59V2086.18L753.468 1811.77V1451.19L828.288 1494.46L1065.74 1631.48L1228.54 1725.59Z"
stroke="#17181B"
stroke-width="18.0294"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M516.012 1314.16V1674.75L40.9387 1400.34V1039.76L353.21 1220.05L516.012 1314.16Z"
stroke="#17181B"
stroke-width="18.0294"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M1696.22 632.651V907.059L1621.58 950.149L1461.66 1042.46V1128.64L1300.12 1221.85L1225.47 1178.76L1224.03 1179.66V905.256L1383.95 812.945L1696.22 632.651Z"
stroke="#17181B"
stroke-width="18.0294"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M1224.03 905.257V1179.66L1149.39 1222.75L988.03 1129.54V1041.56L825.225 947.626L748.96 903.634V631.029L1061.23 811.323L1224.03 905.257Z"
stroke="#17181B"
stroke-width="18.0294"
stroke-linecap="round"
stroke-linejoin="round"
></path></svg
></a>
<h1
class="text-2xl md:text-3xl lg:text-4xl mt-12 font-semibold tracking-tight text-zinc-900"
>
Sign in
</h1>
<p class="text-base mt-4 text-zinc-500 text-balance">
Welcome back. You know the drill, email, password, maybe a tear or two.
</p>
</div>
<form class="mt-10 space-y-4">
<div class="w-full">
<div class="flex justify-between items-baseline mb-1">
<label for="email" class="font-medium text-zinc-500 text-sm"
>Email Address</label
>
</div>
<div class="relative z-0 focus-within:z-10">
<div
class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon icon-tabler-arrow-mail size-4 text-zinc-500"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10z"
></path>
<path d="M3 7l9 6l9 -6"></path>
</svg>
</div>
<input
placeholder="you@example.com"
id="email"
name="email"
type="email"
class="w-full block transition duration-300 ease-in-out leading-tight align-middle focus:z-10 pl-10 h-10 px-4 py-2 text-sm rounded-md bg-white border border-transparent text-zinc-500 ring-1 ring-zinc-200 placeholder-zinc-400 focus:border-blue-500 focus:ring-blue-100 focus:ring-2 focus:outline-none shadow-sm"
inputmode="email"
/>
</div>
</div>
<div class="w-full">
<div class="flex justify-between items-baseline mb-1">
<label for="password" class="font-medium text-zinc-500 text-sm"
>Password</label
>
</div>
<div
class="relative z-0 focus-within:z-10"
x-data="{ value: "", showPassword: false }"
>
<input
placeholder="••••••••"
required
id="password"
name="password"
x-model="value"
x-bind:type="showPassword ? 'text' : 'password'"
class="w-full block transition duration-300 ease-in-out leading-tight align-middle focus:z-10 h-10 px-4 py-2 text-sm rounded-md bg-white border border-transparent text-zinc-500 ring-1 ring-zinc-200 placeholder-zinc-400 focus:border-blue-500 focus:ring-blue-100 focus:ring-2 focus:outline-none shadow-sm"
aria-required="true"
/>
<div class="absolute inset-y-0 right-0 flex items-center pr-3 gap-2">
<button
type="button"
@click="showPassword = !showPassword"
class="text-zinc-500 focus:outline-none"
tabindex="-1"
aria-label="Toggle password visibility"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon icon-tabler-external-link size-4"
x-show="!showPassword"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path>
<path
d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6"
></path></svg
><svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon icon-tabler-external-link size-4"
x-show="showPassword"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828"></path>
<path
d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87"
></path>
<path d="M3 3l18 18"></path>
</svg></button
><button
x-show="value"
type="button"
@click="value = ''"
class="text-zinc-400 hover:text-zinc-600 focus:outline-none"
aria-label="Clear input"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon icon-tabler-wave-x size-4 text-zinc-500"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M18 6l-12 12"></path>
<path d="M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
</div>
<div class="flex items-center">
<input
type="checkbox"
id="login-remember"
name="login-remember"
class="rounded border-zinc-300 text-blue-600 focus:ring-blue-500 focus:ring-offset-2 focus:ring-2 shadow size-4"
/>
<label
for="login-remember"
class="ml-2 text-sm font-medium text-zinc-500"
>
Remember me for 30 days
</label>
</div>
<button
class="relative flex items-center justify-center text-center font-medium transition-colors duration-200 ease-in-out select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:z-10 justify-center rounded-md w-full text-white bg-zinc-900 outline outline-zinc-900 hover:bg-zinc-950 focus-visible:outline-zinc-950 h-10 px-5 text-sm"
>
Sign in with email
</button>
<button
class="relative flex items-center justify-center text-center font-medium transition-colors duration-200 ease-in-out select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:z-10 justify-center rounded-md text-zinc-700 bg-white outline outline-zinc-200 hover:shadow-sm hover:bg-zinc-50 focus-visible:outline-zinc-900 h-10 px-5 text-sm gap-2.5 relative flex items-center w-full"
>
<svg
class="size-4"
slot="left-icon"
viewBox="0 0 256 262"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
fill="#4285F4"
></path>
<path
d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
fill="#34A853"
></path>
<path
d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782"
fill="#FBBC05"
></path>
<path
d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
fill="#EB4335"
></path>
</svg>
Sign in with Google
<span
class="absolute w-2 h-2 bg-green-500 rounded-full right-3 top-1/2 -translate-y-1/2"
></span>
</button>
</form>
<p class="text-xs mt-4 font-medium text-zinc-500">
Dont' have an account?
<a href="#_" class="font-medium text-blue-500 hover:text-zinc-500">
Sign up
</a>
</p>
</div>
</div>
<div class="relative hidden lg:block lg:w-1/2">
<img
src="/.netlify/images?url=_astro%2F1.D7dn6lqI.jpeg&w=1200&h=1200"
alt="#_"
aria-hidden="true"
loading="lazy"
decoding="async"
fetchpriority="auto"
width="1200"
height="1200"
class="object-cover object-left w-full h-full bg-zinc-50 grayscale"
/>
<div
class="absolute p-8 shadow bg-linear-180 outline outline-zinc-100 from-zinc-50 to-zinc-100 rounded-xl bottom-20 left-10 right-10"
>
<div class="relative flex flex-col gap-2">
<h3 class="text-base font-medium text-zinc-900">
"I’ve seen enough UI kits to last a lifetime. Oxbow’s the first one that
didn’t make me roll my eyes."
</h3>
<div class="flex items-center mt-2 gap-4">
<img
src="/.netlify/images?url=_astro%2Favatar1.B8NE3-c_.jpeg&w=1000&h=1000"
alt="#_"
aria-hidden="true"
loading="lazy"
decoding="async"
fetchpriority="auto"
width="1000"
height="1000"
class="object-cover object-top rounded-full size-10"
/>
<div>
<p class="text-base font-semibold text-zinc-900">Johan Snällström</p>
<p class="text-xs text-zinc-500">Software Engineer</p>
</div>
</div>
</div>
</div>
</div>
Wrap‑up
You now have a clean split‑screen sign‑in built with Tailwind CSS and a tiny Alpine.js enhancement. The right side’s floating card is simply an absolutely positioned element anchored to a relative parent, which makes it easy to place testimonials, badges, or promotional content over imagery.
/Michael Andreuzza