Date: 2025-10-31 Status: Approved for Implementation Complexity: 3 story points
Current callout CTAs use inconsistent patterns that break design system enforcement:
-
Custom div wrappers (
statsbomb.mdx:98-102):- Manual
bg-surface,border,p-4styling - Not reusable across codebase
- Breaks component-first enforcement
- Manual
-
Inline links in Callouts (
statsbomb.mdx:269-272):- Bypasses Button component (uses raw
<a>tags) - Lower visual prominence
- Inconsistent with primary CTA patterns
- Bypasses Button component (uses raw
Impact: Technical debt, maintenance burden, design system drift
Chosen Approach: Dedicated CalloutCTA compound component
Rationale (70% confidence):
- Explicit intent (separate component for CTA callouts)
- Doesn't pollute base Callout component
- Better discoverability for developers
- Lower risk to existing Callout usage
Alternative Considered: Native CTA slot in Callout (85% confidence)
- Trade-off: Would require migrating all Callout usage
- Benefit: Unified component API
- Decision: Rejected due to higher migration risk
File: src/components/CalloutCTA.astro
Props Interface:
interface Props {
variant: "insight" | "context" | "warning" | "technical" | "subtle";
size?: "default" | "compact" | "inline";
title?: string;
icon?: ComponentProps<typeof Icon>["name"];
class?: string;
}Slot Pattern:
<slot />(default) - Main content area<slot name="cta" />- Action button(s) area (required)
Visual Hierarchy:
- Icon + Title (if provided) - Top section
- Content slot - Middle section with
[&>*:not(:last-child)]:mb-4rhythm - CTA slot - Bottom section with border-top separator
Base Styles (shared with Callout):
const calloutCTAStyles = cva(
[
"rounded-lg", "border-l-4", "shadow-sm",
"bg-surface", "relative", "overflow-hidden"
],
{
variants: {
variant: {
insight: "border-primary bg-primary/5",
context: "border-secondary bg-secondary/5",
warning: "border-amber-500 bg-amber-50",
technical: "border-emerald-500 bg-emerald-50",
subtle: "border-neutral bg-neutral/5"
},
size: {
default: "p-6 border-l-4",
compact: "p-4 border-l-3",
inline: "p-3 border-l-2"
}
},
defaultVariants: {
variant: "context",
size: "default"
}
}
);CTA Section Styles:
const ctaSectionStyles = cva(
[
"flex", "flex-wrap", "gap-3", "items-center",
"border-t", "border-current/20" // Separator inherits variant color
],
{
variants: {
size: {
default: "pt-6 mt-6", // 24px Fibonacci spacing
compact: "pt-4 mt-4", // 16px Fibonacci spacing
inline: "pt-3 mt-3" // 12px Fibonacci spacing
}
}
}
);Key Styling Decisions:
- Border-top separator - Visual distinction between content and action (20% opacity)
- Flexbox layout - Supports multiple buttons side-by-side
- Gap-3 spacing - 12px between multiple buttons (Fibonacci)
- Size-responsive padding - Matches parent callout's size variant
- Color inheritance - Uses
border-currentto inherit variant's accent color
Basic Usage (single CTA):
<CalloutCTA variant="context" title="📐 For Engineers: Technical Deep Dive Available">
<Body size="sm">
ANTLR grammar, XState machines, claims-based metadata architecture.
</Body>
<Fragment slot="cta">
<Button variant="secondary" size="sm" href="/portfolio/statsbomb-technical-appendix">
View Technical Appendix →
</Button>
</Fragment>
</CalloutCTA>Multiple CTAs (primary + secondary):
<CalloutCTA variant="insight" title="Ready to discuss your project?">
<Body size="sm">
Let's explore how functional architecture patterns can solve your challenges.
</Body>
<Fragment slot="cta">
<Button variant="primary" size="sm" href="/contact">
Start a Conversation
</Button>
<Button variant="secondary" size="sm" href="/portfolio">
View Case Studies →
</Button>
</Fragment>
</CalloutCTA>Compact Variant:
<CalloutCTA variant="technical" size="compact">
<Body size="sm">Quick implementation note: See appendix for details.</Body>
<Fragment slot="cta">
<Button variant="secondary" size="sm" href="#appendix">Jump to Section</Button>
</Fragment>
</CalloutCTA>Semantic HTML:
<aside
role="complementary"
aria-labelledby={title ? titleId : undefined}
class={calloutStyles({ variant, size })}
>
{title && <h3 id={titleId}>{title}</h3>}
<div class="content-area">
<slot />
</div>
<div class="cta-section" role="group" aria-label="Actions">
<slot name="cta" />
</div>
</aside>Features:
- ARIA roles:
role="complementary"for callout,role="group"for CTAs - Keyboard navigation: Button components inherit focus management
- Screen reader labels:
aria-labelledbyconnects title to callout - Color contrast: WCAG AA compliance (7.2-9.1:1 for semantic variants)
- Touch targets: Button components enforce 44x44px minimum (WCAG 2.5.5)
- Motion preferences: No animations (static component)
Responsive Behavior:
- Desktop: Horizontal CTA layout (
flex-row, buttons side-by-side) - Mobile: Stack buttons vertically via
flex-wrap
Target Files: src/content/portfolio/statsbomb.mdx
Pattern 1 (lines 98-102) - Custom div wrapper:
<!-- BEFORE -->
<div class="mt-6 p-4 bg-surface rounded-lg border border-neutral-light">
<Body size="sm" as="p" class="mb-2 font-medium">📐 For Engineers...</Body>
<Body size="sm" as="p" class="mb-3 text-text-lighter">ANTLR grammar...</Body>
<Button variant="secondary" size="sm" href="/portfolio/statsbomb-technical-appendix">
View Technical Appendix →
</Button>
</div>
<!-- AFTER -->
<CalloutCTA variant="context" title="📐 For Engineers: Technical Deep Dive Available">
<Body size="sm">ANTLR grammar, XState machines, claims-based metadata...</Body>
<Fragment slot="cta">
<Button variant="secondary" size="sm" href="/portfolio/statsbomb-technical-appendix">
View Technical Appendix →
</Button>
</Fragment>
</CalloutCTA>Pattern 2 (lines 269-272) - Inline link in Callout:
<!-- BEFORE -->
<Callout variant="subtle" title="Technical Deep Dive Available" class="my-8">
<p class="mb-2">For implementation details—DSL syntax...
see the <a href="/portfolio/statsbomb-technical-appendix">Technical Appendix</a>.
</p>
</Callout>
<!-- AFTER -->
<CalloutCTA variant="context" title="Technical Deep Dive Available" class="my-8">
<Body size="sm">
For implementation details—DSL syntax, state machine configuration.
</Body>
<Fragment slot="cta">
<Button variant="secondary" size="sm" href="/portfolio/statsbomb-technical-appendix">
View Technical Appendix →
</Button>
</Fragment>
</CalloutCTA>Semantic Variant Changes:
subtle→context(better semantic match for informational callouts with actions)
- Empty CTA slot: TypeScript error (slot required via Props)
- Non-Button in CTA slot: Runtime works but violates design system (documented)
- Very long button labels:
flex-wraphandles overflow gracefully - Multiple buttons with different sizes: Allowed (design system supports mixed sizes)
- No title: Icon disappears, content flows naturally
Decision Tree (when to use each pattern):
Need informational content?
├─ No action needed → Use <Callout>
├─ External link only → Use <Callout> with inline <Link>
└─ Primary action(s) → Use <CalloutCTA> with Button in cta slot
Need promotional content?
└─ Use <Card> with manual Button placement (marketing flexibility)
Component Registry Updates:
- Add CalloutCTA to
/design-systemroute - Create Astrobook story with all variants
- Update CLAUDE.md with component-first enforcement rules
CLAUDE.md Addition:
✅ ALWAYS use these components:
- CalloutCTA: Informational boxes with primary actions
- Callout: Informational boxes without actions
- Button: All interactive CTAs
❌ FORBIDDEN patterns:
- Custom div wrappers with bg-surface + manual buttons
- Inline <a> tags for primary actions (use Button or Link components)Rollout Steps:
- Create
src/components/CalloutCTA.astrowith CVA styling - Add Astrobook story (
src/stories/CalloutCTA.stories.ts) - Test in
/design-systemroute (visual validation) - Migrate
statsbomb.mdxpatterns (2 instances) - Visual regression testing (before/after screenshots)
- Update CLAUDE.md with component usage rules
- Commit design document + implementation
Testing Checklist:
- Visual regression: Compare before/after screenshots
- Keyboard navigation: Tab through CTAs, verify focus indicators
- Screen reader: VoiceOver announces "Complementary content" + action group
- Responsive: Test button stacking on mobile (< 640px)
- Color contrast: Verify WCAG AA compliance across all 5 variants
- Multiple CTAs: Test primary + secondary button layout
Performance Considerations:
- Zero JavaScript (static HTML + CSS)
- Shares CVA config with Callout (no bundle duplication)
- Lazy-loaded icons via Lucide (tree-shakeable)
Browser Support:
- Flexbox: All modern browsers (IE11+ if needed)
border-current: Chrome 28+, Firefox 40+, Safari 8+- CSS custom properties: All evergreen browsers
Immediate:
- Component renders correctly in Astrobook
- All 5 variants pass WCAG AA contrast checks
- Migration of 2 existing patterns completes without visual regressions
Long-term:
- Zero custom CTA wrappers in codebase
- All CTAs use CalloutCTA or Button components
- Design system documentation updated
Why CalloutCTA over native slot in Callout?
- Explicit intent: Developers know when to use CalloutCTA vs. Callout
- Lower risk: Doesn't require migrating all Callout usage
- Better discoverability: Separate component signals "this is for CTAs"
Why border-top separator?
- Visual hierarchy: Clear distinction between content and action
- Color inheritance: Matches variant's semantic color (via
border-current) - Accessibility: Provides visual cue for screen reader "Actions" group
Why Fibonacci spacing?
- Design system consistency: Matches existing spacing tokens (4/8/16/24px)
- Cognitive simplicity: One developer can hold spacing system in their head
- Predictable scaling: Size variants use proportional spacing
Reversible (85% confident):
- Adding CalloutCTA doesn't break existing Callout usage
- Can deprecate later if native slot approach proves better
- Migration is isolated to 2 files (low blast radius)
Risk: If we need to add more CTA-specific features (e.g., dismiss button, countdown timer), compound component scales better than slot pattern.
Status: ✅ Completed
1. Composition Over Duplication (implemented)
- CalloutCTA wraps Callout component (44 lines vs initial 119 lines)
- Zero code duplication - all variants inherited automatically
- Type-safe with
ComponentProps<typeof Callout>
2. Visual Cohesion Fixes (implemented)
- Fixed
border-current/20issue that resolved to dark navy instead of accent color - Added explicit variant-aware border colors to
ctaSectionCVA:primary: 'border-primary/20'(gold accent)secondary: 'border-secondary/20'(blue accent)warning: 'border-warning/20'(amber accent)success: 'border-success/20'(emerald accent)neutral: 'border-neutral/30'(gray, higher opacity)
3. Ghost Button Variant (implemented)
- Added new
variant="ghost"to Button component for callout-harmonizing CTAs - Styling:
text-secondary border-secondary/40 hover:border-secondary - Removed harsh
scale-[1.02]animation, added subtle glow shadow - Slower transition (300ms vs 200ms) for smoother feel
<CalloutCTA variant="secondary" title="Title" icon="code">
<Body size="sm">Content here...</Body>
<Fragment slot="cta">
<Button variant="ghost" size="sm" href="/path">
Action Label →
</Button>
</Fragment>
</CalloutCTA>CalloutCTA.astro (final):
import Callout from './Callout.astro';
const ctaSection = cva([
'flex', 'flex-wrap', 'gap-3', 'items-center', 'border-t'
], {
variants: {
variant: {
primary: 'border-primary/20',
secondary: 'border-secondary/20',
warning: 'border-warning/20',
success: 'border-success/20',
neutral: 'border-neutral/30',
},
size: {
default: 'pt-6 mt-6',
compact: 'pt-4 mt-4',
inline: 'pt-3 mt-3',
}
}
});
<Callout variant={variant} size={size} {...calloutProps}>
<slot />
<div class={ctaSection({ variant, size })}>
<slot name="cta" />
</div>
</Callout>Button.astro (ghost variant added):
ghost: 'bg-transparent text-secondary border-2 border-secondary/40 hover:border-secondary hover:shadow-[0_4px_12px_rgba(14,165,233,0.15)] transition-all duration-300'Files Updated:
src/components/CalloutCTA.astro- Created with composition patternsrc/components/Button.astro- Added ghost variantsrc/pages/portfolio/statsbomb.mdx- Migrated 2 CTA patterns to use CalloutCTA + ghost buttonssrc/pages/callout-cta-demo.astro- Added comprehensive demo pageCLAUDE.md- Updated component-first enforcement section
Visual Result:
- CTA section visually integrated with parent callout (blue border matches blue callout)
- Ghost buttons harmonize with callout colors (blue outline, no harsh scale animation)
- Soft, cohesive aesthetic throughout
Approved by: User (brainstorming session, 2025-10-31) Implementation Status: ✅ Completed (2025-10-31) Complexity: 3 story points (actual)