Product interfaces · Original
Card stack
Layered editorial cards with authored cover art, keyboard navigation, and visible depth.
Live preview
Try it in context
Customize preview
Source delivery
v0.3.0Usage & source
"use client";
import type { CSSProperties } from "react";
import { CardStack } from "./components/stepkit/CardStack";
import "./components/stepkit/styles.css";
import "./components/stepkit/showcase.css";
const themeStyle = {"--stepkit-bg":"#f7f7f8","--stepkit-surface":"#ffffff","--stepkit-surface-muted":"#f0f0f3","--stepkit-fg":"#17181c","--stepkit-muted":"#626572","--stepkit-border":"#e3e4e9","--stepkit-border-strong":"#c8cad3","--stepkit-code":"#f1f1f4","--stepkit-shadow":"0 20px 60px rgba(31, 26, 64, .10)","--stepkit-accent":"#b64e0d","--stepkit-accent-strong":"#8f3b08","--stepkit-accent-soft":"#fff0e6","--stepkit-accent-on-soft":"#8f3b08","--stepkit-accent-text":"#ffffff","--stepkit-error":"#b43145","--stepkit-placeholder":"#626572","--stepkit-radius":"16px","--stepkit-space":"16px","--stepkit-control-height":"44px","--stepkit-font-sans":"Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif"} as CSSProperties;
export default function Example() {
return (
<div className="stepkit-theme" data-stepkit-preset="modern" style={themeStyle}>
<CardStack
defaultActiveIndex={0}
paused={false}
/>
</div>
);
}Dependencies
[email protected] [email protected]
/*
* Stepkit 0.3.0
* Copyright (c) 2026 Vladislav Stepanov
*
* Original Stepkit portions in this release are licensed under the MIT License. Adapted portions identify their upstream licence and attribution in the copied source and in the entry-specific upstream notice files.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
"use client";
import { useId, useState, type KeyboardEvent } from "react";
import "./styles.css";
import "./showcase.css";
export type CardStackArtwork = "cream" | "cobalt" | "coral";
export interface CardStackItem { id: string; eyebrow: string; title: string; body: string; artwork?: CardStackArtwork; mark?: string; }
export interface CardStackProps {
cards?: CardStackItem[];
activeIndex?: number;
defaultActiveIndex?: number;
onActiveIndexChange?: (index: number, card: CardStackItem) => void;
paused?: boolean;
className?: string;
}
const defaultCards: CardStackItem[] = [
{ id: "signal", eyebrow: "Field notes / 01", title: "The shape of a signal", body: "A cover-led stack for ideas that deserve a little more room.", artwork: "cream", mark: "S" },
{ id: "motion", eyebrow: "Field notes / 02", title: "Small moves, felt clearly", body: "A cobalt study in rhythm, focus, and deliberate transitions.", artwork: "cobalt", mark: "M" },
{ id: "orbit", eyebrow: "Field notes / 03", title: "Keep the horizon open", body: "A warm editorial card that gives the next thought somewhere to land.", artwork: "coral", mark: "O" }
];
function Artwork({ card }: { card: CardStackItem }) {
const mark = card.mark ?? card.title.slice(0, 1);
return <svg className="stepkit-card-stack__art" viewBox="0 0 720 250" aria-hidden="true" preserveAspectRatio="none"><path d="M0 182C120 92 181 234 304 136S502 58 720 124V250H0Z" fill="rgba(255,255,255,.12)" /><path d="M0 58C112 124 190 20 314 78s218 44 406-36" fill="none" opacity=".7" stroke="rgba(255,255,255,.64)" strokeWidth="2" /><circle cx="600" cy="54" fill="rgba(255,255,255,.25)" r="58" /><text fontSize="170" x="40" y="190">{mark}</text></svg>;
}
function Arrow({ direction }: { direction: "previous" | "next" }) {
return direction === "next" ? <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14m-6-6 6 6-6 6" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" /></svg> : <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M19 12H5m6-6-6 6 6 6" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" /></svg>;
}
export function CardStack({ cards = defaultCards, activeIndex, defaultActiveIndex = 0, onActiveIndexChange, paused = false, className = "" }: CardStackProps) {
const headingId = useId();
const [internalIndex, setInternalIndex] = useState(Math.max(0, Math.min(Math.max(cards.length - 1, 0), defaultActiveIndex)));
const requestedIndex = activeIndex ?? internalIndex;
const currentIndex = cards.length ? ((requestedIndex % cards.length) + cards.length) % cards.length : 0;
const activeCard = cards[currentIndex];
const change = (nextIndex: number) => {
if (!cards.length) return;
const safeIndex = (nextIndex + cards.length) % cards.length;
if (activeIndex === undefined) setInternalIndex(safeIndex);
onActiveIndexChange?.(safeIndex, cards[safeIndex]);
};
const handleKeyDown = (event: KeyboardEvent<HTMLElement>) => {
if (event.key === "ArrowRight") { event.preventDefault(); change(currentIndex + 1); }
if (event.key === "ArrowLeft") { event.preventDefault(); change(currentIndex - 1); }
};
return <section className={`stepkit stepkit-showcase stepkit-card-stack${className ? ` ${className}` : ""}`} data-paused={paused} aria-labelledby={headingId} tabIndex={0} onKeyDown={handleKeyDown}>
<header className="stepkit-card-stack__header"><div><span className="stepkit-card-stack__eyebrow">Card stack / editorial covers</span><h2 className="stepkit-card-stack__title" id={headingId}>Three ways to hold a thought.</h2></div><div className="stepkit-card-stack__counter" aria-live="polite"><strong>{cards.length ? String(currentIndex + 1).padStart(2, "0") : "00"}</strong><span>/ {String(cards.length).padStart(2, "0")}</span></div></header>
<div className="stepkit-card-stack__stage" aria-label="Card stack">
{cards.map((card, index) => { const relative = cards.length ? (index - currentIndex + cards.length) % cards.length : 3; const layer = relative === 0 ? "0" : relative === 1 ? "1" : relative === 2 ? "2" : "hidden"; return <article className="stepkit-card-stack__card" data-art={card.artwork ?? "cream"} data-stack-layer={layer} aria-hidden={relative !== 0} key={card.id}><div className="stepkit-card-stack__cover"><Artwork card={card} /></div><div className="stepkit-card-stack__meta"><div><small>{card.eyebrow}</small><h3>{card.title}</h3><p className="stepkit-card-stack__body">{card.body}</p></div></div></article>; })}
</div>
<div className="stepkit-card-stack__controls"><button className="stepkit-card-stack__control" type="button" aria-label="Previous card" onClick={() => change(currentIndex - 1)} disabled={cards.length < 2}><Arrow direction="previous" /></button><button className="stepkit-card-stack__control" type="button" aria-label="Next card" onClick={() => change(currentIndex + 1)} disabled={cards.length < 2}><Arrow direction="next" /></button></div>
<span className="stepkit-sr-only">Use left and right arrow keys to move through cards.</span>
{activeCard ? <span className="stepkit-sr-only" aria-live="polite">Showing {activeCard.title}</span> : null}
</section>;
}
export default CardStack;
components/CardStack.tsxcomponents/showcase.csscomponents/styles.cssInstall
Use the registry command or download the complete source archive.
npx [email protected] add https://vstepanov.com/stepkit/r/card-stack.jsonIntegration prompt
Install Stepkit Card stack 0.3.0 from the registry and preserve the included MIT notice. npx [email protected] add https://vstepanov.com/stepkit/v0.3.0/r/card-stack.json Install the tested runtime dependencies exactly: npm install [email protected] [email protected] Keep the component source, shared styles, NOTICE.md, and provenance files together. Apply the theme tokens on a wrapper rather than passing theme keys as component props. Usage: ```tsx "use client"; import type { CSSProperties } from "react"; import { CardStack } from "./components/stepkit/CardStack"; import "./components/stepkit/styles.css"; import "./components/stepkit/showcase.css"; const themeStyle = {"--stepkit-bg":"#f7f7f8","--stepkit-surface":"#ffffff","--stepkit-surface-muted":"#f0f0f3","--stepkit-fg":"#17181c","--stepkit-muted":"#626572","--stepkit-border":"#e3e4e9","--stepkit-border-strong":"#c8cad3","--stepkit-code":"#f1f1f4","--stepkit-shadow":"0 20px 60px rgba(31, 26, 64, .10)","--stepkit-accent":"#6950df","--stepkit-accent-strong":"#5137c7","--stepkit-accent-soft":"#eeeafd","--stepkit-accent-on-soft":"#5137c7","--stepkit-accent-text":"#ffffff","--stepkit-error":"#b43145","--stepkit-placeholder":"#626572","--stepkit-radius":"16px","--stepkit-space":"16px","--stepkit-control-height":"44px","--stepkit-font-sans":"Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif"} as CSSProperties; export default function Example() { return ( <div className="stepkit-theme" data-stepkit-preset="modern" style={themeStyle}> <CardStack defaultActiveIndex={0} paused={false} /> </div> ); } ``` Selected bounded settings: {"defaultActiveIndex":0,"paused":false}. Selected theme: {"mode":"light","accent":"violet","radius":"soft","density":"comfortable","preset":"modern"}.
Props
| Name | Type | Default | Description |
|---|---|---|---|
cards | CardStackItem[] | default cards | Editorial card records and artwork choices. |
activeIndex | number | undefined | Controlled active card index. |
onActiveIndexChange | (index: number, card: CardStackItem) => void | undefined | Called after keyboard or button navigation. |
paused | boolean | false | Disables stack transitions. |
Accessibility & limits
- Previous and next controls have explicit labels.
- Arrow keys move through the active card while the count and title are announced.
- Layered cards expose only the active card to assistive technology.
Limitations
- The component intentionally shows up to three layers; excess cards remain available through navigation.
Provenance
Original source under the MIT licence.
Original Stepkit implementation.
Required notices: NOTICE.md, PROVENANCE-stepkit-card-stack.md