בונים פעם אחת. מפיצים בכל מקום.
אבני בניין נגישות שמשתלבות באופן טבעי בכל מוצר.
קרוסלת שקופיות עם מחוות מגע ופקדי מעבר לשקופית הקודמת והבאה.
אפשר לעבור בין הסיפורים באמצעות החצים או לבחור שקופית באמצעות הנקודות.
אבני בניין נגישות שמשתלבות באופן טבעי בכל מוצר.
דפוסים ברורים המורכבים מחלקים קטנים ואמינים.
ממשקים שימושיים המוכנים לשימוש במקלדת עוד לפני טעינת JavaScript.
אבני בניין נגישות שמשתלבות באופן טבעי בכל מוצר.
דפוסים ברורים המורכבים מחלקים קטנים ואמינים.
ממשקים שימושיים המוכנים לשימוש במקלדת עוד לפני טעינת JavaScript.
אבני בניין נגישות שמשתלבות באופן טבעי בכל מוצר.
דפוסים ברורים המורכבים מחלקים קטנים ואמינים.
ממשקים שימושיים המוכנים לשימוש במקלדת עוד לפני טעינת JavaScript.
אבני בניין נגישות שמשתלבות באופן טבעי בכל מוצר.
דפוסים ברורים המורכבים מחלקים קטנים ואמינים.
ממשקים שימושיים המוכנים לשימוש במקלדת עוד לפני טעינת JavaScript.
אבני בניין נגישות שמשתלבות באופן טבעי בכל מוצר.
דפוסים ברורים המורכבים מחלקים קטנים ואמינים.
ממשקים שימושיים המוכנים לשימוש במקלדת עוד לפני טעינת JavaScript.
אבני בניין נגישות שמשתלבות באופן טבעי בכל מוצר.
דפוסים ברורים המורכבים מחלקים קטנים ואמינים.
ממשקים שימושיים המוכנים לשימוש במקלדת עוד לפני טעינת JavaScript.
מיקוד עוצר את המעבר האוטומטי עד ללחיצה על התחלה. ריחוף משהה אותו זמנית.
אבני בניין נגישות שמשתלבות באופן טבעי בכל מוצר.
דפוסים ברורים המורכבים מחלקים קטנים ואמינים.
ממשקים שימושיים המוכנים לשימוש במקלדת עוד לפני טעינת JavaScript.
אבני בניין נגישות שמשתלבות באופן טבעי בכל מוצר.
דפוסים ברורים המורכבים מחלקים קטנים ואמינים.
ממשקים שימושיים המוכנים לשימוש במקלדת עוד לפני טעינת JavaScript.
אבני בניין נגישות שמשתלבות באופן טבעי בכל מוצר.
דפוסים ברורים המורכבים מחלקים קטנים ואמינים.
ממשקים שימושיים המוכנים לשימוש במקלדת עוד לפני טעינת JavaScript.
אבני בניין נגישות שמשתלבות בטבעיות בכל מוצר.
דפוסים ברורים, המורכבים מחלקים קטנים ואמינים.
ממשקים שימושיים שמוכנים למקלדת עוד לפני טעינת JavaScript.
"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