דילוג לתוכן
uiify

Carousel

קרוסלת שקופיות עם מחוות מגע ופקדי מעבר לשקופית הקודמת והבאה.

דפדפנים
Chrome 123+Firefox 121+Safari 17.5+
שכבה
2
גודל חבילה דחוסה
7.61 kB
מגבלת פריסה כפויה
0 ms
מגבלת Axe
0
תואם SSR
כן
חוזה מפורסם
סביבת בדיקות

מגרש ניסויים

Carousel

כרטיסים נבחרים

אפשר לעבור בין הסיפורים באמצעות החצים או לבחור שקופית באמצעות הנקודות.

רצועת כרטיסים מותאמת למסך

מימין לשמאל

ציר הבלוק

פקדים מסונכרנים ופקודות מובנות

הפעלה אוטומטית עם עצירה והתחלה

מיקוד עוצר את המעבר האוטומטי עד ללחיצה על התחלה. ריחוף משהה אותו זמנית.

גרירה אופציונלית בעכבר

עמעום באמצעות מעברי CSS

"use client";

import { CarouselClient } from "@gunkin/uiify/components/carousel/client";

const items = ["One", "Two", "Three"].map((label) => ({
  id: label,
  label,
  children: label,
}));

export default function Example() {
  return (
    <CarouselClient
      id="featured"
      aria-label="Featured cards"
      className=""
      slotProps={{
        viewport: { className: "" },
        status: { className: "" },
        rotationControl: {
          className: " ",
        },
        previousControl: {
          className: " ",
        },
        nextControl: { className: " " },
        dots: { className: "" },
        dot: { className: "" },
        slideContent: { className: "" },
      }}
      items={items}
    />
  );
}
/* Optional carousel skin. The native scrollbar is only hidden once real controls replace it. */
@layer components {
  [data-uiify-carousel] {
    position: relative;
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: var(--space-sm);
    inline-size: 100%;
    max-inline-size: 40rem;
  }

  /* Full-width rows: an optional rotation button, the viewport, and the status text. */
  [data-uiify-carousel] > [data-part="viewport"],
  [data-uiify-carousel] > [data-part="status"],
  [data-uiify-carousel] > button[data-part="controls"][data-carousel-action="rotation"] {
    grid-column: 1 / -1;
  }

  /*
   * Hide the scrollbar only once the carousel is attached AND renders buttons that can
   * take over from it. Without scripting the scrollbar is the sole indication that the
   * content scrolls at all, so the static carousel keeps it. `scrollbar-width` alone is
   * enough for the supported engines — no prefixed fallback.
   */
  [data-uiify-carousel][data-enhanced]:has(> button[data-part="controls"])
    > [data-part="viewport"] {
    scrollbar-width: none;
  }

  [data-uiify-carousel]
    > [data-part="viewport"]
    > [data-part="slide"]
    > [data-part="slide-content"] {
    display: flex;
    container-type: inline-size;
    min-block-size: 18rem;
    aspect-ratio: 1;
    align-items: center;
    justify-content: center;
    border: var(--border-width) solid var(--border);
    border-radius: var(--radius-xl);
    background-color: var(--card);
    color: var(--foreground);
    font-size: 2.25rem;
    font-weight: var(--weight-semibold);
    line-height: 1;
    box-shadow: var(--shadow-popover);
  }

  /* The controls are Button-skinned (`outline`; previous/next at the icon size). */
  [data-uiify-carousel] > button[data-part="controls"][data-carousel-action="rotation"] {
    justify-self: start;
    margin-block-end: var(--space-sm);
  }

  [data-uiify-carousel] > button[data-part="controls"][data-carousel-action="previous"],
  [data-uiify-carousel] > button[data-part="controls"][data-carousel-action="next"] {
    font-size: 1.25rem;
    line-height: 1;
  }

  /* Default chevron for icon-less buttons: two borders of a rotated box on the outline Button's
     free `::before`. `all: unset` drops the wash geometry Button gives every pseudo-element. The
     logical corner follows `dir`; the turn and the optical centring (the arms sit 0.14rem off
     the box centre) are mirrored with it. */
  [data-uiify-carousel]
    > button[data-part="controls"]:is(
      [data-carousel-action="previous"],
      [data-carousel-action="next"]
    ):empty {
    --uiify-carousel-turn: 1;

    &:dir(rtl) {
      --uiify-carousel-turn: -1;
    }

    &::before {
      all: unset;
      content: "";
      box-sizing: border-box;
      inline-size: 0.5rem;
      block-size: 0.5rem;
      border: 0 solid currentcolor;
      rotate: calc(-45deg * var(--uiify-carousel-turn));
    }

    &[data-carousel-action="previous"]::before {
      border-block-start-width: 2px;
      border-inline-start-width: 2px;
      translate: calc(0.14rem * var(--uiify-carousel-turn));
    }

    &[data-carousel-action="next"]::before {
      border-block-end-width: 2px;
      border-inline-end-width: 2px;
      translate: calc(-0.14rem * var(--uiify-carousel-turn));
    }
  }

  [data-uiify-carousel] > [data-part="dots"] > a:focus-visible {
    outline: none;
    box-shadow: 0 0 0 var(--ring-width) color-mix(in oklch, var(--ring) 20%, transparent);
  }

  [data-uiify-carousel] > [data-part="dots"] {
    /* Explicit middle column: pagination stays centred whether or not the
       previous/next buttons exist to occupy the outer columns. */
    grid-column: 2;
    justify-self: stretch;
    display: flex;
    justify-content: center;
    gap: var(--space-xs);
  }

  [data-uiify-carousel] > [data-part="dots"] > a {
    display: inline-grid;
    place-items: center;
    inline-size: var(--control-md);
    block-size: var(--control-md);
    border-radius: var(--radius-full);
    background-color: transparent;
    color: var(--muted-foreground);
    text-decoration: none;
    transition: var(--transition-colors);
    font-size: 0;
  }
  [data-uiify-carousel] > [data-part="dots"] > a::after {
    content: "";
    inline-size: 0.5rem;
    block-size: 0.5rem;
    border-radius: 50%;
    background: var(--input);
  }
  [data-uiify-carousel] > [data-part="dots"] > a[aria-current="true"]::after {
    background: var(--primary);
  }
}

@layer components {
  [data-uiify-carousel] [data-part="story"] {
    display: grid;
    grid-template-rows: auto 1fr auto;
    inline-size: 100%;
    block-size: 100%;
    min-inline-size: 0;
    min-block-size: 0;
    padding: clamp(0.875rem, 4cqi, 1.5rem);
    text-align: start;
  }

  [data-uiify-carousel] [data-part="story-topline"],
  [data-uiify-carousel] [data-part="story-footer"] {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.5rem;
    color: var(--muted-foreground);
    font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
    font-size: 0.75rem;
    font-weight: 500;
    letter-spacing: 0.08em;
  }

  [data-uiify-carousel] [data-part="story-body"] {
    display: grid;
    align-content: center;
    gap: 0.75rem;
    min-inline-size: 0;
    padding-block: 1rem;
  }

  [data-uiify-carousel] [data-part="story-eyebrow"] {
    color: var(--primary);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.1em;
  }

  [data-uiify-carousel] [data-part="story-body"] > h3 {
    max-inline-size: 12ch;
    margin: 0;
    color: var(--foreground);
    font-size: clamp(1.25rem, 6cqi, 2rem);
    font-weight: 600;
    letter-spacing: -0.045em;
    line-height: 1.08;
    white-space: pre-line;
  }

  [data-uiify-carousel] [data-part="story-body"] > p {
    max-inline-size: 30ch;
    margin: 0;
    color: var(--muted-foreground);
    font-size: 0.875rem;
    line-height: 1.55;
  }
}
"use client";

import { CarouselClient } from "@gunkin/uiify/components/carousel/client";

const items = ["One", "Two", "Three"].map((label) => ({
  id: label,
  label,
  children: label,
}));

export default function Example() {
  return (
    <CarouselClient
      id="featured"
      aria-label="Featured cards"
      className="
        relative grid [grid-template-columns:auto_1fr_auto] items-center gap-sm [inline-size:100%]
        [max-inline-size:40rem]
        data-[enhanced]:[&:has(>button[data-part=controls])>[data-part=viewport]]:[scrollbar-width:none]
      "
      slotProps={{
        viewport: { className: "[grid-column:1_/_-1]" },
        status: { className: "[grid-column:1_/_-1]" },
        rotationControl: {
          className:
            "relative isolate inline-flex min-h-10 min-w-10 items-center justify-center gap-1.5 px-5 py-0 border-[length:var(--border-width)] border-solid [border-color:color-mix(in_oklch,_var(--foreground)_20%,_transparent)] rounded-full bg-transparent bg-clip-padding text-foreground text-base leading-6 font-medium [font-family:inherit] no-underline cursor-pointer [transition:var(--transition-colors)] disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:[box-shadow:0_0_0_var(--ring-width)_color-mix(in_oklch,_var(--ring)_20%,_transparent)] [@media_(any-hover:_hover)_and_(any-pointer:_fine)]:[&:hover:not(:disabled)]:[background-color:color-mix(in_oklch,_var(--foreground)_10%,_transparent)] [&:active:not(:disabled)]:[background-color:color-mix(in_oklch,_var(--foreground)_16%,_transparent)] before:content-none after:content-[''] after:absolute after:[inset:calc(-1_*_var(--border-width))] after:z-[-1] after:rounded-[inherit] after:pointer-events-none after:[padding:var(--border-width)] after:[background-image:linear-gradient(to_bottom,_color-mix(in_oklch,_var(--foreground)_25%,_transparent),_transparent_75%)] after:[mask:linear-gradient(#fff_0_0)_content-box_exclude,_linear-gradient(#fff_0_0)] [&[hidden]]:hidden [grid-column:1_/_-1] [justify-self:start] [margin-block-end:var(--space-sm)]",
        },
        previousControl: {
          className:
            "relative isolate inline-flex min-h-10 min-w-10 items-center justify-center gap-1.5 px-5 py-0 border-[length:var(--border-width)] border-solid [border-color:color-mix(in_oklch,_var(--foreground)_20%,_transparent)] rounded-full bg-transparent bg-clip-padding text-foreground text-base leading-6 font-medium [font-family:inherit] no-underline cursor-pointer [transition:var(--transition-colors)] disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:[box-shadow:0_0_0_var(--ring-width)_color-mix(in_oklch,_var(--ring)_20%,_transparent)] [@media_(any-hover:_hover)_and_(any-pointer:_fine)]:[&:hover:not(:disabled)]:[background-color:color-mix(in_oklch,_var(--foreground)_10%,_transparent)] [&:active:not(:disabled)]:[background-color:color-mix(in_oklch,_var(--foreground)_16%,_transparent)] before:content-none after:content-[''] after:absolute after:[inset:calc(-1_*_var(--border-width))] after:z-[-1] after:rounded-[inherit] after:pointer-events-none after:[padding:var(--border-width)] after:[background-image:linear-gradient(to_bottom,_color-mix(in_oklch,_var(--foreground)_25%,_transparent),_transparent_75%)] after:[mask:linear-gradient(#fff_0_0)_content-box_exclude,_linear-gradient(#fff_0_0)] [&[hidden]]:hidden text-xl leading-none [--uiify-carousel-turn:1] rtl:[--uiify-carousel-turn:-1] empty:before:[all:unset] empty:before:content-[''] empty:before:box-border empty:before:[inline-size:0.5rem] empty:before:[block-size:0.5rem] empty:before:[border:0_solid_currentcolor] empty:before:[rotate:calc(-45deg*var(--uiify-carousel-turn))] data-[carousel-action=previous]:empty:before:[border-block-start-width:2px] data-[carousel-action=previous]:empty:before:[border-inline-start-width:2px] data-[carousel-action=previous]:empty:before:[translate:calc(0.14rem*var(--uiify-carousel-turn))] data-[carousel-action=next]:empty:before:[border-block-end-width:2px] data-[carousel-action=next]:empty:before:[border-inline-end-width:2px] data-[carousel-action=next]:empty:before:[translate:calc(-0.14rem*var(--uiify-carousel-turn))]",
        },
        nextControl: {
          className:
            "relative isolate inline-flex min-h-10 min-w-10 items-center justify-center gap-1.5 px-5 py-0 border-[length:var(--border-width)] border-solid [border-color:color-mix(in_oklch,_var(--foreground)_20%,_transparent)] rounded-full bg-transparent bg-clip-padding text-foreground text-base leading-6 font-medium [font-family:inherit] no-underline cursor-pointer [transition:var(--transition-colors)] disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:[box-shadow:0_0_0_var(--ring-width)_color-mix(in_oklch,_var(--ring)_20%,_transparent)] [@media_(any-hover:_hover)_and_(any-pointer:_fine)]:[&:hover:not(:disabled)]:[background-color:color-mix(in_oklch,_var(--foreground)_10%,_transparent)] [&:active:not(:disabled)]:[background-color:color-mix(in_oklch,_var(--foreground)_16%,_transparent)] before:content-none after:content-[''] after:absolute after:[inset:calc(-1_*_var(--border-width))] after:z-[-1] after:rounded-[inherit] after:pointer-events-none after:[padding:var(--border-width)] after:[background-image:linear-gradient(to_bottom,_color-mix(in_oklch,_var(--foreground)_25%,_transparent),_transparent_75%)] after:[mask:linear-gradient(#fff_0_0)_content-box_exclude,_linear-gradient(#fff_0_0)] [&[hidden]]:hidden text-xl leading-none [--uiify-carousel-turn:1] rtl:[--uiify-carousel-turn:-1] empty:before:[all:unset] empty:before:content-[''] empty:before:box-border empty:before:[inline-size:0.5rem] empty:before:[block-size:0.5rem] empty:before:[border:0_solid_currentcolor] empty:before:[rotate:calc(-45deg*var(--uiify-carousel-turn))] data-[carousel-action=previous]:empty:before:[border-block-start-width:2px] data-[carousel-action=previous]:empty:before:[border-inline-start-width:2px] data-[carousel-action=previous]:empty:before:[translate:calc(0.14rem*var(--uiify-carousel-turn))] data-[carousel-action=next]:empty:before:[border-block-end-width:2px] data-[carousel-action=next]:empty:before:[border-inline-end-width:2px] data-[carousel-action=next]:empty:before:[translate:calc(-0.14rem*var(--uiify-carousel-turn))]",
        },
        dots: { className: "[grid-column:2] [justify-self:stretch] flex justify-center gap-xs" },
        dot: {
          className:
            "relative inline-grid place-items-center [inline-size:var(--control-md)] " +
            "[block-size:var(--control-md)] rounded-[var(--radius-full)] bg-transparent " +
            "text-muted-foreground no-underline [transition:var(--transition-colors)] " +
            "[font-size:0] focus-visible:[outline:none] " +
            "focus-visible:[box-shadow:0_0_0_var(--ring-width)_color-mix(in_oklch,_var(--ring)_20%,_transparent)] " +
            "after:content-[''] after:[inline-size:0.5rem] after:[block-size:0.5rem] " +
            "after:rounded-[50%] after:bg-input [&[aria-current=true]::after]:bg-primary",
        },
        slideContent: {
          className:
            "flex [container-type:inline-size] [min-block-size:18rem] [aspect-ratio:1] " +
            "items-center justify-center border-[length:var(--border-width)] border-solid " +
            "border-border rounded-xl bg-card text-foreground [font-size:2.25rem] " +
            "font-[number:var(--weight-semibold)] leading-none shadow-popover",
        },
      }}
      items={items}
    />
  );
}

API

Carousel

CarouselClient — @gunkin/uiify/components/carousel/client

מאפייןטיפוסברירת מחדלתיאור
defaultValue / valuenumber—אינדקס פריט ראשוני או מבוקר; המצב הנצפה לאחר סיום התנועה מניע את הפקדים.
rewindbooleanfalseמעבר מחזורי בין קצות הרשימה הסופית ללא שכפול שקופיות.
mouseDragbooleanfalseגרירה בעכבר בהפעלה מפורשת. מגע וגלגלת ממשיכים לפעול באופן מובנה.
autoplayboolean | CarouselAutoplayOptionsfalseברירת המחדל להשהיה היא 5000ms. אפשרויות: delay, paused, cycles ו-stopOnInteraction. כולל כפתורי עצירה והפעלה גלויים.
onSelect / beforeChange / afterChange / onSwipecallbacks—מדווחת על שינויים בפועל לאחר סיום התנועה ועל הסיבות להם; להסבר על מצב מבוקר ותנועה שנקטעה, ראו את מדריך Carousel.
refRef<CarouselClientHandle>—הפעולות next, previous, goTo, start, stop ו-reset.

FadeCarouselClient — @gunkin/uiify/components/carousel/fade

מאפייןטיפוסברירת מחדלתיאור
swipebooleantrueהחלקה במגע או בעט; עכבר דורש mouseDrag. יורש את מאפייני הלקוח למעט axis. שקופיות לא פעילות אינן אינטראקטיביות.

מקלדת

מקשפעולה
Tab / Enterמיקוד ופתיחת קישורי שקופיות מובנים; כפתורים משופרים מופעלים גם באמצעות Space
Arrow keysשימוש בגלילה מובנית כאשר אזור התצוגה ממוקד

נגישות

Carousel הבסיסי הוא רכיב שרת עם גלילה מובנית, הצמדה וניווט מוגבל בקישורי מקטעים. נקודת הכניסה האינטראקטיבית /client היא Tier 2: נקודות מסונכרנות, חצים קבועים, callbacks, הפעלה אוטומטית וגרירה בעכבר דורשים הידרציה; /fade מוסיפה מעברי אטימות של CSS. מיקוד עוצר הפעלה אוטומטית עד להפעלת Start, ריחוף משהה אותה זמנית, ותנועה מופחתת מבטלת החלפה אוטומטית כברירת מחדל. מעבדת הפקדים הנוצרים ב-CSS היא ניסיונית.

נקודות חיבור לעיצוב

data-uiify-carouseldata-axisdata-enhanceddata-modedata-carousel-indexdata-statedata-dragging--uiify-carousel-slide-size--uiify-carousel-gap--uiify-carousel-fade-duration
דף זה תורגם אוטומטית וטרם נבדק.