Skip to content
oks-ui

Auth

Sign-in screen

A premium two-column auth screen: an editorial brand panel with a feature highlight card on one side, a complete email/password form with Google SSO, remember-me, and forgot-password on the other. FormFieldSet handles the field types and validation; Form collects the values on submit — no form library.

sign-in.tsx

How it's built

1

The layout uses a container query (`@container` on the outer wrapper, `@2xl:grid-cols-2`) so it stays a genuine split-screen even scaled down inside a card, and gracefully collapses to a single column below that width — the brand panel hides entirely on narrow screens, leaving just the form.

2

The outer wrapper carries no border, radius, or shadow of its own — when this pattern is shown inside a mock-browser frame (like this preview), that frame IS the container; a second rounded card floating inside it would just be a frame-in-a-frame. Drop the wrapper's own chrome when embedding this in a real page too, unless you want a literal floating card.

3

`@2xl:min-h-[640px]` gives the split real presence instead of a short strip — applied only at the breakpoint where the two columns actually appear, so the mobile single-column view isn't forced to an arbitrary height it doesn't need.

4

`whitespace-nowrap` on "Forgot password" stops it wrapping to two lines next to the checkbox at in-between widths — small fixed-width text sitting beside a flexible one is exactly where accidental wraps happen.

5

`validation={{ rules: { required: true, email: true } }}` wires up inline errors and `aria-invalid` — nothing else to do. `type="password"` gets a reveal toggle for free.

6

Standalone `Checkbox` (imported from `oks-ui/form-field-set`, not part of a `FormFieldSet` group) takes a `label` prop — passing children instead spreads them onto the underlying `<input>` and breaks it.

7

The brand panel's feature card fills what would otherwise be dead vertical space between the logo and the headline — an empty gradient reads as unfinished, not premium.

8

Each feature-list icon is a real `Avatar`, and "Welcome back" is a real `PageTitle` — not raw spans/headings. The feature icons sit on the dark brand panel, so they're tinted through the `classNames` slot instead of `color`, since that panel is outside oks-ui's `data-theme`-scoped theme system; "Welcome back" sits on the genuinely light form panel, so `PageTitle` needs no such workaround.

9

`PageTitle`'s own `.oksPageTitle` wrapper is `display: inline-flex` with no set direction — built for a compact icon+title(+short subtitle) sitting in one row, not the classic stacked "big heading, paragraph below" layout this pattern (and most page headers) actually want. Pass `classNames={{ base: "flex-col items-start" }}` to stack the title above the subtitle instead of beside it.

10

The "Sign in with Google" button is a real oks-ui `Button` with `startContent` — the multi-colour G mark is just an inline SVG, the same as any other icon slot.

11

"Forgot password", "Sign up", and the dark panel's headline are real oks-ui components too — not raw `<a>`/`<h1>` tags. `Button` is polymorphic: pass `href` (with no `type`) and it renders as an `<a>` automatically, so `variant="link"` plus a `no-underline` override gives a proper accessible, keyboard-focusable text link instead of hand-rolled anchor styling.

Source

One file · copy & paste
sign-in.tsx
import { Avatar } from "oks-ui/avatar";
import { Button } from "oks-ui/button";
import { Checkbox, Form, FormFieldSet } from "oks-ui/form-field-set";
import { PageTitle } from "oks-ui/page-title";
import { CheckIcon, ShieldIcon, ZapIcon } from "../../showcase/icons";

// A small, standard multi-colour "G" mark for the SSO button — not
// reproducing any photo or illustration, just the ubiquitous service icon
// every "Sign in with Google" button uses.
function GoogleIcon() {
  return (
    <svg width="18" height="18" viewBox="0 0 18 18" aria-hidden="true">
      <path
        fill="#4285F4"
        d="M17.64 9.2c0-.64-.06-1.25-.16-1.84H9v3.48h4.84c-.21 1.13-.85 2.09-1.81 2.73v2.26h2.92c1.71-1.57 2.69-3.88 2.69-6.63z"
      />
      <path
        fill="#34A853"
        d="M9 18c2.43 0 4.47-.8 5.96-2.18l-2.92-2.26c-.81.54-1.84.87-3.04.87-2.34 0-4.32-1.58-5.03-3.71H.96v2.33C2.44 15.98 5.48 18 9 18z"
      />
      <path
        fill="#FBBC05"
        d="M3.97 10.72c-.18-.54-.28-1.12-.28-1.72s.1-1.18.28-1.72V4.95H.96A8.99 8.99 0 0 0 0 9c0 1.45.35 2.83.96 4.05l3.01-2.33z"
      />
      <path
        fill="#EA4335"
        d="M9 3.58c1.32 0 2.51.45 3.44 1.35l2.59-2.59C13.46.89 11.43 0 9 0 5.48 0 2.44 2.02.96 4.95l3.01 2.33C4.68 5.16 6.66 3.58 9 3.58z"
      />
    </svg>
  );
}

const features = [
  { icon: <ZapIcon size={16} />, text: "Real-time sync across every device" },
  { icon: <ShieldIcon size={16} />, text: "Enterprise-grade encryption by default" },
  { icon: <CheckIcon size={16} />, text: "Pick up your last session instantly" },
];

export function SignIn() {
  return (
    <div className="@container w-full bg-paper-raised">
      <div className="grid @2xl:min-h-[640px] @2xl:grid-cols-2">
        {/* visual panel — original abstract art, not a stock photo */}
        <div className="relative hidden flex-col overflow-hidden bg-[#04151a] p-9 @2xl:flex">
          <div
            aria-hidden="true"
            className="absolute inset-0"
            style={{
              background:
                "radial-gradient(120% 90% at 15% 10%, color-mix(in oklab, #0f766e 55%, transparent) 0%, transparent 55%), " +
                "radial-gradient(90% 70% at 90% 85%, color-mix(in oklab, #0891b2 45%, transparent) 0%, transparent 60%), " +
                "linear-gradient(160deg, #04151a 0%, #04262b 55%, #062f2c 100%)",
            }}
          />
          <div
            aria-hidden="true"
            className="absolute -left-24 top-1/3 h-72 w-72 rounded-full opacity-40 blur-3xl"
            style={{ background: "#14b8a6" }}
          />
          <div
            aria-hidden="true"
            className="absolute -right-16 bottom-0 h-64 w-64 rounded-full opacity-30 blur-3xl"
            style={{ background: "#22d3ee" }}
          />

          <div className="relative z-10 flex h-full flex-col justify-between gap-10">
            <span className="text-sm font-bold uppercase tracking-[0.2em] text-white">
              Brand
            </span>

            {/* glass card filling the middle of the panel */}
            <div className="rounded-[var(--r-lg)] border border-white/15 bg-white/[0.06] p-5 backdrop-blur-sm">
              <p className="text-[0.7rem] font-semibold uppercase tracking-[0.14em] text-white/50">
                Why teams switch
              </p>
              <ul className="mt-4 flex flex-col gap-3.5">
                {features.map((f) => (
                  <li key={f.text} className="flex items-center gap-3 text-white">
                    <Avatar
                      icon={f.icon as never}
                      radius="full"
                      size="sm"
                      classNames={{ base: "bg-white/10", icon: "text-[#5eead4]" }}
                    />
                    <span className="text-[0.9rem] leading-snug">{f.text}</span>
                  </li>
                ))}
              </ul>
            </div>

            <PageTitle
              as="h1"
              title="Pick up right where you left off."
              subtitle="Sign in to sync your projects, resume your last session, and keep every device in step — everything's exactly where you left it."
              classNames={{
                base: "flex-col items-start gap-4",
                title: "text-[2rem] font-bold leading-[1.1] tracking-tight text-white",
                subtitle: "mt-0 max-w-sm text-sm leading-relaxed text-white/70",
              }}
            />
          </div>
        </div>

        {/* form panel — fills its half edge to edge; the content inside is
            capped to a comfortable reading width and centred, so fields
            don't stretch absurdly wide on a large screen. */}
        <div className="flex flex-col justify-center p-8 @2xl:p-12">
          <div className="mx-auto flex w-full max-w-md flex-col gap-6">
            <PageTitle
              as="h2"
              title="Welcome back"
              subtitle="Welcome back! Please enter your details."
              classNames={{
                base: "flex-col items-start",
                title: "text-[1.6rem] font-bold tracking-tight text-ink",
                subtitle: "mt-1.5 text-[0.9rem] text-ink-muted",
              }}
            />

            <Form onSubmit={(data) => console.log(data)} className="flex flex-col gap-4">
              <FormFieldSet
                type="email"
                name="email"
                label="Email"
                placeholder="Enter your email"
                validation={{ rules: { required: true, email: true } }}
              />
              <FormFieldSet
                type="password"
                name="password"
                label="Password"
                placeholder="Enter your password"
                validation={{ rules: { required: true, minLength: 8 } }}
              />

              <div className="flex items-center justify-between gap-3">
                <Checkbox name="remember" label="Remember me" />
                <Button
                  href="#forgot-password"
                  onClick={(e) => e.preventDefault()}
                  variant="link"
                  size="sm"
                  className="h-auto shrink-0 whitespace-nowrap p-0 text-[0.85rem] font-medium text-accent no-underline"
                >
                  Forgot password
                </Button>
              </div>

              <Button type="submit" color="primary" fullWidth>
                Sign in
              </Button>
              <Button variant="bordered" fullWidth startContent={<GoogleIcon />}>
                Sign in with Google
              </Button>
            </Form>

            <p className="text-center text-[0.85rem] text-ink-muted">
              Don&rsquo;t have an account?{" "}
              <Button
                href="#sign-up"
                onClick={(e) => e.preventDefault()}
                variant="link"
                size="sm"
                className="h-auto p-0 align-baseline text-[0.85rem] font-semibold text-accent no-underline"
              >
                Sign up
              </Button>
            </p>
          </div>
        </div>
      </div>
    </div>
  );
}

More patterns

Auth
Sign-up screenA dark canvas with a feature pitch, SSO row, and a real registration form.
Auth
Forgot passwordA single email field beside an original customer-quote panel.
Auth
OTP verificationFour large circular code boxes with a live resend countdown.