react-carousel-latest
Version:
A headless, accessible, tree-shakeable React carousel with a compound-component API (and a backwards-compatible CardSlider preset).
588 lines (536 loc) • 14.6 kB
CSS
/*
* Preset slider styles (CardSlider + Card + Blobs, ImageSlider, SlicerSlider).
*
* Import alongside the base stylesheet:
* import "react-carousel-latest/styles.css";
* import "react-carousel-latest/presets.css";
*
* Or pull both in with a single import:
* import "react-carousel-latest/bundle.css";
*
* Framework-free CSS — no Tailwind. Cards read their palette from the
* --rc-from / --rc-to custom properties set inline by <Card>, so a single set
* of colours drives every design variant.
*/
.rc-cardslider {
/* Card row height — arrows are centred on THIS, not on the container's full
height, so host layouts that stretch the slider can't push them off. */
--rc-cs-card-h: 180px;
--rc-cs-pad-top: 1.5rem;
position: relative;
width: 100%;
overflow: hidden;
box-sizing: border-box;
}
.rc-cardslider__overlay {
pointer-events: none;
position: absolute;
top: 0;
z-index: 10;
height: 100%;
width: 4rem;
}
.rc-cardslider__overlay--left {
left: 0;
background: linear-gradient(to right, rgba(255, 255, 255, 0.9), transparent);
}
.rc-cardslider__overlay--right {
right: 0;
background: linear-gradient(to left, rgba(255, 255, 255, 0.9), transparent);
}
.rc-cardslider__arrow {
position: absolute;
/* Anchor to the card row's vertical centre (top padding + half a card),
not 50% of the container — robust no matter how tall the host makes it. */
top: calc(var(--rc-cs-pad-top, 1.5rem) + var(--rc-cs-card-h, 180px) / 2);
z-index: 15;
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 2.75rem;
height: 2.75rem;
color: #111827;
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 9999px;
opacity: 0.5;
cursor: pointer;
backdrop-filter: blur(8px);
box-shadow: 0 10px 20px -8px rgba(0, 0, 0, 0.4);
transform: translateY(-50%);
transition: opacity 150ms ease;
}
.rc-cardslider__arrow:hover {
opacity: 1;
}
.rc-cardslider__arrow--left {
left: 0.75rem;
}
.rc-cardslider__arrow--right {
right: 0.75rem;
}
.rc-cardslider__track {
display: flex;
gap: 1.5rem;
box-sizing: border-box;
/* No height:100% — the row is sized by the cards, so a host-imposed height
on the slider can never stretch the track and shift the cards. */
padding: var(--rc-cs-pad-top, 1.5rem) 3.5rem 2rem;
overflow-x: auto;
scroll-behavior: smooth;
scrollbar-width: none;
}
.rc-cardslider__track::-webkit-scrollbar {
display: none;
}
.rc-cardslider__item {
/* Don't let flex shrink the cards — keep them full size and let the row scroll. */
flex: 0 0 auto;
}
.rc-cardslider__item a {
display: block;
text-decoration: none;
}
/* ============================ Card ============================ */
.rc-card {
/* Light text + accents by default (suited to the colourful variants). */
--rc-blob-fill: rgba(255, 255, 255, 0.9);
--rc-card-fg: #ffffff;
--rc-card-muted: rgba(255, 255, 255, 0.85);
--rc-card-tag-bg: rgba(0, 0, 0, 0.2);
--rc-card-tag-fg: #ffffff;
--rc-card-track: rgba(255, 255, 255, 0.3);
--rc-card-fill: #ffffff;
position: relative;
box-sizing: border-box;
height: var(--rc-cs-card-h, 180px);
width: 350px;
padding: 1.25rem;
overflow: hidden;
border-radius: 1rem;
box-shadow: 0 10px 20px -8px rgba(0, 0, 0, 0.25);
cursor: pointer;
/* Self-contained typography + alignment so the card looks identical in any
app, regardless of the host's global font or text-align (e.g. CRA/Vite
templates default to text-align:center). The named fonts are used when
present (e.g. the docs site) and fall back to system fonts everywhere else. */
text-align: left;
font-family: "Hanken Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.4;
}
.rc-card__blob {
position: absolute;
height: 10rem;
width: 12.5rem;
pointer-events: none;
}
.rc-card__tag {
position: relative;
z-index: 1;
display: inline-block;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
font-weight: 600;
color: var(--rc-card-tag-fg);
background: var(--rc-card-tag-bg);
border-radius: 0.5rem;
}
.rc-card__title {
position: relative;
z-index: 1;
margin: 0.75rem 0 0;
/* Serif display face; falls back to a system serif when Fraunces isn't loaded. */
font-family: "Fraunces", ui-serif, Georgia, "Times New Roman", serif;
font-size: 1.25rem;
font-weight: 700;
color: var(--rc-card-fg);
}
.rc-card__progress {
position: relative;
z-index: 1;
margin-top: 1rem;
height: 0.25rem;
width: 100%;
background: var(--rc-card-track);
border-radius: 9999px;
overflow: hidden;
}
.rc-card__progress-fill {
height: 100%;
width: 90%;
background: var(--rc-card-fill);
border-radius: 9999px;
}
.rc-card__desc {
position: relative;
z-index: 1;
margin: 0.75rem 0 0;
font-size: 0.875rem;
font-weight: 600;
color: var(--rc-card-muted);
}
.rc-card__button {
position: absolute;
top: 1rem;
right: 1rem;
z-index: 1;
display: inline-flex;
padding: 0.5rem;
color: var(--rc-card-fg);
background: rgba(255, 255, 255, 0.25);
border: none;
border-radius: 9999px;
cursor: pointer;
}
/* ------------------------- Variants ------------------------- */
/* Gradient (default) — the original look. */
.rc-card--gradient {
background: linear-gradient(135deg, var(--rc-from), var(--rc-to));
}
/* Glass — frosted film layered over the gradient. */
.rc-card--glass {
background:
linear-gradient(160deg, rgba(255, 255, 255, 0.28), rgba(255, 255, 255, 0.04)),
linear-gradient(135deg, var(--rc-from), var(--rc-to));
border: 1px solid rgba(255, 255, 255, 0.35);
backdrop-filter: blur(3px);
}
.rc-card--glass .rc-card__tag {
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(6px);
}
/* Solid — flat single colour. */
.rc-card--solid {
background: var(--rc-from);
}
/* Outline — light card, coloured border + accents, dark text. */
.rc-card--outline {
--rc-blob-fill: rgba(17, 24, 39, 0.05);
--rc-card-fg: #111827;
--rc-card-muted: #4b5563;
--rc-card-tag-bg: var(--rc-from);
--rc-card-tag-fg: #ffffff;
--rc-card-track: rgba(17, 24, 39, 0.1);
--rc-card-fill: linear-gradient(90deg, var(--rc-from), var(--rc-to));
background: #ffffff;
border: 2px solid var(--rc-from);
}
.rc-card--outline .rc-card__title {
color: var(--rc-from);
}
.rc-card--outline .rc-card__button {
color: var(--rc-from);
background: rgba(17, 24, 39, 0.06);
}
/* Dark — deep background, neon accent from the palette. */
.rc-card--dark {
--rc-blob-fill: rgba(255, 255, 255, 0.06);
--rc-card-fg: #f8fafc;
--rc-card-muted: rgba(248, 250, 252, 0.7);
--rc-card-tag-bg: rgba(255, 255, 255, 0.08);
--rc-card-tag-fg: var(--rc-to);
--rc-card-track: rgba(255, 255, 255, 0.12);
--rc-card-fill: linear-gradient(90deg, var(--rc-from), var(--rc-to));
background: #0f172a;
border: 1px solid var(--rc-from);
box-shadow: 0 0 28px -8px var(--rc-from);
}
.rc-card--dark .rc-card__title {
color: var(--rc-to);
}
.rc-card--dark .rc-card__button {
background: rgba(255, 255, 255, 0.1);
}
/* Minimal — clean white card, gradient only as a subtle accent. */
.rc-card--minimal {
--rc-blob-fill: rgba(17, 24, 39, 0.04);
--rc-card-fg: #111827;
--rc-card-muted: #6b7280;
--rc-card-tag-bg: #f3f4f6;
--rc-card-tag-fg: #374151;
--rc-card-track: #f3f4f6;
--rc-card-fill: linear-gradient(90deg, var(--rc-from), var(--rc-to));
background: #ffffff;
border: 1px solid #e5e7eb;
box-shadow: 0 8px 20px -10px rgba(0, 0, 0, 0.2);
}
.rc-card--minimal .rc-card__button {
color: var(--rc-from);
background: #f3f4f6;
}
/* ===================== ImageSlider preset ===================== */
.rc-image {
position: relative;
border-radius: 16px;
overflow: hidden;
background: #000;
/* thin, chrome-free arrows over the photo */
--rc-button-bg: transparent;
--rc-button-bg-hover: rgba(255, 255, 255, 0.18);
--rc-button-color: #fff;
--rc-button-size: 3rem;
}
.rc-image .rc-button {
font-size: 1.6rem;
filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.55));
}
.rc-image__link {
display: block;
width: 100%;
}
.rc-image__slide {
position: relative;
width: 100%;
height: var(--rc-image-height, 460px);
display: flex;
align-items: center;
justify-content: center;
background-size: cover;
background-position: center;
}
.rc-image__caption {
padding: 0 1rem;
color: #fff;
font-size: clamp(1.5rem, 4vw, 2.5rem);
font-weight: 800;
text-align: center;
text-shadow: 0 2px 18px rgba(0, 0, 0, 0.55);
}
.rc-image .rc-dots {
position: absolute;
left: 0;
right: 0;
bottom: 0.9rem;
margin: 0;
z-index: 3;
}
.rc-image .rc-dot {
background: rgba(255, 255, 255, 0.45);
}
.rc-image .rc-dot--active {
background: #fff;
}
/* ===================== SlicerSlider preset ===================== */
.rc-slicer {
position: relative;
height: var(--rc-image-height, 460px);
border-radius: 16px;
overflow: hidden;
background: #000;
outline: none;
--rc-button-bg: transparent;
--rc-button-bg-hover: rgba(255, 255, 255, 0.18);
--rc-button-color: #fff;
--rc-button-size: 3rem;
}
.rc-slicer:focus-visible {
outline: 2px solid var(--rc-accent, #fff);
outline-offset: 2px;
}
.rc-slicer .rc-button {
font-size: 1.6rem;
filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.55));
}
.rc-slicer__viewport {
position: absolute;
inset: 0;
touch-action: pan-y;
}
.rc-slicer__slide {
position: absolute;
inset: 0;
z-index: 0;
}
.rc-slicer__slide[data-active] {
z-index: 1;
}
/*
* Each strip is one horizontal band of the image. The image is sized to the
* full slide box so every strip shares the same coordinates and the bands line
* up; background-position-y selects the band for strip k.
*/
.rc-slicer__strip {
position: absolute;
left: 0;
width: 100%;
height: calc(100% / var(--rc-slices, 6));
top: calc(var(--k) * 100% / var(--rc-slices, 6));
background-repeat: no-repeat;
background-size: 100% var(--rc-image-height, 460px);
background-position: 0 calc(var(--k) / (var(--rc-slices, 6) - 1) * 100%);
transform: translateX(-115%);
transition: transform 600ms cubic-bezier(0.7, 0, 0.3, 1);
transition-delay: calc(var(--k) * 70ms);
will-change: transform;
}
/* alternate strips enter from the opposite side for a woven wipe */
.rc-slicer__strip:nth-child(even) {
transform: translateX(115%);
}
.rc-slicer__slide[data-active] .rc-slicer__strip {
transform: translateX(0);
}
.rc-slicer__caption {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
padding: 0 1rem;
color: #fff;
font-size: clamp(1.5rem, 4vw, 2.5rem);
font-weight: 800;
text-align: center;
text-shadow: 0 2px 18px rgba(0, 0, 0, 0.55);
pointer-events: none;
transition: opacity 250ms ease;
}
.rc-slicer__slide:not([data-active]) .rc-slicer__caption {
opacity: 0;
}
.rc-slicer .rc-dots {
position: absolute;
left: 0;
right: 0;
bottom: 0.9rem;
margin: 0;
z-index: 3;
}
.rc-slicer .rc-dot {
background: rgba(255, 255, 255, 0.45);
}
.rc-slicer .rc-dot--active {
background: #fff;
}
@media (prefers-reduced-motion: reduce) {
.rc-slicer__strip {
transition-duration: 1ms;
transition-delay: 0ms;
}
}
/* ===================== CubeSlider preset ===================== */
.rc-cube {
position: relative;
height: var(--rc-image-height, 460px);
border-radius: 16px;
background: #000;
outline: none;
--rc-button-bg: transparent;
--rc-button-bg-hover: rgba(255, 255, 255, 0.18);
--rc-button-color: #fff;
--rc-button-size: 3rem;
--rc-cube-depth: 0px;
--rc-cube-duration: 600ms;
}
.rc-cube:focus-visible {
outline: 2px solid var(--rc-accent, #fff);
outline-offset: 2px;
}
.rc-cube .rc-button {
z-index: 3;
font-size: 1.6rem;
filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.55));
}
/* Perspective host. touch-action frees the page to scroll on the cross axis. */
.rc-cube__viewport {
position: absolute;
inset: 0;
perspective: 1200px;
touch-action: pan-y;
}
.rc-cube[data-orientation="vertical"] .rc-cube__viewport {
touch-action: pan-x;
}
/*
* The 3D context. Pulled back by one depth so the active face (which is pushed
* forward by the same depth) lands flush in the viewport plane.
*/
.rc-cube__cage {
position: absolute;
inset: 0;
transform-style: preserve-3d;
transform: translateZ(calc(var(--rc-cube-depth) * -1));
}
/*
* Each face sits at `rotate(offset · 90°) translateZ(depth)`. All faces share
* one transition, so when the active index changes every offset shifts by one
* and the faces orbit together — a rigid 90° cube turn. Direction falls out of
* the offset's sign, so no prev-index bookkeeping is needed.
*/
.rc-cube__face {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
border-radius: 14px;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
transform: rotateY(calc(var(--off) * 90deg)) translateZ(var(--rc-cube-depth));
transition: transform var(--rc-cube-duration) cubic-bezier(0.66, 0, 0.34, 1);
}
.rc-cube[data-orientation="vertical"] .rc-cube__face {
transform: rotateX(calc(var(--off) * -90deg)) translateZ(var(--rc-cube-depth));
}
/* Subtle depth: darken the side faces, fading to clear on the active one. */
.rc-cube[data-shadow] .rc-cube__face::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: #000;
opacity: 0.5;
transition: opacity var(--rc-cube-duration) ease;
pointer-events: none;
}
.rc-cube[data-shadow] .rc-cube__face[data-active]::after {
opacity: 0;
}
/* Soft ground shadow under the cube. */
.rc-cube__shadow {
position: absolute;
left: 8%;
right: 8%;
bottom: -7%;
height: 16%;
background: rgba(0, 0, 0, 0.45);
filter: blur(22px);
border-radius: 50%;
z-index: 0;
pointer-events: none;
}
.rc-cube__caption {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
padding: 0 1rem;
color: #fff;
font-size: clamp(1.5rem, 4vw, 2.5rem);
font-weight: 800;
text-align: center;
text-shadow: 0 2px 18px rgba(0, 0, 0, 0.55);
pointer-events: none;
}
.rc-cube .rc-dots {
position: absolute;
left: 0;
right: 0;
bottom: 0.9rem;
margin: 0;
z-index: 3;
}
.rc-cube .rc-dot {
background: rgba(255, 255, 255, 0.45);
}
.rc-cube .rc-dot--active {
background: #fff;
}
@media (prefers-reduced-motion: reduce) {
.rc-cube__face {
transition-duration: 1ms;
}
}