themeplate.xyz

/components

Component library

Don't need the whole template? Copy a single part, or add just that part with the CLI.

SignInCard

Auth

Email and password form with Google and Apple buttons.

From keystone

export function SignInCard({ onSubmit }: { onSubmit: (data: FormData) => void }) {
  return (
    <form
      action={onSubmit}
      className="w-full max-w-sm space-y-4 rounded-lg border border-border p-6"
    >
      <h2 className="text-lg font-semibold">Sign in</h2>
      <Input name="email" type="email" placeholder="you@company.com" required />
      <Input name="password" type="password" placeholder="Password" required />
      <Button type="submit" className="w-full">Continue</Button>
      <div className="grid grid-cols-2 gap-2">
        <Button type="button" variant="outline">Google</Button>
        <Button type="button" variant="outline">Apple</Button>
      </div>
    </form>
  )
}
npx themeplate add keystone --only sign-in-card

WorkspaceSwitcher

Auth

Header control to switch between workspaces.

From keystone

export function WorkspaceSwitcher({ workspaces, activeId }: Props) {
  const active = workspaces.find((w) => w.id === activeId)
  return (
    <button className="flex h-9 items-center gap-2 rounded-md border border-border px-2.5 text-sm">
      <span className="grid size-5 place-items-center rounded bg-tint text-xs font-medium text-accent-foreground">
        {active?.name[0]}
      </span>
      {active?.name}
      <ChevronsUpDown className="size-3.5 text-muted-foreground" />
    </button>
  )
}
npx themeplate add keystone --only workspace-switcher

RoleBadge

Auth

Compact indicator for owner, admin and member roles.

From keystone

const styles = {
  owner: 'bg-tint text-accent-foreground',
  admin: 'bg-surface text-foreground',
  member: 'bg-surface text-muted-foreground',
}

export function RoleBadge({ role }: { role: keyof typeof styles }) {
  return (
    <span className={cn('rounded px-1.5 py-0.5 font-mono text-[11px] uppercase', styles[role])}>
      {role}
    </span>
  )
}
npx themeplate add keystone --only role-badge

AnnouncementBar

Marketing

Dismissible notice that sits above the header.

From atrium

export function AnnouncementBar({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(true)
  if (!open) return null
  return (
    <div className="flex items-center justify-center gap-3 bg-foreground px-4 py-2 text-sm text-background">
      {children}
      <button onClick={() => setOpen(false)} aria-label="Dismiss announcement">
        <X className="size-4" />
      </button>
    </div>
  )
}
npx themeplate add atrium --only announcement-bar

PricingTier

Marketing

A single plan with price, features and a call to action.

From lintel

export function PricingTier({ name, price, features, highlighted }: Tier) {
  return (
    <div
      className={cn(
        'rounded-lg border p-6',
        highlighted ? 'border-primary' : 'border-border',
      )}
    >
      <p className="font-medium">{name}</p>
      <p className="mt-2 text-3xl font-semibold">{price}<span className="text-sm text-muted-foreground">/mo</span></p>
      <ul className="mt-4 space-y-2 text-sm">
        {features.map((f) => (
          <li key={f} className="flex gap-2"><Check className="size-4 text-primary" />{f}</li>
        ))}
      </ul>
      <Button className="mt-6 w-full" variant={highlighted ? 'default' : 'outline'}>Choose {name}</Button>
    </div>
  )
}
npx themeplate add lintel --only pricing-tier

StatCard

Dashboard

Metric with a change indicator for dashboards.

From girder

export function StatCard({ label, value, delta }: Props) {
  const positive = delta >= 0
  return (
    <div className="rounded-lg border border-border p-4">
      <p className="text-sm text-muted-foreground">{label}</p>
      <p className="mt-1 text-2xl font-semibold tabular-nums">{value}</p>
      <p className={cn('mt-1 flex items-center gap-1 text-xs', positive ? 'text-primary' : 'text-destructive')}>
        {positive ? <ArrowUpRight className="size-3" /> : <ArrowDownRight className="size-3" />}
        {Math.abs(delta)}% vs last month
      </p>
    </div>
  )
}
npx themeplate add girder --only stat-card

ArticleCard

Content

Post summary with tags and reading time.

From mullion

export function ArticleCard({ title, excerpt, tags, minutes, href }: Props) {
  return (
    <article className="rounded-lg border border-border p-5">
      <div className="flex gap-2">
        {tags.map((tag) => (
          <span key={tag} className="font-mono text-[11px] uppercase text-primary">{tag}</span>
        ))}
      </div>
      <h3 className="mt-2 text-lg font-semibold text-balance">
        <Link href={href}>{title}</Link>
      </h3>
      <p className="mt-1 text-sm text-muted-foreground">{excerpt}</p>
      <p className="mt-3 text-xs text-muted-foreground">{minutes} min read</p>
    </article>
  )
}
npx themeplate add mullion --only article-card

ProductCard

Commerce

Product tile with price and an add-to-cart action.

From bazaar

export function ProductCard({ name, price, image, onAdd }: Props) {
  return (
    <div className="rounded-lg border border-border p-3">
      <img src={image} alt={name} className="aspect-square w-full rounded-md object-cover" />
      <div className="mt-3 flex items-center justify-between">
        <div>
          <p className="text-sm font-medium">{name}</p>
          <p className="font-mono text-sm text-muted-foreground">{price}</p>
        </div>
        <Button size="sm" onClick={onAdd}>Add</Button>
      </div>
    </div>
  )
}
npx themeplate add bazaar --only product-card