Skip to content
Block

Auth panel

A centred sign-in panel with the providers stacked rather than ranked.

  • #arc
  • #block
  • #auth
  • #login
  • #signin
  • #panel
  • #account
  • #illustration

Install

One command installs the files and packages this item needs.

Terminalbash
npx shadcn@latest add https://dsui.vercel.app/r/auth-panel.json

auth-panel.module.csscss
.panel { display: flex; width: 100%; max-width: 26rem; margin-inline: auto; flex-direction: column; align-items: center; gap: var(--space-3); border: 1px solid var(--border-subtle); border-radius: var(--radius-surface); padding: var(--space-10) var(--space-6); background: var(--surface); text-align: center; }
.icon { display: inline-flex; width: var(--space-12); height: var(--space-12); align-items: center; justify-content: center; border-radius: var(--radius-control); background: var(--highlight-subtle); color: var(--foreground); }
.title { margin: 0; color: var(--foreground); font-size: var(--text-xl); font-weight: 600; line-height: 1.4; text-wrap: balance; }
.description { margin: 0; color: var(--text-secondary); font-size: var(--text-sm); line-height: 1.6; word-break: keep-all; }
/* Stacked, not in a row: providers are not ranked, and a row makes the first one look like the right answer. */
.actions { display: flex; width: 100%; flex-direction: column; gap: var(--space-2); margin-block-start: var(--space-3); }
.actions > * { width: 100%; }
.notice { margin: var(--space-2) 0 0; color: var(--text-secondary); font-size: var(--text-xs); line-height: 1.6; word-break: keep-all; }

/* The page-top illustration runs edge to edge across the panel top, under the rounded corners. */
.panel .art { width: calc(100% + var(--space-12)); margin: calc(var(--space-10) * -1) calc(var(--space-6) * -1) var(--space-2); }
auth-panel.tsxtsx
import type { HTMLAttributes, ReactNode } from "react";
import { Illustration } from "../../illustration/illustration";
import styles from "./auth-panel.module.css";

export interface AuthPanelProps extends HTMLAttributes<HTMLElement> {
  title: string;
  /** Why signing in is worth it, in one line. */
  description?: ReactNode;
  /** The sign-in buttons, stacked. One per provider. */
  actions: ReactNode;
  /** Terms and privacy, under the buttons. */
  notice?: ReactNode;
  /** A mark above the title. */
  icon?: ReactNode;
  /** Draws the login page-top illustration (a lock and a key) at the top of the panel. It replaces `icon`. The theme's own art is used when it has one. */
  illustration?: boolean;
  /** The heading level. Default 1: this panel is usually the whole page. */
  headingLevel?: 1 | 2 | 3;
}

/**
 * The centred panel on a sign-in page, or wherever something asks the reader to sign in first.
 *
 * The description says what signing in gets them, not that they must. Someone who arrived here by following a link
 * has already been interrupted; the panel's job is to make the interruption worth it rather than to scold.
 *
 * The buttons stack rather than sitting in a row: providers are not ranked, and a row makes the first one look like
 * the right answer.
 */
export function AuthPanel({ className, title, description, actions, notice, icon, illustration = false, headingLevel = 1, ...props }: AuthPanelProps) {
  const Heading = `h${headingLevel}` as "h1" | "h2" | "h3";
  return (
    <section {...props} className={[styles.panel, className].filter(Boolean).join(" ")}>
      {illustration ? <Illustration name="page-login" animated className={styles.art} /> : icon ? <span className={styles.icon} aria-hidden="true">{icon}</span> : null}
      <Heading className={styles.title}>{title}</Heading>
      {description ? <p className={styles.description}>{description}</p> : null}
      <div className={styles.actions}>{actions}</div>
      {notice ? <p className={styles.notice}>{notice}</p> : null}
    </section>
  );
}

AuthPanel.displayName = "AuthPanel";

export default AuthPanel;

Used items