1 GitHub star = 1 new component.

Star the repo
E-commerce

Shop Values Marquee

Brand promise band: oversized italic marquee followed by numbered value columns.

marqueevaluesecommercebrand
Live Preview
  1. Honest materials

    Linen, wool, organic cotton and stoneware โ€” traceable to the mill and the kiln.

  2. Made in small runs

    Each piece is produced in batches of a few hundred, never mass-manufactured.

  3. Kept for years

    Free repairs on everything we make, and a buy-back for pieces you have outgrown.

Usage

Copy and paste this code into your React component:

tsx
import { ShopValuesMarquee } from 'react-component-kit-ui/sections';

<ShopValuesMarquee words={['Made slowly', 'Natural fibres']} values={[{ title: 'Honest materials', text: 'Traceable to the mill.' }]} />

Manual Installation

If you are not using the npm package, create a new file and paste the following code: src/components/sections/ShopValuesMarquee.tsx + ShopValuesMarquee.css:

ShopValuesMarquee.tsx
'use client';

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

/** class:list โ†’ className */
function cx(...args: unknown[]): string {
  const out: string[] = [];
  const walk = (v: unknown) => {
    if (!v) return;
    if (typeof v === 'string' || typeof v === 'number') out.push(String(v));
    else if (Array.isArray(v)) v.forEach(walk);
    else if (typeof v === 'object') for (const [k, on] of Object.entries(v as Record<string, unknown>)) if (on) out.push(k);
  };
  args.forEach(walk);
  return out.join(' ');
}

/** "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;
}

/**
 * ShopValuesMarquee โ€” Brand promise band: an oversized italic marquee followed by numbered
 * value columns (materials, making, after-careโ€ฆ).
 *
 * @param {string[]} words - Marquee words. Empty hides the marquee.
 * @param {Array<{title: string, text: string}>} values - 2โ€“4 value columns.
 * @param {number} speed - Marquee loop duration in seconds. Default 36.
 * @param {string} variant - Style (plain|inverted). Default 'plain'.
 * @param {string} size - Marquee scale (sm|md|lg). Default 'md'.
 * @param {string} className - Extra class.
 *
 * Accessibility: the marquee is decorative (aria-hidden) and pauses on hover and with reduced
 * motion; values are a real list with headings.
 */
export interface ShopValuesMarqueeProps {
  words?: string[];
  values?: Array<{ title: string; text: string }>;
  speed?: number;
  variant?: 'plain' | 'inverted';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

export function ShopValuesMarquee({
  words = ['Made slowly', 'Natural fibres', 'Small workshops', 'Repaired for life'],
  values = [
    { title: 'Honest materials', text: 'Linen, wool, organic cotton and stoneware โ€” traceable to the mill and the kiln.' },
    { title: 'Made in small runs', text: 'Each piece is produced in batches of a few hundred, never mass-manufactured.' },
    { title: 'Kept for years', text: 'Free repairs on everything we make, and a buy-back for pieces you have outgrown.' },
  ],
  speed = 36,
  variant = 'plain',
  size = 'md',
  className = '',
}: ShopValuesMarqueeProps) {
  return (
    <>
      <section className={cx(['svm', `svm--${variant}`, `svm--${size}`, className])} style={st(`--svm-speed: ${speed}s`)} data-rk-6a922c="">
        {words.length > 0 && (
          <div className="svm__marquee" aria-hidden="true" data-rk-6a922c="">
            <div className="svm__track" data-rk-6a922c="">
              {[0, 1].map(() => words.map((w, __i: number) => <span key={__i} data-rk-6a922c="">{w}<i data-rk-6a922c="">โœบ</i></span>))}
            </div>
          </div>
        )}
        {values.length > 0 && (
          <ol className="svm__values" data-rk-6a922c="">
            {values.map((v, i) => (
              <li key={i} data-rk-6a922c="">
                <span className="svm__num" aria-hidden="true" data-rk-6a922c="">{String(i + 1).padStart(2, '0')}</span>
                <h3 data-rk-6a922c="">{v.title}</h3>
                <p data-rk-6a922c="">{v.text}</p>
              </li>
            ))}
          </ol>
        )}
      </section>
    </>
  );
}

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

.svm:where([data-rk-6a922c]) { container-type: inline-size; --svm-fs: clamp(3rem, 9vw, 7.5rem); color: var(--c-text-1, #fff); overflow: hidden; }
.svm--sm:where([data-rk-6a922c]) { --svm-fs: clamp(2rem, 6vw, 4.5rem); }
.svm--lg:where([data-rk-6a922c]) { --svm-fs: clamp(4rem, 12vw, 10rem); }
.svm--inverted:where([data-rk-6a922c]) { background: var(--c-text-1, #fff); color: var(--c-bg, #050505); }
.svm__marquee:where([data-rk-6a922c]) { border-block: 1px solid currentColor; overflow: hidden; }
.svm__track:where([data-rk-6a922c]) { display: flex; width: max-content; animation: shop-values-marquee-svm-run var(--svm-speed) linear infinite; }
.svm:hover:where([data-rk-6a922c]) .svm__track:where([data-rk-6a922c]) { animation-play-state: paused; }
.svm__track:where([data-rk-6a922c]) span:where([data-rk-6a922c]) { display: inline-flex; align-items: center; gap: 0.35em; padding: 0.1em 0 0.15em 0.35em; font-family: var(--font-display, Georgia, 'Times New Roman', serif); font-size: var(--svm-fs); font-style: italic; letter-spacing: -0.04em; line-height: 1.05; white-space: nowrap; }
.svm__track:where([data-rk-6a922c]) i:where([data-rk-6a922c]) { font-style: normal; font-size: 0.4em; color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
@keyframes shop-values-marquee-svm-run { to { transform: translateX(-50%); } }

.svm__values:where([data-rk-6a922c]) {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  margin: 0 auto;
  max-width: 1440px;
  padding: clamp(2rem, 5vw, 4rem) clamp(1rem, 3vw, 2.5rem);
  list-style: none;
  gap: 0;
}
.svm__values:where([data-rk-6a922c]) li:where([data-rk-6a922c]) { padding: 0 var(--sp-6, 1.5rem) var(--sp-4, 1rem); border-left: 1px solid var(--c-border, rgba(255, 255, 255, 0.12)); }
.svm--inverted:where([data-rk-6a922c]) .svm__values:where([data-rk-6a922c]) li:where([data-rk-6a922c]) { border-left-color: rgb(var(--paper-rgb, 5 5 5) / 0.2); }
.svm__num:where([data-rk-6a922c]) { font-size: 0.75rem; font-variant-numeric: tabular-nums; color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
.svm__values:where([data-rk-6a922c]) h3:where([data-rk-6a922c]) { margin: var(--sp-3, 0.75rem) 0 var(--sp-2, 0.5rem); font-family: var(--font-display, Georgia, serif); font-size: 1.5rem; font-weight: 400; letter-spacing: -0.02em; }
.svm__values:where([data-rk-6a922c]) p:where([data-rk-6a922c]) { margin: 0; max-width: 22rem; font-size: 0.92rem; line-height: 1.65; opacity: 0.75; }

@container (max-width: 640px) {
  .svm__values:where([data-rk-6a922c]) li:where([data-rk-6a922c]) { padding: var(--sp-4, 1rem) 0; border-left: 0; border-top: 1px solid var(--c-border, rgba(255, 255, 255, 0.12)); }
}
@media (prefers-reduced-motion: reduce) { .svm__track:where([data-rk-6a922c]) { animation: none; } }