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
Built with
- Form / FormFieldSetComponent
- Password FieldComponent
- ButtonComponent
- AvatarComponent
- Page TitleComponent
How it’s built
- 01oks-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. - 02The 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-raisedcard floating on top, so every oks-ui component renders with the contrast it was built for — no fighting the theme system. - 03The brand mark and the three feature icons are still real
Avatarcomponents even on the dark canvas — they're tinted via theclassNamesslot override ({ base: "bg-white/10", icon: "text-white" }) instead of thecolorprop, sincecolorresolves through the same:root[data-theme]-scoped tokens that don't apply here. - 04The headline is a real
PageTitletoo —classNames={{ base: "flex-col items-start" }}becausePageTitle's own wrapper isdisplay: inline-flexwith no set direction, built for a compact inline icon+title rather than a stacked heading. - 05
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. - 06The three SSO buttons are
Button variant="bordered"in agrid-cols-3row — 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. - 07"Terms of Service" and "Log in" are real
Button variant="link"components (withhref, notype, so it renders as an<a>), not raw anchors —no-underlineoverrides the variant's default underline to match this pattern's plain-text link style. - 08Layout 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
Two files · copy & pasteStyling: this source uses Tailwind utility classes and this site’s colour tokens (for example text-ink, bg-paper and border-line). The oks-ui components render without them; swap those classes for your own.
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 "./icons";
// Standard, single-colour SSO service marks — the same idiom every
// "continue with X" button on the web uses, not reproducing any artwork.
const 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>
);
};
const 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.
const 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.",
},
];
const 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’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>
);
};
export default SignUp;It imports ./icons, so save this next to it:
export const CheckIcon = ({ size = 18 }: { size?: number }) => {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M5 13l4 4L19 7" />
</svg>
);
};
export const PieChartIcon = ({ size = 18 }: { size?: number }) => {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M21.21 15.89A10 10 0 1 1 8 2.83" />
<path d="M22 12A10 10 0 0 0 12 2v10z" />
</svg>
);
};
export const ShieldIcon = ({ size = 18 }: { size?: number }) => {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M12 2 4 5v6c0 5 3.5 8.5 8 10 4.5-1.5 8-5 8-10V5l-8-3Z" />
<path d="m9 12 2 2 4-4" />
</svg>
);
};
export const UsersIcon = ({ size = 18 }: { size?: number }) => {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
);
};