Stepper
Rangkaian langkah untuk menunjukkan kemajuan dalam proses bertahap.
Address
Add your address
Shipping
Set your preferred
Payment
Add any payment
Checkout
Confirm your order
Step 1 of 0
vue
<script setup lang="ts">
import { BookUser, CreditCard, Truck } from '@lucide/vue'
import { Stepper, StepperDescription, StepperIndicator, StepperItem, StepperSeparator, StepperTitle, StepperTrigger } from '@/components/ui/stepper'
const steps = [
{
step: 1,
title: 'Address',
description: 'Add your address',
icon: BookUser,
},
{
step: 2,
title: 'Shipping',
description: 'Set your preferred',
icon: Truck,
},
{
step: 3,
title: 'Payment',
description: 'Add any payment',
icon: CreditCard,
},
{
step: 4,
title: 'Checkout',
description: 'Confirm your order',
},
]
</script>
<template>
<Stepper class="flex w-10/12 items-start gap-2">
<StepperItem
v-for="item in steps"
:key="item.step"
:step="item.step"
class="relative flex w-full flex-col items-center justify-center"
>
<StepperTrigger>
<StepperIndicator v-slot="{ step }" class="bg-muted">
<template v-if="item.icon">
<component :is="item.icon" class="w-4 h-4" />
</template>
<span v-else>{{ step }}</span>
</StepperIndicator>
</StepperTrigger>
<StepperSeparator
v-if="item.step !== steps[steps.length - 1]?.step"
class="absolute left-[calc(50%+20px)] right-[calc(-50%+10px)] top-5 block h-0.5 shrink-0 rounded-full bg-muted group-data-[state=completed]:bg-primary"
/>
<div class="flex flex-col items-center">
<StepperTitle>
{{ item.title }}
</StepperTitle>
<StepperDescription>
{{ item.description }}
</StepperDescription>
</div>
</StepperItem>
</Stepper>
</template>Instalasi
CLI
bash
npx shadcn-vue@latest add stepperManual
Pasang dependensi berikut:
bash
npm install reka-uiSalin dan tempel kode sumber di GitHub ke project Anda.
Sesuaikan path import dengan struktur project Anda.
Penggunaan
vue
<script setup lang="ts">
import {
Stepper,
StepperDescription,
StepperIndicator,
StepperItem,
StepperSeparator,
StepperTitle,
StepperTrigger,
} from '@/components/ui/stepper'
</script>
<template>
<Stepper>
<StepperItem :step="1">
<StepperTrigger>
<StepperIndicator>1</StepperIndicator>
<StepperTitle>Step 1</StepperTitle>
<StepperDescription>This is the first step</StepperDescription>
</StepperTrigger>
<StepperSeparator />
</StepperItem>
<StepperItem :step="2">
<StepperTrigger>
<StepperIndicator>2</StepperIndicator>
<StepperTitle>Step 2</StepperTitle>
<StepperDescription>This is the second step</StepperDescription>
</StepperTrigger>
</StepperItem>
</Stepper>
</template>Contoh
Mendatar
Your details
Provide your name and email
Company details
A few details about your company
Invite your team
Start collaborating with your team
Step 1 of 0
vue
<script setup lang="ts">
import { Check, Circle, Dot } from '@lucide/vue'
import { Button } from '@/components/ui/button'
import { Stepper, StepperDescription, StepperItem, StepperSeparator, StepperTitle, StepperTrigger } from '@/components/ui/stepper'
const steps = [
{
step: 1,
title: 'Your details',
description: 'Provide your name and email',
},
{
step: 2,
title: 'Company details',
description: 'A few details about your company',
},
{
step: 3,
title: 'Invite your team',
description: 'Start collaborating with your team',
},
]
</script>
<template>
<Stepper class="flex w-full items-start gap-2">
<StepperItem
v-for="step in steps"
:key="step.step"
v-slot="{ state }"
class="relative flex w-full flex-col items-center justify-center"
:step="step.step"
>
<StepperSeparator
v-if="step.step !== steps[steps.length - 1]?.step"
class="absolute left-[calc(50%+20px)] right-[calc(-50%+10px)] top-5 block h-0.5 shrink-0 rounded-full bg-muted group-data-[state=completed]:bg-primary"
/>
<StepperTrigger as-child>
<Button
:variant="state === 'completed' || state === 'active' ? 'default' : 'outline'"
size="icon"
class="z-10 rounded-full shrink-0"
:class="[state === 'active' && 'ring-2 ring-ring ring-offset-2 ring-offset-background']"
>
<Check v-if="state === 'completed'" class="size-5" />
<Circle v-if="state === 'active'" />
<Dot v-if="state === 'inactive'" />
</Button>
</StepperTrigger>
<div class="mt-5 flex flex-col items-center text-center">
<StepperTitle
:class="[state === 'active' && 'text-primary']"
class="text-sm font-semibold transition lg:text-base"
>
{{ step.title }}
</StepperTitle>
<StepperDescription
:class="[state === 'active' && 'text-primary']"
class="sr-only text-xs text-muted-foreground transition md:not-sr-only lg:text-sm"
>
{{ step.description }}
</StepperDescription>
</div>
</StepperItem>
</Stepper>
</template>Tegak
Your details
Provide your name and email address. We will use this information to create your account
Company details
A few details about your company will help us personalize your experience
Invite your team
Start collaborating with your team by inviting them to join your account. You can skip this step and invite them later
Step 1 of 0
vue
<script setup lang="ts">
import { Check, Circle, Dot } from '@lucide/vue'
import { Button } from '@/components/ui/button'
import { Stepper, StepperDescription, StepperItem, StepperSeparator, StepperTitle, StepperTrigger } from '@/components/ui/stepper'
const steps = [
{
step: 1,
title: 'Your details',
description:
'Provide your name and email address. We will use this information to create your account',
},
{
step: 2,
title: 'Company details',
description: 'A few details about your company will help us personalize your experience',
},
{
step: 3,
title: 'Invite your team',
description:
'Start collaborating with your team by inviting them to join your account. You can skip this step and invite them later',
},
]
</script>
<template>
<Stepper orientation="vertical" class="mx-auto flex w-full max-w-md flex-col justify-start gap-10">
<StepperItem
v-for="step in steps"
:key="step.step"
v-slot="{ state }"
class="relative flex w-full items-start gap-6"
:step="step.step"
>
<StepperSeparator
v-if="step.step !== steps[steps.length - 1]?.step"
class="absolute left-[18px] top-[38px] block h-[105%] w-0.5 shrink-0 rounded-full bg-muted group-data-[state=completed]:bg-primary"
/>
<StepperTrigger as-child>
<Button
:variant="state === 'completed' || state === 'active' ? 'default' : 'outline'"
size="icon"
class="z-10 rounded-full shrink-0"
:class="[state === 'active' && 'ring-2 ring-ring ring-offset-2 ring-offset-background']"
>
<Check v-if="state === 'completed'" class="size-5" />
<Circle v-if="state === 'active'" />
<Dot v-if="state === 'inactive'" />
</Button>
</StepperTrigger>
<div class="flex flex-col gap-1">
<StepperTitle
:class="[state === 'active' && 'text-primary']"
class="text-sm font-semibold transition lg:text-base"
>
{{ step.title }}
</StepperTitle>
<StepperDescription
:class="[state === 'active' && 'text-primary']"
class="sr-only text-xs text-muted-foreground transition md:not-sr-only lg:text-sm"
>
{{ step.description }}
</StepperDescription>
</div>
</StepperItem>
</Stepper>
</template>Form
Step 1 of 0
vue
<script setup lang="ts">
import { Check, Circle, Dot } from '@lucide/vue'
import { toTypedSchema } from '@vee-validate/zod'
import { Field as VeeField, Form as VeeForm } from 'vee-validate'
import { h, ref } from 'vue'
import { toast } from 'vue-sonner'
import * as z from 'zod'
import { Button } from '@/components/ui/button'
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
} from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Stepper, StepperDescription, StepperItem, StepperSeparator, StepperTitle, StepperTrigger } from '@/components/ui/stepper'
const formSchema = [
z.object({
fullName: z.string(),
email: z.string().email(),
}),
z.object({
password: z.string().min(2).max(50),
confirmPassword: z.string(),
}).refine(
(values) => {
return values.password === values.confirmPassword
},
{
message: 'Passwords must match!',
path: ['confirmPassword'],
},
),
z.object({
favoriteDrink: z.union([z.literal('coffee'), z.literal('tea'), z.literal('soda')]),
}),
]
const stepIndex = ref(1)
const steps = [
{
step: 1,
title: 'Your details',
description: 'Provide your name and email',
},
{
step: 2,
title: 'Your password',
description: 'Choose a password',
},
{
step: 3,
title: 'Your Favorite Drink',
description: 'Choose a drink',
},
]
function onSubmit(values: any) {
toast('You submitted the following values:', {
description: h('pre', { class: 'mt-2 w-[320px] rounded-md bg-neutral-950 p-4' }, h('code', { class: 'text-white' }, JSON.stringify(values, null, 2))),
})
}
</script>
<template>
<VeeForm
v-slot="{ meta, values, validate }"
as="" keep-values :validation-schema="toTypedSchema(formSchema[stepIndex - 1]!)"
>
<Stepper v-slot="{ isNextDisabled, isPrevDisabled, nextStep, prevStep, modelValue }" v-model="stepIndex" class="block w-full">
<form
@submit="(e) => {
e.preventDefault()
validate()
if (stepIndex === steps.length && meta.valid) {
onSubmit(values)
}
}"
>
<div class="flex w-full flex-start gap-2">
<StepperItem
v-for="(step, index) in steps"
:key="step.step"
v-slot="{ state }"
class="relative flex w-full flex-col items-center justify-center"
:step="step.step"
>
<StepperSeparator
v-if="step.step !== steps[steps.length - 1]!.step"
class="absolute left-[calc(50%+20px)] right-[calc(-50%+10px)] top-5 block h-0.5 shrink-0 rounded-full bg-muted group-data-[state=completed]:bg-primary"
/>
<StepperTrigger as-child>
<Button
:variant="state === 'completed' || state === 'active' ? 'default' : 'outline'"
size="icon"
class="z-10 rounded-full shrink-0"
:class="[state === 'active' && 'ring-2 ring-ring ring-offset-2 ring-offset-background']"
:disabled="state !== 'completed' && (index >= (modelValue || 0) && !meta.valid)"
>
<Check v-if="state === 'completed'" class="size-5" />
<Circle v-if="state === 'active'" />
<Dot v-if="state === 'inactive'" />
</Button>
</StepperTrigger>
<div class="mt-5 flex flex-col items-center text-center">
<StepperTitle
:class="[state === 'active' && 'text-primary']"
class="text-sm font-semibold transition lg:text-base"
>
{{ step.title }}
</StepperTitle>
<StepperDescription
:class="[state === 'active' && 'text-primary']"
class="sr-only text-xs text-muted-foreground transition md:not-sr-only lg:text-sm"
>
{{ step.description }}
</StepperDescription>
</div>
</StepperItem>
</div>
<FieldGroup class="mt-4">
<template v-if="stepIndex === 1">
<VeeField v-slot="{ componentField, errors }" name="fullName">
<Field :data-invalid="!!errors.length">
<FieldLabel for="form-stepper-fullName">
Full Name
</FieldLabel>
<Input
id="form-stepper-fullName"
v-bind="componentField"
type="text"
:aria-invalid="!!errors.length"
/>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
<VeeField v-slot="{ componentField, errors }" name="email">
<Field :data-invalid="!!errors.length">
<FieldLabel for="form-stepper-email">
Email
</FieldLabel>
<Input
id="form-stepper-email"
v-bind="componentField"
type="email"
:aria-invalid="!!errors.length"
/>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
</template>
<template v-if="stepIndex === 2">
<VeeField v-slot="{ componentField, errors }" name="password">
<Field :data-invalid="!!errors.length">
<FieldLabel for="form-stepper-password">
Password
</FieldLabel>
<Input
id="form-stepper-password"
v-bind="componentField"
type="password"
:aria-invalid="!!errors.length"
/>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
<VeeField v-slot="{ componentField, errors }" name="confirmPassword">
<Field :data-invalid="!!errors.length">
<FieldLabel for="form-stepper-confirmPassword">
Confirm Password
</FieldLabel>
<Input
id="form-stepper-confirmPassword"
v-bind="componentField"
type="password"
:aria-invalid="!!errors.length"
/>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
</template>
<template v-if="stepIndex === 3">
<VeeField v-slot="{ componentField, errors }" name="favoriteDrink">
<Field :data-invalid="!!errors.length">
<FieldLabel for="form-stepper-favoriteDrink">
Drink
</FieldLabel>
<Select v-bind="componentField">
<SelectTrigger
id="form-stepper-favoriteDrink"
class="w-full!"
:aria-invalid="!!errors.length"
>
<SelectValue placeholder="Select a drink" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="coffee">
Coffee
</SelectItem>
<SelectItem value="tea">
Tea
</SelectItem>
<SelectItem value="soda">
Soda
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
</template>
</FieldGroup>
<div class="flex items-center justify-between mt-4">
<Button :disabled="isPrevDisabled" variant="outline" size="sm" @click="prevStep()">
Back
</Button>
<div class="flex items-center gap-3">
<Button v-if="stepIndex !== 3" :type="meta.valid ? 'button' : 'submit'" :disabled="isNextDisabled" size="sm" @click="meta.valid && nextStep()">
Next
</Button>
<Button
v-if="stepIndex === 3" size="sm" type="submit"
>
Submit
</Button>
</div>
</div>
</form>
</Stepper>
</VeeForm>
</template>