Color field hero

Packshot in a framed tile on a full accent color field, with an inverted CTA, fact row and a pinned price tag.

blockcolor-fieldbeveragewellnessanycta
$npx shadcn@latest add @storekit/hero-color-field
hero-color-field

Live and interactive. Switch the brand in the frame header to see the same source under another token file.

Installation

$npx shadcn@latest add @storekit/hero-color-field
$pnpm dlx shadcn@latest add @storekit/hero-color-field
$yarn dlx shadcn@latest add @storekit/hero-color-field
$bunx --bun shadcn@latest add @storekit/hero-color-field

Source

components/storekit/hero/hero-color-field.tsx
94 lines
"use client"

import { motion, useReducedMotion } from "motion/react"
import { cn, money } from "@/components/storekit/lib/cn"
import { useSkBlock } from "@/components/storekit/lib/sk-analytics"

/* Product on a color field. The whole section is the brand accent; copy sits
   in accent-ink so contrast follows the brand pack, and the packshot rides in
   a framed tile with a price tag pinned to its corner. The CTA inverts to
   accent-ink fill so it stays the strongest mark on the field. Stacks with
   the packshot first under lg. */

const EASE = [0.23, 1, 0.32, 1] as const

type Cta = { label: string; href: string }

export function HeroColorField({
  eyebrow = "New flavor",
  headline = "Hydration that tastes like summer.",
  subcopy = "Citrus salt electrolytes with zero sugar. Third-party tested and made in the USA.",
  primaryCta = { label: "Shop Electrolytes", href: "#" },
  secondaryCta = { label: "See lab results", href: "#" },
  product = { title: "Electrolytes", image: "/placeholder.webp", price: 3200 },
  facts = ["Zero sugar", "30 servings", "Ships in 24h"],
  className,
}: {
  eyebrow?: string
  headline?: string
  subcopy?: string
  primaryCta?: Cta
  secondaryCta?: Cta
  product?: { title: string; image: string; price: number }
  facts?: string[]
  className?: string
}) {
  const reduce = useReducedMotion()
  const sk = useSkBlock("hero", "hero-color-field")

  return (
    <section ref={sk.ref} {...sk.attrs} className={cn("overflow-hidden bg-sk-accent text-sk-accent-ink", className)}>
      <div className="mx-auto grid w-full max-w-6xl items-center gap-10 px-4 py-12 sm:px-8 sm:py-16 lg:min-h-[540px] lg:grid-cols-[1.05fr_0.95fr] lg:gap-16">
        <motion.div
          initial={{ opacity: 0, y: reduce ? 0 : 8 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.5, ease: EASE }}
          className="order-2 lg:order-1"
        >
          <p className="font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-accent-ink/75">{eyebrow}</p>
          <h1
            className="mt-4 max-w-xl text-balance font-sk-display font-semibold leading-[1.05] tracking-[-0.02em]"
            style={{ fontSize: "clamp(2.25rem, 5vw, 4rem)" }}
          >
            {headline}
          </h1>
          <p className="mt-5 max-w-md text-[15px] leading-[1.5] text-sk-accent-ink/80">{subcopy}</p>
          <div className="mt-8 flex flex-col gap-3 sm:flex-row">
            <a
              href={primaryCta.href}
              onClick={() => sk.emit("interact", "cta")}
              className="sk-btn inline-flex min-h-12 items-center justify-center py-2.5 text-center leading-tight rounded-sk-btn bg-sk-accent-ink px-7 text-[15px] font-medium text-sk-accent outline-none transition-[opacity,transform] duration-200 ease-sk-out hover:opacity-90 focus-visible:ring-2 focus-visible:ring-sk-accent-ink focus-visible:ring-offset-2 focus-visible:ring-offset-sk-accent active:scale-[0.97]"
            >
              {primaryCta.label}
            </a>
            <a
              href={secondaryCta.href}
              onClick={() => sk.emit("interact", "cta_secondary")}
              className="sk-btn inline-flex min-h-12 items-center justify-center py-2.5 text-center leading-tight rounded-sk-btn border border-sk-accent-ink/35 px-7 text-[15px] font-medium text-sk-accent-ink outline-none transition-[background-color,transform] duration-200 ease-sk-out hover:bg-sk-accent-ink/10 focus-visible:ring-2 focus-visible:ring-sk-accent-ink focus-visible:ring-offset-2 focus-visible:ring-offset-sk-accent active:scale-[0.97]"
            >
              {secondaryCta.label}
            </a>
          </div>
          <ul className="mt-10 flex flex-wrap gap-x-6 gap-y-2 border-t border-sk-accent-ink/20 pt-5">
            {facts.map((f) => (
              <li key={f} className="font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-accent-ink/80">
                {f}
              </li>
            ))}
          </ul>
        </motion.div>

        <div className="relative order-1 mx-auto w-full max-w-[420px] lg:order-2 lg:max-w-none">
          <div className="relative aspect-square overflow-hidden rounded-sk-card border border-sk-accent-ink/15 bg-sk-surface">
            <img src={product.image} alt={product.title} className="absolute inset-0 h-full w-full object-cover" />
          </div>
          <div className="absolute -bottom-4 left-4 flex items-baseline gap-3 rounded-sk-chip border border-sk-line bg-sk-paper px-4 py-2.5 text-sk-ink shadow-[0_24px_60px_-24px_rgb(var(--sk-ink)/0.25)] sm:left-6">
            <span className="text-sm font-medium">{product.title}</span>
            <span className="text-sm font-semibold tabular-nums text-sk-price">{money(product.price)}</span>
          </div>
        </div>
      </div>
    </section>
  )
}

Usage

Import from "@/components/storekit/hero/hero-color-field" and pass your product data. The demo above exercises the full prop surface with sample values.

Color field hero, StoreKit