Carousel Default Controlled index, autoplay, arrows, and line indicators use stable defaults.
< script lang = " ts" setup >
import { computed, shallowRef } from 'vue'
import { useRoute } from 'vue-router'
const active = shallowRef ( 0 )
const route = useRoute ( )
const isZh = computed ( ( ) => route. path. startsWith ( '/zh/' ) )
const items = computed ( ( ) =>
isZh. value
? [
{ name : 'build' , title : '构建' , description : '可复用的基础模块' } ,
{ name : 'compose' , title : '组合' , description : '统一的视觉节奏' } ,
{ name : 'ship' , title : '交付' , description : '完整的交互状态' } ,
]
: [
{
name : 'build' ,
title : 'Build' ,
description : 'Reusable building blocks' ,
} ,
{
name : 'compose' ,
title : 'Compose' ,
description : 'Consistent visual rhythm' ,
} ,
{
name : 'ship' ,
title : 'Ship' ,
description : 'Polished interaction states' ,
} ,
] ,
)
</ script>
< template>
< s-carousel v-model = " active" :items = " items" :autoplay = " false" height = " 210" >
< template #item = " { item, index }" >
< div class = " slide" :class = " `slide--${index + 1}`" >
< span> 0{{ index + 1 }}</ span>
< strong> {{ item.title }}</ strong>
< small> {{ item.description }}</ small>
</ div>
</ template>
</ s-carousel>
</ template>
< style scoped >
.slide {
display : flex;
height : 100%;
flex-direction : column;
justify-content : center;
gap : 7px;
padding : 30px 56px;
color : white;
}
.slide--1 {
background : linear-gradient ( 135deg, #2563ff, #725cff) ;
}
.slide--2 {
background : linear-gradient ( 135deg, #008d92, #37c6aa) ;
}
.slide--3 {
background : linear-gradient ( 135deg, #d85d77, #ff9a58) ;
}
.slide span {
font-size : 12px;
letter-spacing : 0.18em;
opacity : 0.72;
}
.slide strong {
font-size : 25px;
}
.slide small {
opacity : 0.82;
}
</ style>
Card deck deck owns the centered card and the inactive layers on both sides. Use deck-visible for the visible layer count and deck-blur for optional depth blur instead of maintaining several nearly identical effect types.
< script lang = " ts" setup >
import { computed, shallowRef } from 'vue'
import { useRoute } from 'vue-router'
type DeckPreset = 'single' | 'layered' | 'soft'
const route = useRoute ( )
const isZh = computed ( ( ) => route. path. startsWith ( '/zh/' ) )
const activePreset = shallowRef< DeckPreset> ( 'layered' )
const items = computed ( ( ) =>
isZh. value
? [
{ title : '聚焦' , description : '当前内容位于中央前景' } ,
{ title : '层级' , description : '相邻内容退到左右后方' } ,
{ title : '流动' , description : '方向与空间连续变化' } ,
{ title : '景深' , description : '远处内容逐渐弱化' } ,
{ title : '回响' , description : '多层卡片保持稳定对称' } ,
]
: [
{ title : 'Focus' , description : 'Active content stays in front' } ,
{ title : 'Layer' , description : 'Adjacent content recedes evenly' } ,
{ title : 'Flow' , description : 'Spatial movement stays continuous' } ,
{ title : 'Depth' , description : 'Distant content becomes quieter' } ,
{ title : 'Echo' , description : 'Layers remain visually balanced' } ,
] ,
)
const presets = computed ( ( ) => [
{
key : 'single' as const ,
label : isZh. value ? '单层露出' : 'One layer' ,
visible : 1 ,
blur : false ,
scale : 0.88 ,
} ,
{
key : 'layered' as const ,
label : isZh. value ? '双层露出' : 'Two layers' ,
visible : 2 ,
blur : false ,
scale : 0.86 ,
} ,
{
key : 'soft' as const ,
label : isZh. value ? '双层虚化' : 'Soft depth' ,
visible : 2 ,
blur : true ,
scale : 0.86 ,
} ,
] )
const currentPreset = computed (
( ) =>
presets. value. find ( ( preset ) => preset. key === activePreset. value) ??
presets. value[ 1 ] ,
)
</ script>
< template>
< div class = " deck-example" >
< div class = " deck-example__controls" >
< s-button
v-for = " preset in presets"
:key = " preset.key"
type = " flat"
size = " small"
:active = " activePreset === preset.key"
@click = " activePreset = preset.key"
>
{{ preset.label }}
</ s-button>
</ div>
< s-carousel
:items = " items"
effect = " deck"
:autoplay = " false"
:radius = " 16"
:deck-visible = " currentPreset.visible"
:deck-blur = " currentPreset.blur"
:deck-scale = " currentPreset.scale"
:depth = " 140"
:perspective = " 960"
draggable
arrow = " always"
indicator-position = " outside"
height = " 250"
>
< template #item = " { item, index }" >
< div class = " deck-slide" :class = " `deck-slide--${(index % 5) + 1}`" >
< span> 0{{ index + 1 }}</ span>
< strong> {{ item.title }}</ strong>
< small> {{ item.description }}</ small>
</ div>
</ template>
</ s-carousel>
</ div>
</ template>
< style scoped >
.deck-example {
min-width : 0;
}
.deck-example__controls {
display : flex;
flex-wrap : wrap;
gap : 8px;
margin-bottom : 16px;
}
.deck-slide {
display : flex;
height : 100%;
box-sizing : border-box;
flex-direction : column;
justify-content : flex-end;
gap : 4px;
padding : 24px;
border-radius : inherit;
color : white;
}
.deck-slide--1 {
background : linear-gradient ( 145deg, #285fff, #7656ee) ;
}
.deck-slide--2 {
background : linear-gradient ( 145deg, #007f94, #38c9b0) ;
}
.deck-slide--3 {
background : linear-gradient ( 145deg, #c8497b, #f47d65) ;
}
.deck-slide--4 {
background : linear-gradient ( 145deg, #5f43b7, #aa68e8) ;
}
.deck-slide--5 {
background : linear-gradient ( 145deg, #2675a8, #44b4d5) ;
}
.deck-slide span {
font-size : 10px;
letter-spacing : 0.16em;
opacity : 0.72;
}
.deck-slide strong {
font-size : 18px;
}
.deck-slide small {
opacity : 0.78;
}
</ style>
Orbit stage orbit uses a continuous phase to move a lower ring of compact cards smoothly around the Y axis. A separate, larger active card sits above the ring and changes with a short GPU-friendly crossfade. orbit-max-visible defaults to 10. When a larger data set would produce an even base window, the orbit takes one additional real card to form an odd ring. Every looping orbit keeps one hidden clone buffer at each edge: the outgoing card moves into the back and fades out while the matching buffer fades in from the other side, preventing a wrap teleport. Only when every real item already fits and no extra item is available does it render a decorative placeholder with a transparent interior and visible outline as that back-side portal. Fewer than four source items are cloned to fill the ring. Set a non-zero orbit-angle to override automatic spacing.
< script lang = " ts" setup >
import { computed, shallowRef } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute ( )
const isZh = computed ( ( ) => route. path. startsWith ( '/zh/' ) )
const orbitAngle = shallowRef ( 0 )
const itemCount = shallowRef ( 8 )
const items = computed ( ( ) =>
Array. from ( { length : itemCount. value } , ( _, index ) => ( {
title : isZh. value ? ` 轨道 ${ index + 1 } ` : ` Orbit ${ index + 1 } ` ,
description : isZh. value
? index === 0
? '当前卡片对齐正前方'
: '点击可见卡片切换'
: index === 0
? 'The active card faces forward'
: 'Click a visible card to switch' ,
} ) ) ,
)
const angles = computed ( ( ) => [
{ value : 0 , label : isZh. value ? '自动均分' : 'Auto spacing' } ,
{ value : 34 , label : isZh. value ? '固定 34°' : 'Fixed 34°' } ,
{ value : 56 , label : isZh. value ? '固定 56°' : 'Fixed 56°' } ,
] )
const counts = computed ( ( ) => [
{ value : 2 , label : isZh. value ? '2 项 · 自动克隆' : '2 items · cloned' } ,
{ value : 3 , label : isZh. value ? '3 项 · 自动克隆' : '3 items · cloned' } ,
{ value : 8 , label : isZh. value ? '8 项' : '8 items' } ,
{ value : 10 , label : isZh. value ? '10 项' : '10 items' } ,
{
value : 100 ,
label : isZh. value ? '100 项 · 虚拟轨道' : '100 items · virtual' ,
} ,
] )
</ script>
< template>
< div class = " orbit-example" >
< div class = " orbit-example__toolbar" >
< div class = " orbit-example__controls" >
< s-button
v-for = " count in counts"
:key = " count.value"
type = " flat"
size = " small"
:active = " itemCount === count.value"
@click = " itemCount = count.value"
>
{{ count.label }}
</ s-button>
</ div>
< div class = " orbit-example__controls" >
< s-button
v-for = " angle in angles"
:key = " angle.value"
type = " flat"
size = " small"
:active = " orbitAngle === angle.value"
@click = " orbitAngle = angle.value"
>
{{ angle.label }}
</ s-button>
</ div>
</ div>
< s-carousel
:items = " items"
effect = " orbit"
:autoplay = " false"
:radius = " 14"
:depth = " 230"
:perspective = " 1000"
:orbit-angle = " orbitAngle"
:orbit-max-visible = " 10"
draggable
arrow = " always"
:indicator-position = " itemCount > 20 ? 'none' : 'outside'"
height = " 320"
>
< template #item = " { item, index }" >
< div class = " orbit-slide" :class = " `orbit-slide--${(index % 4) + 1}`" >
< span> 0{{ index + 1 }}</ span>
< strong> {{ item.title }}</ strong>
< small> {{ item.description }}</ small>
</ div>
</ template>
</ s-carousel>
</ div>
</ template>
< style scoped >
.orbit-example {
min-width : 0;
}
.orbit-example__toolbar {
display : flex;
flex-wrap : wrap;
justify-content : space-between;
gap : 10px 18px;
margin-bottom : 16px;
}
.orbit-example__controls {
display : flex;
flex-wrap : wrap;
gap : 8px;
}
.orbit-slide {
display : flex;
height : 100%;
box-sizing : border-box;
flex-direction : column;
justify-content : flex-end;
gap : 4px;
padding : 16px;
border-radius : inherit;
color : white;
}
.orbit-slide--1 {
background : linear-gradient ( 145deg, #285fff, #7656ee) ;
}
.orbit-slide--2 {
background : linear-gradient ( 145deg, #007f94, #38c9b0) ;
}
.orbit-slide--3 {
background : linear-gradient ( 145deg, #c8497b, #f47d65) ;
}
.orbit-slide--4 {
background : linear-gradient ( 145deg, #5f43b7, #aa68e8) ;
}
.orbit-slide span {
font-size : 9px;
letter-spacing : 0.12em;
opacity : 0.72;
}
.orbit-slide strong {
font-size : 14px;
}
.orbit-slide small {
font-size : 10px;
opacity : 0.76;
}
</ style>
Mirror prism prism arranges three cards around one shared axis. Every face keeps a continuous rotation phase, so side cards move like one rigid prism instead of flipping independently. radius is shared by the viewport and every card; pass false for crisp prism faces.
< script lang = " ts" setup >
import { computed, shallowRef } from 'vue'
import { useRoute } from 'vue-router'
type PrismPreset = 'faceted' | 'soft'
const route = useRoute ( )
const isZh = computed ( ( ) => route. path. startsWith ( '/zh/' ) )
const activePreset = shallowRef< PrismPreset> ( 'faceted' )
const items = computed ( ( ) =>
isZh. value
? [
{ title : '正面' , description : '当前内容清晰可读' } ,
{ title : '侧面' , description : '背面内容自然反向' } ,
{ title : '回转' , description : '点击侧面完成切换' } ,
]
: [
{ title : 'Front' , description : 'Active content stays readable' } ,
{ title : 'Side' , description : 'Back content reverses naturally' } ,
{ title : 'Turn' , description : 'Click a side to rotate' } ,
] ,
)
const presets = computed ( ( ) => [
{
key : 'faceted' as const ,
label : isZh. value ? '硬朗棱面' : 'Faceted' ,
radius : false ,
depth : 220 ,
} ,
{
key : 'soft' as const ,
label : isZh. value ? '柔和圆角' : 'Soft corners' ,
radius : 16 ,
depth : 240 ,
} ,
] )
const currentPreset = computed (
( ) =>
presets. value. find ( ( preset ) => preset. key === activePreset. value) ??
presets. value[ 0 ] ,
)
</ script>
< template>
< div class = " prism-example" >
< div class = " prism-example__controls" >
< s-button
v-for = " preset in presets"
:key = " preset.key"
type = " flat"
size = " small"
:active = " activePreset === preset.key"
@click = " activePreset = preset.key"
>
{{ preset.label }}
</ s-button>
</ div>
< s-carousel
:items = " items"
effect = " prism"
:autoplay = " false"
:radius = " currentPreset.radius"
:depth = " currentPreset.depth"
:perspective = " 900"
draggable
arrow = " always"
indicator-position = " outside"
height = " 280"
>
< template #item = " { item, index }" >
< div class = " prism-slide" :class = " `prism-slide--${index + 1}`" >
< span> 0{{ index + 1 }}</ span>
< strong> {{ item.title }}</ strong>
< small> {{ item.description }}</ small>
</ div>
</ template>
</ s-carousel>
</ div>
</ template>
< style scoped >
.prism-example {
min-width : 0;
}
.prism-example__controls {
display : flex;
flex-wrap : wrap;
gap : 8px;
margin-bottom : 16px;
}
.prism-slide {
display : flex;
height : 100%;
box-sizing : border-box;
align-items : center;
justify-content : center;
flex-direction : column;
gap : 4px;
padding : 18px;
border-radius : inherit;
color : white;
text-align : center;
}
.prism-slide--1 {
background : linear-gradient ( 145deg, #285fff, #7656ee) ;
}
.prism-slide--2 {
background : linear-gradient ( 145deg, #007f94, #38c9b0) ;
}
.prism-slide--3 {
background : linear-gradient ( 145deg, #c8497b, #f47d65) ;
}
.prism-slide span {
font-size : 24px;
font-weight : 700;
letter-spacing : 0.08em;
opacity : 0.82;
}
.prism-slide strong {
font-size : 16px;
}
.prism-slide small {
opacity : 0.76;
}
</ style>
Fade fade only transitions opacity. Enable motion-blur for a brief directional softening and compare both parameter states in the same example.
< script lang = " ts" setup >
import { computed, shallowRef } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute ( )
const isZh = computed ( ( ) => route. path. startsWith ( '/zh/' ) )
const motionBlur = shallowRef ( false )
const items = computed ( ( ) =>
Array. from ( { length : 4 } , ( _, index ) => ( {
title : ` 0 ${ index + 1 } ` ,
description : isZh. value
? index % 2
? 'Sax 动效'
: '聚焦内容'
: index % 2
? 'Sax motion'
: 'Focused content' ,
} ) ) ,
)
</ script>
< template>
< div class = " fade-example" >
< div class = " fade-example__controls" >
< s-button
type = " flat"
size = " small"
:active = " !motionBlur"
@click = " motionBlur = false"
>
{{ isZh ? '纯淡入淡出' : 'Plain fade' }}
</ s-button>
< s-button
type = " flat"
size = " small"
:active = " motionBlur"
@click = " motionBlur = true"
>
{{ isZh ? '动态虚化' : 'Motion blur' }}
</ s-button>
</ div>
< s-carousel
:items = " items"
effect = " fade"
:motion-blur = " motionBlur"
:autoplay = " false"
indicator-type = " number"
height = " 220"
>
< template #item = " { item, index }" >
< div class = " fade-slide" :class = " `fade-slide--${index + 1}`" >
< strong> {{ item.title }}</ strong>
< span> {{ item.description }}</ span>
</ div>
</ template>
</ s-carousel>
</ div>
</ template>
< style scoped >
.fade-example {
min-width : 0;
}
.fade-example__controls {
display : flex;
flex-wrap : wrap;
gap : 8px;
margin-bottom : 16px;
}
.fade-slide {
display : flex;
height : 100%;
align-items : center;
justify-content : center;
flex-direction : column;
gap : 7px;
color : white;
}
.fade-slide--1 {
background : linear-gradient ( 135deg, #205cff, #724de8) ;
}
.fade-slide--2 {
background : linear-gradient ( 135deg, #007e8d, #35bfa8) ;
}
.fade-slide--3 {
background : linear-gradient ( 135deg, #c34a78, #ed7a5c) ;
}
.fade-slide--4 {
background : linear-gradient ( 135deg, #573fb0, #9d63df) ;
}
.fade-slide strong {
font-size : 28px;
}
.fade-slide span {
font-size : 12px;
opacity : 0.78;
}
</ style>
External control The exposed API supports named navigation and explicit playback control. Disabled items are skipped by arrows, autoplay, keyboard, and drag navigation.
< script lang = " ts" setup >
import { computed, shallowRef } from 'vue'
import { useRoute } from 'vue-router'
import type { CarouselInstance } from '@vuesax-alpha/components/carousel'
const carousel = shallowRef< CarouselInstance> ( )
const active = shallowRef ( 0 )
const route = useRoute ( )
const isZh = computed ( ( ) => route. path. startsWith ( '/zh/' ) )
const labels = computed ( ( ) =>
isZh. value
? { previous : '上一张' , play : '播放' , pause : '暂停' , next : '下一张' }
: { previous : 'Previous' , play : 'Play' , pause : 'Pause' , next : 'Next' } ,
)
const items = [
{ name : 'alpha' , title : 'A' } ,
{ name : 'beta' , title : 'B' } ,
{ name : 'gamma' , title : 'C' } ,
]
</ script>
< template>
< div class = " controls" >
< s-button icon :aria-label = " labels.previous" @click = " carousel?.prev()" >
←
</ s-button>
< s-button icon :aria-label = " labels.play" @click = " carousel?.play()" >
▶
</ s-button>
< s-button icon :aria-label = " labels.pause" @click = " carousel?.pause()" >
Ⅱ
</ s-button>
< s-button icon :aria-label = " labels.next" @click = " carousel?.next()" >
→
</ s-button>
< span> {{ active + 1 }} / {{ items.length }}</ span>
</ div>
< s-carousel
ref = " carousel"
v-model = " active"
:items = " items"
effect = " slide"
:autoplay = " false"
height = " 210"
>
< template #item = " { item, index }" >
< div class = " control-slide" :class = " `control-slide--${index + 1}`" >
{{ item.title }}
</ div>
</ template>
</ s-carousel>
</ template>
< style scoped >
.controls {
display : flex;
align-items : center;
justify-content : center;
gap : 8px;
margin-bottom : 14px;
}
.controls span {
min-width : 52px;
color : rgb ( var ( --sax-text-color) ) ;
font-size : 12px;
text-align : center;
}
.control-slide {
display : grid;
height : 100%;
place-items : center;
color : white;
font-size : 38px;
font-weight : 700;
}
.control-slide--1 {
background : linear-gradient ( 135deg, #245dff, #7354ec) ;
}
.control-slide--2 {
background : linear-gradient ( 135deg, #008594, #3bc4aa) ;
}
.control-slide--3 {
background : linear-gradient ( 135deg, #c84d7a, #f1845f) ;
}
</ style>
Last Updated: 2026-08-17, 08:33:54