בונים פעם אחת. מפיצים בכל מקום.
אבני בניין נגישות שמשתלבות באופן טבעי בכל מוצר.
קרוסלת שקופיות עם מחוות מגע ופקדי מעבר לשקופית הקודמת והבאה.
"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}
/>
);
}Carousel| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
id* | string | — | מזהה שורש ייחודי; משמש ליצירת יעדי מקטע יציבים. |
items* | readonly CarouselItem[] | — | פריטים עם מזהה יציב, תווית נגישה וילדים. רשימה ריקה מציגה null. |
aria-label* | string | — | שם נגיש לקרוסלה. |
axis | "inline" | "block" | "inline" | ציר גלילה לוגי. ציר הבלוק דורש block-size מוגדר לאזור התצוגה. |
dir | "ltr" | "rtl" | — | כיוון הטקסט וההתקדמות בציר השורה. |
navigation | boolean | true | מציג את כפתורי הקודם והבא הקבועים במצב לקוח. |
pagination | boolean | true | קישורי מקטע ישירים. המצב הנוכחי נחשף רק לאחר חיבור הלקוח. |
labels | Partial<CarouselLabels> | — | תרגום התוויות previous, next, goTo, navigation, start, stop ו-of. |
icons | CarouselIcons | — | תוכן גלוי לכפתורי הקודם והבא, הריקים כברירת מחדל. ניתן להעביר סמל, אייקון או מילה; שמותיהם עדיין נקבעים באמצעות labels. |
className | string | — | מחלקת השורש. |
slotProps | CarouselSlotProps | — | מאפיינים מובנים לפקדים, לאזור התצוגה, לשקופיות, לנקודות ולמצב שנוצרים; ניתן להשתמש במזהה השקופית כמפתח למשבצות חוזרות. |
style | CarouselStyle | — | ערכי CSS, לרבות גודל השקופית, הרווח ומשך הדהייה. |
CarouselClient — @gunkin/uiify/components/carousel/client| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
defaultValue / value | number | — | אינדקס פריט ראשוני או מבוקר; המצב הנצפה לאחר סיום התנועה מניע את הפקדים. |
rewind | boolean | false | מעבר מחזורי בין קצות הרשימה הסופית ללא שכפול שקופיות. |
mouseDrag | boolean | false | גרירה בעכבר בהפעלה מפורשת. מגע וגלגלת ממשיכים לפעול באופן מובנה. |
autoplay | boolean | CarouselAutoplayOptions | false | ברירת המחדל להשהיה היא 5000ms. אפשרויות: delay, paused, cycles ו-stopOnInteraction. כולל כפתורי עצירה והפעלה גלויים. |
onSelect / beforeChange / afterChange / onSwipe | callbacks | — | מדווחת על שינויים בפועל לאחר סיום התנועה ועל הסיבות להם; להסבר על מצב מבוקר ותנועה שנקטעה, ראו את מדריך Carousel. |
ref | Ref<CarouselClientHandle> | — | הפעולות next, previous, goTo, start, stop ו-reset. |
FadeCarouselClient — @gunkin/uiify/components/carousel/fade| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
swipe | boolean | true | החלקה במגע או בעט; עכבר דורש 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