Skip to content
oks-ui

Auth

Sign-up screen

A premium registration screen: an original dark, textured canvas carries the pitch and feature list as plain text, while a real form floats on top in its own light card — Google/GitHub/SSO buttons, name/username/email/password with a helper hint, and Form validating the whole set on submit.

sign-up.tsx

How it's built

1

oks-ui's dark theme only activates via `data-theme="dark"` on the document root (`:root[data-theme="dark"]` in its tokens CSS) — not on an arbitrary nested `<div>`. Real FormFieldSet/Button components dropped directly onto a custom dark background with no theme flip render dark-on-dark, illegible text.

2

The fix here: the dark canvas carries only plain text we colour ourselves (the pitch, the feature list); the actual form lives in a normal light `bg-paper-raised` card floating on top, so every oks-ui component renders with the contrast it was built for — no fighting the theme system.

3

The brand mark and the three feature icons are still real `Avatar` components even on the dark canvas — they're tinted via the `classNames` slot override (`{ base: "bg-white/10", icon: "text-white" }`) instead of the `color` prop, since `color` resolves through the same `:root[data-theme]`-scoped tokens that don't apply here.

4

The headline is a real `PageTitle` too — `classNames={{ base: "flex-col items-start" }}` because `PageTitle`'s own wrapper is `display: inline-flex` with no set direction, built for a compact inline icon+title rather than a stacked heading.

5

`description="Minimum length is 8 characters."` on the password FormFieldSet renders as real helper text under the field, not a separate paragraph you have to position yourself.

6

The three SSO buttons are `Button variant="bordered"` in a `grid-cols-3` row — Google's mark is the real multi-colour icon; the other two are simple, original single-colour marks, not reproductions of a specific brand's artwork.

7

"Terms of Service" and "Log in" are real `Button variant="link"` components (with `href`, no `type`, so it renders as an `<a>`), not raw anchors — `no-underline` overrides the variant's default underline to match this pattern's plain-text link style.

8

Layout stacks by default and only moves to a side-by-side canvas + card at `@2xl` — on a narrow screen the pitch sits above the form instead of disappearing, since a first-time visitor benefits from the context more than a returning one does.

Source

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

// Standard, single-colour SSO service marks — the same idiom every
// "continue with X" button on the web uses, not reproducing any artwork.
function GoogleIcon() {
  return (
    <svg width="16" height="16" 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>
  );
}

function GitHubIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
      <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z" />
    </svg>
  );
}

// A simplified, original geometric mark (not a reproduction of any specific
// brand's logo artwork) standing in for a third SSO provider slot.
function ThirdPartyIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
      <path d="M8 1.2 14.4 6l-2.4 7.3H4L1.6 6 8 1.2Z" opacity="0.85" />
    </svg>
  );
}

const features = [
  {
    icon: <UsersIcon size={18} />,
    title: "Invite your whole team",
    text: "No per-seat pricing games — bring everyone in from day one.",
  },
  {
    icon: <PieChartIcon size={18} />,
    title: "Real-time usage insights",
    text: "See exactly what's being used, by whom, as it happens.",
  },
  {
    icon: <ShieldIcon size={18} />,
    title: "Enterprise-grade security",
    text: "SSO, audit logs, and encryption at rest — on every plan.",
  },
];

export function SignUp() {
  return (
    <div className="@container relative w-full overflow-hidden bg-[#04140a]">
      {/* original dark canvas — a grid + warm glow, not a stock photo */}
      <div
        aria-hidden="true"
        className="absolute inset-0"
        style={{
          backgroundImage:
            "linear-gradient(rgba(255,255,255,0.05) 1px, transparent 1px), " +
            "linear-gradient(90deg, rgba(255,255,255,0.05) 1px, transparent 1px)",
          backgroundSize: "40px 40px",
          maskImage: "radial-gradient(120% 100% at 50% 0%, black 30%, transparent 90%)",
        }}
      />
      <div
        aria-hidden="true"
        className="absolute inset-0"
        style={{
          background:
            "radial-gradient(70% 60% at 15% 15%, color-mix(in oklab, #fe6601 35%, transparent) 0%, transparent 60%), " +
            "radial-gradient(60% 50% at 100% 100%, color-mix(in oklab, #b44801 30%, transparent) 0%, transparent 65%)",
        }}
      />

      <div className="relative z-10 flex flex-col gap-10 p-8 @2xl:min-h-[680px] @2xl:flex-row @2xl:items-center @2xl:justify-between @2xl:gap-14 @2xl:p-14">
        {/* brand + feature list — plain text on the dark canvas, colour fully
            under our own control */}
        <div className="flex flex-col gap-8 @2xl:max-w-sm">
          <div className="flex items-center gap-2">
            <Avatar
              name="Brand"
              radius="sm"
              size="sm"
              classNames={{ base: "bg-white/10", name: "text-white" }}
            />
            <span className="text-sm font-bold uppercase tracking-[0.2em] text-white">
              Brand
            </span>
          </div>

          <div className="flex flex-col gap-2">
            <PageTitle
              as="h1"
              title="Start your 30-day free trial."
              classNames={{
                base: "flex-col items-start",
                title: "text-[2rem] font-bold leading-[1.1] tracking-tight text-white",
              }}
            />
            <p className="flex items-center gap-1.5 text-sm text-white/60">
              <CheckIcon size={14} /> No credit card required
            </p>
          </div>

          <ul className="flex flex-col gap-6">
            {features.map((f) => (
              <li key={f.title} className="flex gap-3.5">
                <Avatar
                  icon={f.icon as never}
                  radius="full"
                  size="sm"
                  classNames={{ base: "mt-0.5 shrink-0 bg-white/10", icon: "text-white" }}
                />
                <div>
                  <p className="font-semibold text-white">{f.title}</p>
                  <p className="mt-0.5 text-[0.85rem] leading-relaxed text-white/60">
                    {f.text}
                  </p>
                </div>
              </li>
            ))}
          </ul>
        </div>

        {/* the actual form — a real, light card floating on the canvas, so
            every oks-ui field/label/button renders with the contrast it was
            designed for (oks-ui's dark theme only activates via
            data-theme="dark" on the document root, not on a nested div —
            putting these components directly on a dark background with no
            theme flip would render dark-on-dark, illegible text). */}
        <div className="w-full rounded-[var(--r-xl)] border border-line bg-paper-raised p-6 shadow-[var(--shadow-lg)] @2xl:max-w-md @2xl:p-8">
          <p className="text-center text-[0.85rem] font-medium text-ink-muted">
            Register with
          </p>
          <div className="mt-3 grid grid-cols-3 gap-2">
            <Button variant="bordered" size="sm" startContent={<GoogleIcon />}>
              Google
            </Button>
            <Button variant="bordered" size="sm" startContent={<GitHubIcon />}>
              GitHub
            </Button>
            <Button variant="bordered" size="sm" startContent={<ThirdPartyIcon />}>
              SSO
            </Button>
          </div>

          <div className="my-5 flex items-center gap-3">
            <span className="h-px flex-1 bg-line" aria-hidden="true" />
            <span className="text-[0.72rem] font-medium uppercase tracking-wide text-ink-faint">
              Or
            </span>
            <span className="h-px flex-1 bg-line" aria-hidden="true" />
          </div>

          <Form onSubmit={(data) => console.log(data)} className="flex flex-col gap-4">
            <div className="grid grid-cols-2 gap-3">
              <FormFieldSet
                type="text"
                name="firstName"
                label="First name"
                placeholder="Jane"
                validation={{ rules: { required: true } }}
              />
              <FormFieldSet
                type="text"
                name="lastName"
                label="Last name"
                placeholder="Doe"
                validation={{ rules: { required: true } }}
              />
            </div>
            <FormFieldSet
              type="text"
              name="username"
              label="Username"
              placeholder="janedoe"
              validation={{ rules: { required: true } }}
            />
            <FormFieldSet
              type="email"
              name="email"
              label="Email"
              placeholder="you@example.com"
              validation={{ rules: { required: true, email: true } }}
            />
            <FormFieldSet
              type="password"
              name="password"
              label="Password"
              placeholder="Create a password"
              description="Minimum length is 8 characters."
              validation={{ rules: { required: true, minLength: 8 } }}
            />
            <Button type="submit" color="primary" fullWidth>
              Sign up
            </Button>
          </Form>

          <p className="mt-4 text-center text-[0.75rem] leading-relaxed text-ink-muted">
            By creating an account, you agree to the{" "}
            <Button
              href="/terms"
              variant="link"
              size="sm"
              className="h-auto p-0 align-baseline text-[0.75rem] font-medium text-accent no-underline"
            >
              Terms of Service
            </Button>
            . We&rsquo;ll occasionally send you account-related emails.
          </p>

          <p className="mt-4 text-center text-[0.85rem] text-ink-muted">
            Already have an account?{" "}
            <Button
              href="/sign-in"
              variant="link"
              size="sm"
              className="h-auto p-0 align-baseline text-[0.85rem] font-semibold text-accent no-underline"
            >
              Log in
            </Button>
          </p>
        </div>
      </div>
    </div>
  );
}

More patterns

Auth
Sign-in screenA split-screen sign-in with a feature panel, Google SSO, and remember-me.
Auth
Forgot passwordA single email field beside an original customer-quote panel.
Auth
OTP verificationFour large circular code boxes with a live resend countdown.