VisorVisor
ComponentsFeedback

ChallengeCard

Adversarial challenge message card — an AI pushes back on the user's input and the human holds the gate.

ChallengeCard is a first-class adversarial message component: an AI pushes back on the user's input and the human holds the gate. It is distinct from Alert in both purpose and API.

Alert vs ChallengeCard:

  • Alert — passive notice. Static callout for information, warnings, or errors that require no explicit decision.
  • ChallengeCard — adversarial prompt. The AI is questioning the user's intent; the human must choose an action before proceeding.

Why standalone, not built on Alert internals? Alert's compound sub-components (AlertTitle, AlertDescription, AlertActions) don't cover the warning-toned filled primary action button or the gate affordance — those are semantically different (real <button> elements with click handlers, not layout containers). Building standalone keeps the API clean and avoids leaking Alert's variant CVA logic into a component that has no variants.

The onliness challenge

Custom gate label

Override the default "You hold the gate" text by passing children to ChallengeCardGate.

Installation

npx visor add challenge-card

This copies two files into your project:

  • components/ui/challenge-card/challenge-card.tsx — the component
  • components/ui/challenge-card/challenge-card.module.css — the styles

Usage

import {
  ChallengeCard,
  ChallengeCardHeader,
  ChallengeCardBody,
  ChallengeCardActions,
  ChallengeCardAction,
  ChallengeCardGate,
} from '@/components/ui/challenge-card/challenge-card';

<ChallengeCard>
  <ChallengeCardHeader>Is it actually only?</ChallengeCardHeader>
  <ChallengeCardBody>
    The word "only" in this claim is absolute — it invites factual rebuttal.
  </ChallengeCardBody>
  <ChallengeCardActions>
    <ChallengeCardAction variant="primary" onClick={handleAccept}>
      Use the sharper version
    </ChallengeCardAction>
    <ChallengeCardAction variant="ghost" onClick={handleRewrite}>
      I'll rewrite it
    </ChallengeCardAction>
    <ChallengeCardGate />
  </ChallengeCardActions>
</ChallengeCard>

API Reference

ChallengeCardProps

PropTypeDefaultDescription
iconReactNode | nullChallengeCardHeader: override the default Flag icon (null suppresses it). ChallengeCardAction: optional leading icon — primary defaults to a Check icon.
variant'primary' | 'ghost''primary'ChallengeCardAction style — "primary" is the filled warning-toned action, "ghost" is transparent with a border.
classNamestringAdditional CSS class names to merge onto any sub-component.

The component also accepts all standard <div> HTML attributes.

Sub-components

  • ChallengeCardHeader — Flag icon + uppercase warning-toned title. Accepts icon prop to override (null to suppress the icon). Accepts all <div> attributes.
  • ChallengeCardBody — Prose body text. Accepts all <div> attributes.
  • ChallengeCardActions — Flex row container for actions and the gate. Accepts all <div> attributes.
  • ChallengeCardAction — Real <button> with variant="primary" (filled warning) or variant="ghost" (transparent + border). Accepts icon prop to override the default icon (null to suppress). Accepts all <button> attributes including onClick and disabled.
  • ChallengeCardGate — Lock icon + label pushed right via margin-left: auto. Defaults to "You hold the gate"; pass children to override. Accepts all <span> attributes.

Accessibility

  • The root renders with role="alert" so screen readers announce the challenge immediately.
  • Action buttons are real <button> elements with keyboard focus and visible focus rings via var(--focus-ring-width) and var(--focus-ring-offset).
  • The header icon and gate icon are aria-hidden="true" — meaning is conveyed through the title and gate label text.
  • Disabled actions receive opacity only; the element remains in the DOM so users can still discover it exists.