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
The word “only” in this claim is absolute — it invites factual rebuttal. Do you have data to back it up, or would a sharper qualifier work better?
Custom gate label
Override the default "You hold the gate" text by passing children to ChallengeCardGate.
Installation
npx visor add challenge-cardThis copies two files into your project:
components/ui/challenge-card/challenge-card.tsx— the componentcomponents/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
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ReactNode | null | — | ChallengeCardHeader: 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. |
className | string | — | Additional 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. Acceptsiconprop to override (nullto 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>withvariant="primary"(filled warning) orvariant="ghost"(transparent + border). Acceptsiconprop to override the default icon (nullto suppress). Accepts all<button>attributes includingonClickanddisabled.ChallengeCardGate— Lock icon + label pushed right viamargin-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 viavar(--focus-ring-width)andvar(--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
opacityonly; the element remains in the DOM so users can still discover it exists.