# Minimal Login Card

> Plain sign-in card: a title, email and password fields with a forgot-password link, a sign-in button and a link to create an account, validated with React Hook Form and Zod. Theme tokens only, light and dark.

A shadcn/ui block from the [@01orbital](https://01orbital.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/01orbital/minimal-login-01).

- Page, with a live preview: https://shadcn.exchange/r/01orbital/minimal-login-01
- Screenshot: https://shadcn.exchange/shot/01orbital/minimal-login-01/light.png
- Registry item: https://01orbital.dev/r/minimal-login-01.json
- Categories: login
- Listed: 2026-11-01, source last changed 2026-11-01

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/01orbital/minimal-login-01.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@hookform/resolvers`, `react-hook-form`, `zod`
- Registry items: `@01orbital/button`, `card`, `@01orbital/field`, `@01orbital/input`, `@01orbital/section`

## Files

### `blocks/login/minimal-login-01/index.tsx`

```tsx
"use client"

import { useId } from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { Controller, useForm } from "react-hook-form"
import { z } from "zod"

import {
  Section,
  SectionContent,
} from "@/components/editorial/section"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import {
  Field,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"

// The copy: change text here, not in the markup.
const COPY = {
  title: "Sign in to Orbcore",
  body: "Use the email you signed up with.",
  email: "Email",
  password: "Password",
  forgot: "Forgot your password?",
  submit: "Sign in",
  signup: { prompt: "New to Orbcore? ", cta: "Create an account" },
  errors: {
    email: "Enter a valid email address.",
    password: "Enter your password.",
  },
} as const

const schema = z.object({
  email: z.email(COPY.errors.email),
  password: z.string().min(1, COPY.errors.password),
})
type Values = z.infer<typeof schema>

export default function MinimalLogin01() {
  const id = useId()
  const form = useForm<Values>({
    resolver: zodResolver(schema),
    defaultValues: { email: "", password: "" },
  })

  // Your sign-in action goes here; until then the form only validates.
  function onSubmit(values: Values) {
    void values
  }

  return (
    <Section>
      <SectionContent>
        <Card className="mx-auto w-full max-w-sm">
          <CardHeader>
            <CardTitle className="text-xl">
              <h1>{COPY.title}</h1>
            </CardTitle>
            <CardDescription>{COPY.body}</CardDescription>
          </CardHeader>
          <CardContent>
            <form onSubmit={form.handleSubmit(onSubmit)} noValidate>
              <FieldGroup>
                <Controller
                  name="email"
                  control={form.control}
                  render={({ field, fieldState }) => (
                    <Field data-invalid={fieldState.invalid}>
                      <FieldLabel htmlFor={`${id}-email`}>
                        {COPY.email}
                      </FieldLabel>
                      <Input
                        {...field}
                        id={`${id}-email`}
                        type="email"
                        autoComplete="email"
                        required
                        aria-invalid={fieldState.invalid}
                      />
                      <FieldError errors={[fieldState.error]} />
                    </Field>
                  )}
                />
                <Controller
                  name="password"
                  control={form.control}
                  render={({ field, fieldState }) => (
                    <Field data-invalid={fieldState.invalid}>
                      <div className="flex items-center">
                        <FieldLabel htmlFor={`${id}-password`}>
                          {COPY.password}
                        </FieldLabel>
                        <a
                          href="#"
                          className="ml-auto text-sm underline-offset-4 hover:underline"
                        >
                          {COPY.forgot}
                        </a>
                      </div>
                      <Input
                        {...field}
                        id={`${id}-password`}
                        type="password"
                        autoComplete="current-password"
                        required
                        aria-invalid={fieldState.invalid}
                      />
                      <FieldError errors={[fieldState.error]} />
                    </Field>
                  )}
                />
                <Field>
                  <Button type="submit">{COPY.submit}</Button>
                </Field>
                <FieldDescription className="text-center">
                  {COPY.signup.prompt}
                  <a href="#">{COPY.signup.cta}</a>
                </FieldDescription>
              </FieldGroup>
            </form>
          </CardContent>
        </Card>
      </SectionContent>
    </Section>
  )
}
```
