Login Form

A login page with email and password fields, error state and submit button. Composed from foldcn primitives.

Preview
Style
Welcome back
Enter your email below to login to your account

Forgot your password? Reset it

src/demo/views/login-form.ts70 lines
import { Schema as S } from 'effect'
import { evo } from 'foldkit/struct'
import { defineMessageUnion } from 'foldkit/message'
import type { Html, HtmlBuilder } from 'foldkit/html'

import { loginForm } from '../../generated/registry/blocks/login-form/login-form'

import { defineSlice, type UpdateReturn } from '../slice'
import type { Model, Message as AppMessage } from '../assemble'

const Message = defineMessageUnion({
  UpdatedLoginEmail: { value: S.String },
  UpdatedLoginPassword: { value: S.String },
  SubmittedLogin: {},
})

export const loginFormView = (model: Model, h: HtmlBuilder<AppMessage>): Html =>
  h.div(
    [h.Class('w-full overflow-hidden rounded-xl border border-border')],
    [
      loginForm<AppMessage>(
        {
          email: model.loginEmail,
          onEmailInput: (value) => Message.UpdatedLoginEmail({ value }),
          password: model.loginPassword,
          onPasswordInput: (value) => Message.UpdatedLoginPassword({ value }),
          onSubmit: Message.SubmittedLogin(),
        },
        h,
      ),
      ...(model.loginSubmitted
        ? [
            h.p(
              [h.Class('mb-4 px-6 text-center text-sm text-emerald-600 dark:text-emerald-400')],
              ['Signed in (demo).'],
            ),
          ]
        : []),
    ],
  )

const fields = {
  loginEmail: S.String,
  loginPassword: S.String,
  loginSubmitted: S.Boolean,
}

const stateSchema = S.Struct(fields)
type State = typeof stateSchema.Type

export const slice = defineSlice({
  fields,
  init: {
    loginEmail: '',
    loginPassword: '',
    loginSubmitted: false,
  },
  messages: [Message.UpdatedLoginEmail, Message.UpdatedLoginPassword, Message.SubmittedLogin],
  handlers: (model: State) => ({
    UpdatedLoginEmail: ({ value }: typeof Message.UpdatedLoginEmail.Type): UpdateReturn => ({
      model: evo(model, { loginEmail: () => value }),
    }),
    UpdatedLoginPassword: ({ value }: typeof Message.UpdatedLoginPassword.Type): UpdateReturn => ({
      model: evo(model, { loginPassword: () => value }),
    }),
    SubmittedLogin: (): UpdateReturn => ({ model: evo(model, { loginSubmitted: () => true }) }),
  }),
  samples: [Message.UpdatedLoginEmail({ value: 'ada@example.com' }), Message.SubmittedLogin()],
})

This code is shown verbatim — it’s the exact file used for this preview. Much of the surrounding wiring (slice, fields, messages, handlers) belongs to this site’s demo harness; you only need the view and the state that matters for your own app. Adapt what you need.

Installation

Add this component to your project:

pnpm dlx shadcn@latest add @foldcn/login-form

Source

The component ships as plain source — no build step, no wrapper. Copy it and make it yours.

registry/default/blocks/login-form/login-form.ts103 lines
import type { Html, HtmlBuilder } from 'foldkit/html'

import { button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { input } from '@/components/ui/input'

export type LoginFormConfig<M> = Readonly<{
  email: string
  password: string
  onEmailInput: (value: string) => M
  onPasswordInput: (value: string) => M
  onSubmit: M
  isSubmitting?: boolean
  error?: string
  className?: string
}>

/** Login page block: email + password with a submit button, composed from
 *  foldcn primitives. */
export const loginForm = <M>(config: LoginFormConfig<M>, h: HtmlBuilder<M>): Html =>
  h.div(
    [h.Class('flex min-h-svh w-full items-center justify-center p-4')],
    [
      Card<M>(
        { className: 'w-full max-w-md' },
        [
          Card.header<M>(
            {},
            [
              Card.title<M>({}, ['Welcome back'], h),
              Card.description<M>({}, ['Enter your email below to login to your account'], h),
            ],
            h,
          ),
          Card.content<M>(
            {},
            [
              h.div(
                [h.Class('grid gap-4')],
                [
                  input<M>(
                    {
                      id: 'login-email',
                      label: 'Email',
                      type: 'email',
                      value: config.email,
                      onInput: config.onEmailInput,
                      placeholder: 'you@example.com',
                      isDisabled: config.isSubmitting,
                    },
                    h,
                  ),
                  input<M>(
                    {
                      id: 'login-password',
                      label: 'Password',
                      type: 'password',
                      value: config.password,
                      onInput: config.onPasswordInput,
                      placeholder: '••••••••',
                      isDisabled: config.isSubmitting,
                    },
                    h,
                  ),
                  ...(config.error === undefined
                    ? []
                    : [h.p([h.Class('text-sm text-destructive')], [config.error])]),
                  button<M>(
                    {
                      onClick: config.onSubmit,
                      isDisabled: config.isSubmitting,
                      className: 'w-full',
                    },
                    config.isSubmitting === true ? 'Signing in...' : 'Login',
                    h,
                  ),
                ],
              ),
            ],
            h,
          ),
          Card.footer<M>(
            {},
            [
              h.p(
                [h.Class('text-sm text-muted-foreground')],
                [
                  'Forgot your password? ',
                  h.a(
                    [h.Href('#'), h.Class('underline underline-offset-4 hover:text-primary')],
                    ['Reset it'],
                  ),
                ],
              ),
            ],
            h,
          ),
        ],
        h,
      ),
    ],
  )