Three ways to get Split image CTA into one of your own projects, all pre-filled with this component's name and library page.
Start here
Component reference
Paste it into a request you write yourself, e.g. “I want to add [paste] to Section 3 of my homepage.” It names the component, links its page, and fixes what must not change — the design build and its effects — while leaving the branding and content open.
Read it first
“Split image CTA” from my Astro Component Library:
https://astro.baysixmedia.com/components/split-image-cta/
Use the existing library component as the implementation source. Preserve its design structure, layout, proportions, spacing system, responsive behavior, visual effects, animations, transitions, hover states, interactions, and accessibility.
The copy, colors, typography, images, and other content may be adapted to match the destination website. Do not redesign, simplify, or reinterpret the component’s underlying design build or effects.
ChatGPT brief
Ask ChatGPT to plan a more detailed customization. Use it when you are still deciding which project the component belongs in, where on the page it sits, and what needs changing. It answers with a Claude Code prompt to run.
Read it first
Help me add this component to one of my Astro websites:
Component: Split image CTA
Library page: https://astro.baysixmedia.com/components/split-image-cta
I want it added to:
[ENTER PROJECT, PAGE, OR ROUTE]
Place it:
[ENTER LOCATION OR SELECTOR]
Customize it for:
[ENTER BUSINESS, CONTENT, COLORS, IMAGES, OR OTHER CHANGES]
Create a complete Claude Code implementation prompt. Tell Claude to read the component's source, README, documented props, dependencies, and required assets from the library page; copy everything locally into the target project; preserve its responsive behavior, accessibility, interactions, reduced-motion behavior, and multiple-instance safety; test it at desktop, tablet, and mobile widths; run the target project's available check and build commands; report all changed files and differences from the library version; and avoid committing or deploying.
Placement prompt
Send the implementation request straight to Claude Code. Use it when you already know which project, which page and where on that page it goes. Paste it into Claude Code inside the destination project and it does the work.
Read it first
Add Split image CTA from my Astro Component Library to this Astro project.
Library page:
https://astro.baysixmedia.com/components/split-image-cta
First read the component source, README, documented props, dependencies, and required assets from the library page. Copy the component and every required companion asset locally into this project. Do not import files from the deployed library at runtime.
Place it in:
[ENTER PAGE, FILE, OR ROUTE]
Position:
[ENTER WHERE IT SHOULD APPEAR]
Adapt its demo content, links, colors, typography, and images to this project through documented props where possible. Preserve its responsive behavior, accessibility, interaction logic, reduced-motion behavior, and support for multiple instances. Avoid changing unrelated project code.
Test it at desktop, tablet, and mobile widths. Run the project's available check and build commands. Report the files changed and any differences from the library version. Do not commit or deploy.
src/components/library/SplitImageCta.astro
This component is not one file on its own
assetsCopy these across too — they are referenced by URL, so nothing in the source above points at them: public/split-image-cta/workspace-1200.webp. Leave them behind and those URLs resolve to nothing on the other end. What each component does without them is in its README.
---
/**
* SplitImageCta — a call to action laid out as one rounded card split between
* copy and a full-bleed image.
*
* One self-contained file. No imports, no global stylesheet, no npm packages,
* no script: drop it into any Astro project and render it.
*
* The breakpoints are **container queries**, not viewport ones, so the card
* decides its own layout from the width it is actually given. Dropped into a
* narrow sidebar it stacks; given a full-width page it splits — without the
* host having to tell it which.
*
* Measured behaviour:
* card 18px radius, hairline border, muted ground, overflow clipped
* split two equal columns at a container width of 672px and up
* stacking below that the image goes on top and the copy beneath it
* copy pad 24px stacked, 40px split, 64px from 768px up
* cta 44px tall pill, recolours on hover, ring on focus
*/
type HeadingLevel = "h1" | "h2" | "h3";
interface Props {
/** Small pill above the heading. Empty string hides it. */
eyebrow?: string;
/** The offer. Empty string hides it. */
heading?: string;
/** Paragraph under the heading. Empty string hides it. */
description?: string;
/** Text on the button. Empty string hides the button entirely. */
ctaLabel?: string;
/**
* Where the button goes. Without it the label renders as plain text
* instead of a button that looks live and does nothing.
*/
ctaHref?: string;
/** Open the call to action in a new tab, with the matching rel. */
ctaExternal?: boolean;
/** Image filling the other half. Omit for the drawn placeholder. */
image?: string;
/**
* Alternative text. Leave it out — or pass "" — for an image that carries
* no information the copy does not already give, which is the usual case
* for a decorative CTA illustration.
*/
imageAlt?: string;
/** `object-position` for the image, for choosing what survives the crop. */
imagePosition?: string;
/** Heading level, for pages where h2 is wrong. */
headingLevel?: HeadingLevel;
/** Put the image on the left and the copy on the right, when split. */
reverse?: boolean;
/** Card background. */
background?: string;
/** Heading and pill colour, and the button's ground. */
accent?: string;
/** Body text colour. The muted tone is derived from it. */
ink?: string;
/** Corner radius of the card. */
radius?: string;
/** Caps the card width and centres it. */
maxWidth?: string;
class?: string;
id?: string;
}
const {
eyebrow = "Start free",
heading = "Ship the next page without rebuilding the last one",
description =
"Every part in this catalog arrives with its own demo, its own README and no dependency on the project it came from. Copy one file, read what it assumes, and move on.",
ctaLabel = "Browse the catalog",
/*
* No default destination on purpose. A placeholder like "#catalog" would
* render a real <a> pointing at an anchor that does not exist in the host
* page — exactly the dead link the guard below is meant to prevent. Left
* undefined, a caller who forgets to pass one gets the visibly inert
* label instead of a button that silently goes nowhere.
*/
ctaHref,
ctaExternal = false,
image,
imageAlt = "",
imagePosition = "center",
headingLevel = "h2",
reverse = false,
background = "#f4f4f7",
accent = "#4a35c4",
ink = "#16161d",
radius = "18px",
maxWidth = "80rem",
class: className,
id,
} = Astro.props;
const Heading: HeadingLevel = headingLevel;
/*
* A destination only counts if it is a non-empty string. Anything else — an
* omitted prop, an empty string, a stray "#" — renders the label as text, so
* nothing here can look like a button while doing nothing.
*/
const trimmed = ctaHref?.trim();
const hasDestination = !!trimmed && trimmed !== "#";
---
<section
class:list={["cta", className]}
id={id}
data-split-cta
data-reverse={reverse ? "" : undefined}
style={`--sp-bg:${background};--sp-accent:${accent};--sp-ink:${ink};--sp-radius:${radius};--sp-max:${maxWidth};--sp-image-pos:${imagePosition};`}
>
<div class="card">
<div class="grid">
<div class="copy">
{eyebrow && <p class="eyebrow">{eyebrow}</p>}
{heading && <Heading class="heading">{heading}</Heading>}
{description && <p class="description">{description}</p>}
{
ctaLabel &&
(hasDestination ? (
<p class="actions">
<a
class="button"
href={ctaHref}
target={ctaExternal ? "_blank" : undefined}
rel={ctaExternal ? "noopener noreferrer" : undefined}
>
{ctaLabel}
</a>
</p>
) : (
<p class="actions">
<span class="button button-static">{ctaLabel}</span>
</p>
))
}
</div>
<div class="media">
{
image ? (
<img
class="shot"
src={image}
alt={imageAlt}
loading="lazy"
decoding="async"
/>
) : (
<div class="placeholder" aria-hidden="true" />
)
}
</div>
</div>
</div>
</section>
<style>
.cta {
--sp-muted: color-mix(in srgb, var(--sp-ink) 62%, transparent);
--sp-line: color-mix(in srgb, var(--sp-ink) 12%, transparent);
--sp-pill-bg: color-mix(in srgb, var(--sp-accent) 12%, transparent);
--sp-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
box-sizing: border-box;
display: block;
padding: clamp(48px, 7vw, 96px) 16px;
font-family: var(--sp-font);
/* The card sizes itself from this box, not from the viewport. */
container-type: inline-size;
}
.cta :where(*, *::before, *::after) {
box-sizing: border-box;
}
.card {
max-width: var(--sp-max);
margin-inline: auto;
overflow: hidden;
border: 1px solid var(--sp-line);
border-radius: var(--sp-radius);
background: var(--sp-bg);
color: var(--sp-ink);
}
.grid {
display: grid;
grid-template-columns: 1fr;
}
/*
* Stacked, the image leads: it is the thing that identifies the offer at a
* glance, and a reader on a phone meets it before the paragraph.
*/
.copy {
order: 2;
padding: 24px;
}
.media {
order: 1;
position: relative;
min-height: 12rem;
}
/* ---------- copy ---------- */
.eyebrow {
display: inline-block;
margin: 0 0 16px;
padding: 6px 16px;
border-radius: 999px;
background: var(--sp-pill-bg);
color: var(--sp-accent);
font-size: 0.875rem;
font-weight: 500;
line-height: 1.43;
}
.heading {
margin: 0 0 16px;
font-size: 1.875rem;
line-height: 1.1;
font-weight: 600;
letter-spacing: -0.025em;
text-wrap: balance;
}
.description {
margin: 0 0 32px;
max-width: 46ch;
font-size: 1rem;
line-height: 1.6;
color: var(--sp-muted);
text-wrap: pretty;
}
.actions {
margin: 0;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
padding: 0 20px;
border-radius: 8px;
background: var(--sp-accent);
color: #fff;
font-size: 1rem;
font-weight: 500;
line-height: 1.5;
text-decoration: none;
transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
}
/*
* The label with no destination. It is a <span>, so it is already out of
* the tab order and unclickable; this makes that legible rather than
* leaving something that looks like a working button.
*/
.button-static {
background: color-mix(in srgb, var(--sp-ink) 12%, transparent);
color: var(--sp-muted);
cursor: default;
transition: none;
}
.button:focus-visible {
outline: 3px solid var(--sp-accent);
outline-offset: 3px;
}
/* ---------- media ---------- */
.shot {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
object-position: var(--sp-image-pos);
}
/* Drawn stand-in, so the component has nothing to download by default. */
.placeholder {
width: 100%;
height: 100%;
min-height: 12rem;
background:
radial-gradient(
ellipse 70% 60% at 30% 30%,
color-mix(in srgb, var(--sp-accent) 32%, transparent) 0%,
transparent 70%
),
repeating-linear-gradient(
135deg,
color-mix(in srgb, var(--sp-ink) 5%, transparent) 0 12px,
transparent 12px 24px
),
color-mix(in srgb, var(--sp-ink) 8%, var(--sp-bg));
}
/* ---------- container breakpoints ---------- */
@container (min-width: 672px) {
.grid {
grid-template-columns: 1fr 1fr;
align-items: stretch;
}
/* Split, the copy leads and the image sits beside it. */
.copy {
order: 1;
align-self: center;
padding: 40px;
}
.media {
order: 2;
min-height: 0;
}
/* `reverse` only means anything once there are two columns to swap. */
.cta[data-reverse] .copy {
order: 2;
}
.cta[data-reverse] .media {
order: 1;
}
.heading {
font-size: 2.25rem;
}
}
@container (min-width: 768px) {
.copy {
padding: 64px;
}
}
@container (min-width: 896px) {
.heading {
font-size: 3rem;
line-height: 1;
}
.description {
font-size: 1.125rem;
}
}
/* Hover is a pointer affordance. On a touch screen :hover sticks after the
finger lifts, which would strand the button in its hover colour. */
@media (hover: hover) and (pointer: fine) {
a.button:hover {
background: color-mix(in srgb, var(--sp-accent) 88%, #fff);
}
}
@media (prefers-reduced-motion: reduce) {
.button {
transition: none;
}
}
</style>