1 GitHub star = 1 new component.

Star the repo
Call to Action

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.

Usage

Copy and paste this code into your React component:

tsx
import { CtaSplitImage } from 'react-component-kit-ui/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.tsx + CtaSplitImage.css:

CtaSplitImage.tsx
'use client';

import type { CSSProperties } from 'react';
import './CtaSplitImage.css';

/** "a: b; --c: d" โ†’ style object */
function st(css: string | undefined): CSSProperties {
  const o: Record<string, string> = {};
  for (const decl of String(css ?? '').split(';')) {
    const i = decl.indexOf(':');
    if (i < 0) continue;
    const k = decl.slice(0, i).trim();
    if (k) o[k.startsWith('--') ? k : k.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = decl.slice(i + 1).trim();
  }
  return o as CSSProperties;
}

/**
 * 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.
 */
export interface CtaSplitImageProps {
  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;
}

export function CtaSplitImage({
  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 = '',
}: CtaSplitImageProps) {
  const id = `csi-${Math.random().toString(36).slice(2, 8)}`;
  return (
    <>
      <div className="cq-cta-split-image-cq" data-rk-56220c="">
      <section className={`csi csi--${variant} csi--${size} ${className}`} aria-labelledby={id} data-rk-56220c="">
        <div className="csi__box" data-rk-56220c="">
          <div className="csi__copy" data-rk-56220c="">
            <h2 id={id} className="csi__title" data-rk-56220c="">{title}</h2>
            <p className="csi__text" data-rk-56220c="">{text}</p>
            <div className="csi__actions" data-rk-56220c="">
              <a className="csi__btn csi__btn--primary" href={primary.href} data-rk-56220c="">{primary.label}</a>
              {secondary && <a className="csi__btn csi__btn--ghost" href={secondary.href} data-rk-56220c="">{secondary.label}</a>}
            </div>
          </div>
          <div className="csi__visual" data-rk-56220c="">
            {image ? (
              <img src={image} alt={imageAlt} loading="lazy" decoding="async" data-rk-56220c="" />
            ) : (
              <div className="csi__mesh" aria-hidden="true" data-rk-56220c="">
                {chips.map((c, i) => (
                  <span key={i} className="csi__chip" style={st(`--i:${i}`)} data-rk-56220c=""><strong data-rk-56220c="">{c.value}</strong>{c.label}</span>
                ))}
              </div>
            )}
          </div>
        </div>
      </section>
      </div>
    </>
  );
}

export default CtaSplitImage;
CtaSplitImage.css
/* CtaSplitImage โ€” ported from astro-component-kit (sections/CtaSplitImage.astro) */

.cq-cta-split-image-cq:where([data-rk-56220c]) { container-type: inline-size; }
.csi:where([data-rk-56220c]) {
  --csi-py: 5rem;
  padding: var(--csi-py) var(--sp-6, 1.5rem);
  background: var(--c-bg, #050505);
}
.csi--sm:where([data-rk-56220c]) { --csi-py: 3rem; }
.csi--lg:where([data-rk-56220c]) { --csi-py: 7rem; }
.csi__box:where([data-rk-56220c]) {
  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:where([data-rk-56220c]) .csi__box:where([data-rk-56220c]) { background: transparent; border-color: rgb(var(--ink-rgb, 255 255 255) / 0.15); }
.csi__copy:where([data-rk-56220c]) { padding: clamp(2rem, 6vw, 4rem); display: flex; flex-direction: column; justify-content: center; }
.csi__title:where([data-rk-56220c]) { 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:where([data-rk-56220c]) { margin: 0 0 2rem; font-size: 1.0625rem; line-height: 1.6; color: var(--c-text-2, #a3a3a3); max-width: 460px; }
.csi__actions:where([data-rk-56220c]) { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.csi__btn:where([data-rk-56220c]) { 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:where([data-rk-56220c]) { background: var(--c-primary-light, #d4a017); color: var(--c-on-primary, #0a0a0a); }
.csi__btn--primary:hover:where([data-rk-56220c]) { background: var(--c-accent, #fef3c7); color: var(--c-on-primary, #0a0a0a); }
.csi__btn--ghost:where([data-rk-56220c]) { border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.15); color: var(--c-text-1, #fff); }
.csi__btn--ghost:hover:where([data-rk-56220c]) { border-color: rgb(var(--ink-rgb, 255 255 255) / 0.32); color: var(--c-text-1, #fff); }
.csi__btn:focus-visible:where([data-rk-56220c]) { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 3px; }
.csi__visual:where([data-rk-56220c]) { position: relative; min-height: 320px; }
.csi__visual:where([data-rk-56220c]) img:where([data-rk-56220c]) { width: 100%; height: 100%; object-fit: cover; display: block; }
.csi__mesh:where([data-rk-56220c]) {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 1rem;
  padding: 2rem;
}
.csi--mesh:where([data-rk-56220c]) .csi__mesh:where([data-rk-56220c]) {
  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:where([data-rk-56220c]) .csi__mesh:where([data-rk-56220c]) { background: linear-gradient(135deg, #1a1408, #0c0c10); }
.csi--outline:where([data-rk-56220c]) .csi__mesh:where([data-rk-56220c]) { background: repeating-linear-gradient(45deg, rgb(var(--ink-rgb, 255 255 255) / 0.03) 0 12px, transparent 12px 24px); }
.csi__chip:where([data-rk-56220c]) {
  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: cta-split-image-csi-float 6s ease-in-out infinite;
  animation-delay: calc(var(--i) * -2s);
}
.csi__chip:nth-child(2):where([data-rk-56220c]) { transform: translateX(2.5rem); }
.csi__chip:nth-child(3):where([data-rk-56220c]) { transform: translateX(-2rem); }
.csi__chip:where([data-rk-56220c]) strong:where([data-rk-56220c]) { font-size: 1.25rem; color: var(--c-text-1, #fff); font-variant-numeric: tabular-nums; }
@keyframes cta-split-image-csi-float { 50% { translate: 0 -8px; } }
@container (max-width: 820px) {
  .csi__box:where([data-rk-56220c]) { grid-template-columns: 1fr; }
  .csi__visual:where([data-rk-56220c]) { min-height: 240px; }
}
@media (prefers-reduced-motion: reduce) { .csi__chip:where([data-rk-56220c]) { animation: none; } }