Pro Tip

Highlighted tip block with variants for tips, best practices, gotchas, and more.

Report a bug

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/pro-tip.json
bash

Storybook

Explore all variants, controls, and accessibility checks in the interactive Storybook playground.

View in Storybook

Code

import type { ReactNode } from "react";

import { cn } from "../../lib/utils";

export type ProTipVariant =
  | "advanced"
  | "best-practice"
  | "expert"
  | "gotcha"
  | "performance"
  | "tip";

export type ProTipProps = {
  children: ReactNode;
  className?: string;
  icon?: ReactNode;
  title?: string;
  variant?: ProTipVariant;
};

const variantStyles: Record<
  ProTipVariant,
  { className: string; defaultTitle: string }
> = {
  advanced: {
    className:
      "border-purple-500/50 bg-purple-500/10 text-purple-700 dark:text-purple-300",
    defaultTitle: "Advanced",
  },
  "best-practice": {
    className:
      "border-blue-500/50 bg-blue-500/10 text-blue-700 dark:text-blue-300",
    defaultTitle: "Best Practice",
  },
  expert: {
    className:
      "border-amber-500/50 bg-amber-500/10 text-amber-700 dark:text-amber-300",
    defaultTitle: "Expert Tip",
  },
  gotcha: {
    className: "border-red-500/50 bg-red-500/10 text-red-700 dark:text-red-300",
    defaultTitle: "Common Gotcha",
  },
  performance: {
    className:
      "border-green-500/50 bg-green-500/10 text-green-700 dark:text-green-300",
    defaultTitle: "Performance",
  },
  tip: {
    className: "border-primary/50 bg-primary/10 text-primary",
    defaultTitle: "Pro Tip",
  },
};

export function ProTip({
  children,
  className,
  icon,
  title,
  variant = "tip",
}: ProTipProps): React.ReactNode {
  const config = variantStyles[variant];

  return (
    <div
      className={cn("my-6 rounded-lg border p-4", config.className, className)}
    >
      <div className="flex items-start gap-3">
        <div className="flex size-8 items-center justify-center rounded-full bg-current/10 flex-shrink-0">
          {icon ? (
            <span className="size-4">{icon}</span>
          ) : (
            <svg
              className="size-4"
              fill="none"
              stroke="currentColor"
              viewBox="0 0 24 24"
            >
              <path
                d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth={2}
              />
            </svg>
          )}
        </div>
        <div className="flex-1 min-w-0">
          <p className="font-semibold text-sm mb-1">
            {title || config.defaultTitle}
          </p>
          <div className="text-sm [&>p]:mb-0 opacity-90">{children}</div>
        </div>
      </div>
    </div>
  );
}

export type CommonMistakeProps = {
  children: ReactNode;
  className?: string;
  fix?: ReactNode;
  fixIcon?: ReactNode;
  icon?: ReactNode;
  title?: string;
};

// eslint-disable-next-line max-lines-per-function -- Complex component with conditional fix section
export function CommonMistake({
  children,
  className,
  fix,
  fixIcon,
  icon,
  title = "Common Mistake",
}: CommonMistakeProps): React.ReactNode {
  return (
    <div
      className={cn(
        "my-6 rounded-lg border border-red-500/50 bg-red-500/5 overflow-hidden",
        className,
      )}
    >
      <div className="p-4">
        <div className="flex items-start gap-3">
          {icon ? (
            <span className="size-5 text-red-500 flex-shrink-0 mt-0.5">
              {icon}
            </span>
          ) : (
            <svg
              className="size-5 text-red-500 flex-shrink-0 mt-0.5"
              fill="none"
              stroke="currentColor"
              viewBox="0 0 24 24"
            >
              <path
                d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth={2}
              />
            </svg>
          )}
          <div className="flex-1 min-w-0">
            <p className="font-semibold text-sm text-red-700 dark:text-red-300 mb-1">
              {title}
            </p>
            <div className="text-sm text-muted-foreground [&>p]:mb-0">
              {children}
            </div>
          </div>
        </div>
      </div>
      {fix ? (
        <div className="border-t border-red-500/30 bg-green-500/5 p-4">
          <div className="flex items-start gap-3">
            {fixIcon ? (
              <span className="size-5 text-green-500 flex-shrink-0 mt-0.5">
                {fixIcon}
              </span>
            ) : (
              <svg
                className="size-5 text-green-500 flex-shrink-0 mt-0.5"
                fill="none"
                stroke="currentColor"
                viewBox="0 0 24 24"
              >
                <path
                  d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth={2}
                />
              </svg>
            )}
            <div className="flex-1 min-w-0">
              <p className="font-semibold text-sm text-green-700 dark:text-green-300 mb-1">
                The Fix
              </p>
              <div className="text-sm text-muted-foreground [&>p]:mb-0">
                {fix}
              </div>
            </div>
          </div>
        </div>
      ) : null}
    </div>
  );
}
typescript

Dependencies

  • @vllnt/ui@^0.2.1