CTA Split Image
Split conversion banner with copy and CTAs beside a gradient-mesh visual with floating stat chips.
ctasplitbannerconversion
Live Preview
Ready to ship your best page yet?
Join thousands of teams building fast, accessible Astro sites. Free forever for personal projects.
99.9%uptime< 50msTTFB100Lighthouse
Usage
Copy and paste this code into your Astro page:
astro
---
import { CtaSplitImage } from 'astro-component-kit/sections';
---
<CtaSplitImage
title="Ready to ship?"
primary={{ label: 'Get started', href: '/signup' }}
chips={[{ value: '100', label: 'Lighthouse' }]}
/> ---
import { CtaSplitImage } from 'astro-component-kit/sections';
---
<CtaSplitImage
title="Ready to ship?"
primary={{ label: 'Get started', href: '/signup' }}
chips={[{ value: '100', label: 'Lighthouse' }]}
/> Manual Installation
If you are not using the npm package, create a new file and paste the following code: src/components/sections/CTASplitImage.astro:
astro
---
/**
* CtaSplitImage — Conversion banner split into copy + CTA and a decorative
* visual panel (CSS gradient mesh with floating stat chips). Optional real image.
*
* @param {string} title - Heading.
* @param {string} text - Supporting copy.
* @param {{label: string, href: string}} primary - Primary CTA.
* @param {{label: string, href: string}} secondary - Secondary CTA.
* @param {string} image - Optional image URL (replaces the CSS visual).
* @param {string} imageAlt - Alt text when an image is used.
* @param {Array<{value: string, label: string}>} chips - Floating stat chips on the visual.
* @param {string} variant - Panel style (mesh|solid|outline).
* @param {string} size - Vertical rhythm (sm|md|lg).
* @param {string} className - Extra CSS class.
*/
interface Props {
title?: string;
text?: string;
primary?: { label: string; href: string };
secondary?: { label: string; href: string };
image?: string;
imageAlt?: string;
chips?: Array<{ value: string; label: string }>;
variant?: 'mesh' | 'solid' | 'outline';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const {
title = 'Ready to ship your best page yet?',
text = 'Join thousands of teams building fast, accessible Astro sites. Free forever for personal projects.',
primary = { label: 'Get started free', href: '#' },
secondary = { label: 'Talk to sales', href: '#' },
image,
imageAlt = '',
chips = [
{ value: '99.9%', label: 'uptime' },
{ value: '< 50ms', label: 'TTFB' },
{ value: '100', label: 'Lighthouse' },
],
variant = 'mesh',
size = 'md',
className = '',
} = Astro.props;
const id = `csi-${Math.random().toString(36).slice(2, 8)}`;
---
<div class="cq-cta-split-image-cq">
<section class={`csi csi--${variant} csi--${size} ${className}`} aria-labelledby={id}>
<div class="csi__box">
<div class="csi__copy">
<h2 id={id} class="csi__title">{title}</h2>
<p class="csi__text">{text}</p>
<div class="csi__actions">
<a class="csi__btn csi__btn--primary" href={primary.href}>{primary.label}</a>
{secondary && <a class="csi__btn csi__btn--ghost" href={secondary.href}>{secondary.label}</a>}
</div>
</div>
<div class="csi__visual">
{image ? (
<img src={image} alt={imageAlt} loading="lazy" decoding="async" />
) : (
<div class="csi__mesh" aria-hidden="true">
{chips.map((c, i) => (
<span class="csi__chip" style={`--i:${i}`}><strong>{c.value}</strong>{c.label}</span>
))}
</div>
)}
</div>
</div>
</section>
</div>
<style>
.cq-cta-split-image-cq { container-type: inline-size; }
.csi {
--csi-py: 5rem;
padding: var(--csi-py) var(--sp-6, 1.5rem);
background: var(--c-bg, #050505);
}
.csi--sm { --csi-py: 3rem; }
.csi--lg { --csi-py: 7rem; }
.csi__box {
max-width: 1180px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr;
border-radius: var(--r-xl, 28px);
overflow: hidden;
border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.08));
background: var(--c-bg-1, #0a0a0a);
}
.csi--outline .csi__box { background: transparent; border-color: rgb(var(--ink-rgb, 255 255 255) / 0.15); }
.csi__copy { padding: clamp(2rem, 6vw, 4rem); display: flex; flex-direction: column; justify-content: center; }
.csi__title { margin: 0 0 1rem; font-size: clamp(1.75rem, 4vw, 2.75rem); font-weight: 800; letter-spacing: -0.03em; line-height: 1.1; color: var(--c-text-1, #fff); text-wrap: balance; }
.csi__text { margin: 0 0 2rem; font-size: 1.0625rem; line-height: 1.6; color: var(--c-text-2, #a3a3a3); max-width: 460px; }
.csi__actions { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.csi__btn { display: inline-flex; align-items: center; min-height: 48px; padding: 0 1.5rem; border-radius: var(--r-md, 12px); font-weight: 650; text-decoration: none; transition: background 0.2s ease, border-color 0.2s ease; }
.csi__btn--primary { background: var(--c-primary-light, #d4a017); color: var(--c-on-primary, #0a0a0a); }
.csi__btn--primary:hover { background: var(--c-accent, #fef3c7); color: var(--c-on-primary, #0a0a0a); }
.csi__btn--ghost { border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.15); color: var(--c-text-1, #fff); }
.csi__btn--ghost:hover { border-color: rgb(var(--ink-rgb, 255 255 255) / 0.32); color: var(--c-text-1, #fff); }
.csi__btn:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 3px; }
.csi__visual { position: relative; min-height: 320px; }
.csi__visual img { width: 100%; height: 100%; object-fit: cover; display: block; }
.csi__mesh {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1rem;
padding: 2rem;
}
.csi--mesh .csi__mesh {
background:
radial-gradient(circle at 20% 20%, color-mix(in srgb, var(--c-primary-light, #d4a017) 55%, transparent), transparent 45%),
radial-gradient(circle at 80% 30%, rgba(144, 133, 233, 0.55), transparent 45%),
radial-gradient(circle at 50% 90%, rgba(57, 135, 229, 0.5), transparent 50%),
#121218;
}
.csi--solid .csi__mesh { background: linear-gradient(135deg, #1a1408, #0c0c10); }
.csi--outline .csi__mesh { background: repeating-linear-gradient(45deg, rgb(var(--ink-rgb, 255 255 255) / 0.03) 0 12px, transparent 12px 24px); }
.csi__chip {
display: inline-flex;
align-items: baseline;
gap: 0.5rem;
padding: 0.75rem 1.1rem;
border-radius: var(--r-md, 14px);
background: rgba(10, 10, 12, 0.6);
border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.14);
backdrop-filter: blur(12px);
color: var(--c-text-2, #c4c4c4);
font-size: 0.875rem;
animation: csi-float 6s ease-in-out infinite;
animation-delay: calc(var(--i) * -2s);
}
.csi__chip:nth-child(2) { transform: translateX(2.5rem); }
.csi__chip:nth-child(3) { transform: translateX(-2rem); }
.csi__chip strong { font-size: 1.25rem; color: var(--c-text-1, #fff); font-variant-numeric: tabular-nums; }
@keyframes csi-float { 50% { translate: 0 -8px; } }
@container (max-width: 820px) {
.csi__box { grid-template-columns: 1fr; }
.csi__visual { min-height: 240px; }
}
@media (prefers-reduced-motion: reduce) { .csi__chip { animation: none; } }
</style> ---
/**
* CtaSplitImage — Conversion banner split into copy + CTA and a decorative
* visual panel (CSS gradient mesh with floating stat chips). Optional real image.
*
* @param {string} title - Heading.
* @param {string} text - Supporting copy.
* @param {{label: string, href: string}} primary - Primary CTA.
* @param {{label: string, href: string}} secondary - Secondary CTA.
* @param {string} image - Optional image URL (replaces the CSS visual).
* @param {string} imageAlt - Alt text when an image is used.
* @param {Array<{value: string, label: string}>} chips - Floating stat chips on the visual.
* @param {string} variant - Panel style (mesh|solid|outline).
* @param {string} size - Vertical rhythm (sm|md|lg).
* @param {string} className - Extra CSS class.
*/
interface Props {
title?: string;
text?: string;
primary?: { label: string; href: string };
secondary?: { label: string; href: string };
image?: string;
imageAlt?: string;
chips?: Array<{ value: string; label: string }>;
variant?: 'mesh' | 'solid' | 'outline';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const {
title = 'Ready to ship your best page yet?',
text = 'Join thousands of teams building fast, accessible Astro sites. Free forever for personal projects.',
primary = { label: 'Get started free', href: '#' },
secondary = { label: 'Talk to sales', href: '#' },
image,
imageAlt = '',
chips = [
{ value: '99.9%', label: 'uptime' },
{ value: '< 50ms', label: 'TTFB' },
{ value: '100', label: 'Lighthouse' },
],
variant = 'mesh',
size = 'md',
className = '',
} = Astro.props;
const id = `csi-${Math.random().toString(36).slice(2, 8)}`;
---
<div class="cq-cta-split-image-cq">
<section class={`csi csi--${variant} csi--${size} ${className}`} aria-labelledby={id}>
<div class="csi__box">
<div class="csi__copy">
<h2 id={id} class="csi__title">{title}</h2>
<p class="csi__text">{text}</p>
<div class="csi__actions">
<a class="csi__btn csi__btn--primary" href={primary.href}>{primary.label}</a>
{secondary && <a class="csi__btn csi__btn--ghost" href={secondary.href}>{secondary.label}</a>}
</div>
</div>
<div class="csi__visual">
{image ? (
<img src={image} alt={imageAlt} loading="lazy" decoding="async" />
) : (
<div class="csi__mesh" aria-hidden="true">
{chips.map((c, i) => (
<span class="csi__chip" style={`--i:${i}`}><strong>{c.value}</strong>{c.label}</span>
))}
</div>
)}
</div>
</div>
</section>
</div>
<style>
.cq-cta-split-image-cq { container-type: inline-size; }
.csi {
--csi-py: 5rem;
padding: var(--csi-py) var(--sp-6, 1.5rem);
background: var(--c-bg, #050505);
}
.csi--sm { --csi-py: 3rem; }
.csi--lg { --csi-py: 7rem; }
.csi__box {
max-width: 1180px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr;
border-radius: var(--r-xl, 28px);
overflow: hidden;
border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.08));
background: var(--c-bg-1, #0a0a0a);
}
.csi--outline .csi__box { background: transparent; border-color: rgb(var(--ink-rgb, 255 255 255) / 0.15); }
.csi__copy { padding: clamp(2rem, 6vw, 4rem); display: flex; flex-direction: column; justify-content: center; }
.csi__title { margin: 0 0 1rem; font-size: clamp(1.75rem, 4vw, 2.75rem); font-weight: 800; letter-spacing: -0.03em; line-height: 1.1; color: var(--c-text-1, #fff); text-wrap: balance; }
.csi__text { margin: 0 0 2rem; font-size: 1.0625rem; line-height: 1.6; color: var(--c-text-2, #a3a3a3); max-width: 460px; }
.csi__actions { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.csi__btn { display: inline-flex; align-items: center; min-height: 48px; padding: 0 1.5rem; border-radius: var(--r-md, 12px); font-weight: 650; text-decoration: none; transition: background 0.2s ease, border-color 0.2s ease; }
.csi__btn--primary { background: var(--c-primary-light, #d4a017); color: var(--c-on-primary, #0a0a0a); }
.csi__btn--primary:hover { background: var(--c-accent, #fef3c7); color: var(--c-on-primary, #0a0a0a); }
.csi__btn--ghost { border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.15); color: var(--c-text-1, #fff); }
.csi__btn--ghost:hover { border-color: rgb(var(--ink-rgb, 255 255 255) / 0.32); color: var(--c-text-1, #fff); }
.csi__btn:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 3px; }
.csi__visual { position: relative; min-height: 320px; }
.csi__visual img { width: 100%; height: 100%; object-fit: cover; display: block; }
.csi__mesh {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1rem;
padding: 2rem;
}
.csi--mesh .csi__mesh {
background:
radial-gradient(circle at 20% 20%, color-mix(in srgb, var(--c-primary-light, #d4a017) 55%, transparent), transparent 45%),
radial-gradient(circle at 80% 30%, rgba(144, 133, 233, 0.55), transparent 45%),
radial-gradient(circle at 50% 90%, rgba(57, 135, 229, 0.5), transparent 50%),
#121218;
}
.csi--solid .csi__mesh { background: linear-gradient(135deg, #1a1408, #0c0c10); }
.csi--outline .csi__mesh { background: repeating-linear-gradient(45deg, rgb(var(--ink-rgb, 255 255 255) / 0.03) 0 12px, transparent 12px 24px); }
.csi__chip {
display: inline-flex;
align-items: baseline;
gap: 0.5rem;
padding: 0.75rem 1.1rem;
border-radius: var(--r-md, 14px);
background: rgba(10, 10, 12, 0.6);
border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.14);
backdrop-filter: blur(12px);
color: var(--c-text-2, #c4c4c4);
font-size: 0.875rem;
animation: csi-float 6s ease-in-out infinite;
animation-delay: calc(var(--i) * -2s);
}
.csi__chip:nth-child(2) { transform: translateX(2.5rem); }
.csi__chip:nth-child(3) { transform: translateX(-2rem); }
.csi__chip strong { font-size: 1.25rem; color: var(--c-text-1, #fff); font-variant-numeric: tabular-nums; }
@keyframes csi-float { 50% { translate: 0 -8px; } }
@container (max-width: 820px) {
.csi__box { grid-template-columns: 1fr; }
.csi__visual { min-height: 240px; }
}
@media (prefers-reduced-motion: reduce) { .csi__chip { animation: none; } }
</style>