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 read

Hello 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-label for icon-only elements, and sr-only text 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: &#34;&#34;, 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&#38;w=1200&#38;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&#38;w=1000&#38;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