---
name: delivery-slot
title: Delivery Slot
url: https://ui.frontic.com/components/sections/delivery-slot.md
tier: sections
install: npx @frontic/ui add delivery-slot
files:
  - components/sections/delivery-slot/DeliverySlot.vue
  - components/sections/delivery-slot/index.ts
  - components/sections/delivery-slot/types.ts
registryDependencies:
  - alert
  - button
  - calendar
  - card
  - field
  - format
  - tabs
  - toggle-group
dependencies:
  - "@internationalized/date"
  - "@lucide/vue"
  - reka-ui
---

# Delivery Slot

Delivery card with standard and express tabs, a calendar of bookable days, a row of time windows, the chosen slot with its price, and a confirm button.

## Install

```sh
npx @frontic/ui add delivery-slot
```

Built from: [Alert](https://ui.frontic.com/components/ui/alert.md), [Button](https://ui.frontic.com/components/ui/button.md), [Calendar](https://ui.frontic.com/components/ui/calendar.md), [Card](https://ui.frontic.com/components/ui/card.md), [Field](https://ui.frontic.com/components/ui/field.md), [Format](https://ui.frontic.com/lib/format.md), [Tabs](https://ui.frontic.com/components/ui/tabs.md), [Toggle Group](https://ui.frontic.com/components/ui/toggle-group.md)

npm packages: `@internationalized/date`, `@lucide/vue`, `reka-ui`

## Examples

```vue
<script setup lang="ts">
import { CalendarDate, getDayOfWeek, type DateValue } from '@internationalized/date'
import { ref, type Ref } from 'vue'
import { DeliverySlot, type DeliveryWindow } from '@/components/sections/delivery-slot'

// Pinned to October 2026, so the demo reads the same on every day it is shown.
const first = new CalendarDate(2026, 10, 12)
const fullyBooked = new CalendarDate(2026, 10, 16)
const unavailable = (date: DateValue) => getDayOfWeek(date, 'en-GB') > 4 || date.compare(fullyBooked) === 0

const windows: DeliveryWindow[] = [
  { value: '08-12', label: '8–12', detail: '08:00–12:00' },
  { value: '12-16', label: '12–16', detail: '12:00–16:00' },
  { value: '16-20', label: '16–20', detail: '16:00–20:00' },
  { value: '18-21', label: '18–21', detail: '18:00–21:00', full: true },
]

const day = ref(new CalendarDate(2026, 10, 14)) as Ref<DateValue>
const timeWindow = ref('16-20')
</script>

<template>
  <div class="mx-auto w-full max-w-md">
    <DeliverySlot
      v-model:day="day"
      v-model:window="timeWindow"
      :windows="windows"
      description="Pick a day and a time window. Free shipping and returns on all orders."
      :express-notice="{ title: 'Tomorrow before noon', description: 'Order by 14:00 today and it arrives on Friday, 9 October.' }"
      day-hint="Weekdays only. Friday the 16th is fully booked."
      :default-placeholder="first"
      :min-value="first"
      :max-value="first.add({ days: 18 })"
      :is-date-unavailable="unavailable"
      locale="en-GB"
      :week-starts-on="1"
      :price="{ amount: 0, currency: 'EUR' }"
    />
  </div>
</template>
```

## API reference

### DeliverySlot

Lets the shopper pick standard or express delivery, a day and a delivery window.

Styling: `delivery-slot-price`, which extends [`note-positive`](https://ui.frontic.com/DESIGN.md#note-positive); [`hint`](https://ui.frontic.com/DESIGN.md#hint). Change them there and every part that extends them follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `windows` (required) | `DeliveryWindow[]` |  | The delivery windows offered for the chosen day, one toggle each. |
| `title` | `string` | `'Delivery'` | The card's heading. |
| `description` | `string` |  | Shown under the heading. |
| `standardLabel` | `string` | `'Standard'` | Shown on the tab for standard delivery. |
| `expressLabel` | `string` | `'Express'` | Shown on the tab for express delivery. |
| `expressNotice` | `DeliveryNotice` |  | What the express tab says instead of the day and window pickers. |
| `dayLabel` | `string` | `'Day'` | Shown above the calendar. |
| `dayHint` | `string` |  | Shown under the calendar, e.g. how far ahead a day can be booked. |
| `defaultPlaceholder` | `DateValue`: `CalendarDate \| CalendarDateTime \| ZonedDateTime` |  | The month the calendar opens on when no day is chosen. |
| `minValue` | `DateValue`: `CalendarDate \| CalendarDateTime \| ZonedDateTime` |  | The earliest day the shopper can pick. |
| `maxValue` | `DateValue`: `CalendarDate \| CalendarDateTime \| ZonedDateTime` |  | The latest day the shopper can pick. |
| `isDateUnavailable` | `((date: DateValue) => boolean)`: `(date: DateValue): boolean` |  | Returns true for a day that cannot be booked, such as a Sunday or holiday. |
| `locale` | `string` | `'en'` | BCP 47 locale for the calendar and the chosen day in the summary, e.g. `de-DE`; the ConfigProvider's when left out. |
| `weekStartsOn` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6` | `0` | The calendar's first weekday, from 0 for Sunday to 6 for Saturday. |
| `windowLabel` | `string` | `'Time window'` | Shown above the delivery windows. |
| `windowAriaLabel` | `((label: string) => string)`: `(label: string): string` | ``Between ${label}`` | Builds each window toggle's screen-reader name from its label. |
| `price` | `PriceType` |  | The slot's price: `amount` in minor units such as cents, `currency` as an ISO 4217 code. |
| `freeLabel` | `string` | `'Free'` | Shown in place of the price when it is 0. |
| `slotLabel` | `((day: DateValue \| undefined, window: DeliveryWindow \| undefined) => string)`: `(day: DateValue \| undefined, window: DeliveryWindow \| undefined): string` |  | Builds the summary above the button from the chosen day and window; the formatted day and the window's time when left out. |
| `confirmLabel` | `string` | `'Confirm delivery slot'` | Shown on the button that confirms the delivery slot. |
| `surface` | `'raised' \| 'bare'` | `'raised'` | Bare drops the edge, shadow and inset, for a section placed inside another Card. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `speed` | `string` | `'standard'` | The chosen delivery speed, `standard` or `express`. |
| `day` | `DateValue`: `CalendarDate \| CalendarDateTime \| ZonedDateTime` |  | The chosen delivery day. |
| `window` | `string` |  | The chosen delivery window, by its `value`. |

| Event | Payload | Description |
| --- | --- | --- |
| `update:speed` | `value: string` | Fires with the new value when `speed` changes; `v-model:speed` binds it. |
| `update:day` | `value: DateValue \| undefined` | Fires with the new value when `day` changes; `v-model:day` binds it. |
| `update:window` | `value: string \| undefined` | Fires with the new value when `window` changes; `v-model:window` binds it. |
| `confirm` | `slot: { speed: string; day?: DateValue \| undefined; window?: string \| undefined; }` | Fires with the chosen delivery speed, day and time window when the shopper confirms the slot. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-surface` | `'raised' \| 'bare'` | Carries the `surface` prop. |

## Exports

```ts
import { type DeliveryNotice, type DeliveryWindow } from '@/components/sections/delivery-slot'
```

- `DeliveryNotice`: below
- `DeliveryWindow`: below

```ts
interface DeliveryNotice {
  title: string
  description?: string
}

interface DeliveryWindow {
  value: string
  /** The toggle's text, e.g. `8–12`. */
  label: string
  /** The summary's text, e.g. `08:00–12:00`; falls back to `label`. */
  detail?: string
  /** Booked out: shown, not selectable. */
  full?: boolean
}
```

## Styling

| Part | Recipe |
| --- | --- |
| `hint` | [`hint`](https://ui.frontic.com/DESIGN.md#hint) |
| `price` | [`delivery-slot-price`](https://ui.frontic.com/DESIGN.md#delivery-slot-price) |

## Source

`components/sections/delivery-slot/DeliverySlot.vue`

```vue
<script lang="ts">
/** Lets the shopper pick standard or express delivery, a day and a delivery window. */
export default {}
</script>

<script setup lang="ts">
import { TruckIcon, ZapIcon } from '@lucide/vue'

import type { DateValue } from '@internationalized/date'
import type { HTMLAttributes } from 'vue'
import type { DeliveryNotice, DeliveryWindow } from './types'
import { computed, toRef } from 'vue'
import { formatDate, formatPrice, type PriceType } from '@library/lib/format'
import { Alert, AlertDescription, AlertTitle } from '@library/components/ui/alert'
import { Button } from '@library/components/ui/button'
import { Calendar } from '@library/components/ui/calendar'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@library/components/ui/card'
import { Field, FieldDescription, FieldGroup, FieldLabel } from '@library/components/ui/field'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@library/components/ui/tabs'
import { ToggleGroup, ToggleGroupItem } from '@library/components/ui/toggle-group'
import { useLocale } from 'reka-ui'

const props = withDefaults(
  defineProps<{
    /** The delivery windows offered for the chosen day, one toggle each. */
    windows: DeliveryWindow[]
    /** The card's heading. */
    title?: string
    /** Shown under the heading. */
    description?: string
    /** Shown on the tab for standard delivery. */
    standardLabel?: string
    /** Shown on the tab for express delivery. */
    expressLabel?: string
    /** What the express tab says instead of the day and window pickers. */
    expressNotice?: DeliveryNotice
    /** Shown above the calendar. */
    dayLabel?: string
    /** Shown under the calendar, e.g. how far ahead a day can be booked. */
    dayHint?: string
    /** The month the calendar opens on when no day is chosen. */
    defaultPlaceholder?: DateValue
    /** The earliest day the shopper can pick. */
    minValue?: DateValue
    /** The latest day the shopper can pick. */
    maxValue?: DateValue
    /** Returns true for a day that cannot be booked, such as a Sunday or holiday. */
    isDateUnavailable?: (date: DateValue) => boolean
    /** BCP 47 locale for the calendar and the chosen day in the summary, e.g. `de-DE`; the ConfigProvider's when left out. */
    locale?: string
    /** The calendar's first weekday, from 0 for Sunday to 6 for Saturday. */
    weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6
    /** Shown above the delivery windows. */
    windowLabel?: string
    /** Builds each window toggle's screen-reader name from its label. */
    windowAriaLabel?: (label: string) => string
    /** The slot's price: `amount` in minor units such as cents, `currency` as an ISO 4217 code. */
    price?: PriceType
    /** Shown in place of the price when it is 0. */
    freeLabel?: string
    /** Builds the summary above the button from the chosen day and window; the formatted day and the window's time when left out. */
    slotLabel?: (day: DateValue | undefined, window: DeliveryWindow | undefined) => string
    /** Shown on the button that confirms the delivery slot. */
    confirmLabel?: string
    /** Bare drops the edge, shadow and inset, for a section placed inside another Card. */
    surface?: 'raised' | 'bare'
    class?: HTMLAttributes['class']
  }>(),
  {
    surface: 'raised',
    title: 'Delivery',
    standardLabel: 'Standard',
    expressLabel: 'Express',
    dayLabel: 'Day',
    locale: 'en',
    weekStartsOn: 0,
    windowLabel: 'Time window',
    freeLabel: 'Free',
    windowAriaLabel: (label: string) => `Between ${label}`,
    confirmLabel: 'Confirm delivery slot',
  },
)
const locale = useLocale(toRef(() => props.locale))
const priceText = computed(() => (props.price ? (props.price.amount === 0 ? props.freeLabel : formatPrice(props.price, locale.value)) : undefined))
const emit = defineEmits<{
  /** Fires with the chosen delivery speed, day and time window when the shopper confirms the slot. */
  confirm: [slot: { speed: string; day?: DateValue; window?: string }]
}>()
/** The chosen delivery speed, `standard` or `express`. */
const speed = defineModel<string>('speed', { default: 'standard' })
/** The chosen delivery day. */
const day = defineModel<DateValue | undefined>('day')
/** The chosen delivery window, by its `value`. */
const timeWindow = defineModel<string | undefined>('window')

const chosenWindow = computed(() => props.windows.find((w) => w.value === timeWindow.value))
const slot = computed(() => {
  if (props.slotLabel) return props.slotLabel(day.value, chosenWindow.value)
  const date = day.value && formatDate(day.value, { weekday: 'long', day: 'numeric', month: 'long' }, locale.value)
  const time = chosenWindow.value && (chosenWindow.value.detail ?? chosenWindow.value.label)
  return [date, time].filter(Boolean).join(' · ')
})
</script>

<template>
  <Card :surface="surface" :class="props.class">
    <CardHeader>
      <CardTitle>{{ title }}</CardTitle>
      <CardDescription v-if="description">{{ description }}</CardDescription>
    </CardHeader>
    <CardContent>
      <Tabs v-model="speed" class="flex flex-col gap-6">
        <TabsList class="w-full">
          <TabsTrigger value="standard">
            <TruckIcon data-icon />
            {{ standardLabel }}
          </TabsTrigger>
          <TabsTrigger value="express">
            <template #icon><ZapIcon data-icon /></template>
            {{ expressLabel }}
          </TabsTrigger>
        </TabsList>
        <TabsContent value="standard">
          <FieldGroup>
            <Field>
              <FieldLabel>{{ dayLabel }}</FieldLabel>
              <Calendar
                v-model="day"
                :default-placeholder="defaultPlaceholder"
                :min-value="minValue"
                :max-value="maxValue"
                :is-date-unavailable="isDateUnavailable"
                :locale="locale"
                :week-starts-on="weekStartsOn"
                class="self-center"
              />
              <FieldDescription v-if="dayHint">{{ dayHint }}</FieldDescription>
            </Field>
            <Field>
              <FieldLabel>{{ windowLabel }}</FieldLabel>
              <ToggleGroup v-model="timeWindow" type="single" variant="outline" class="w-full">
                <ToggleGroupItem v-for="w in windows" :key="w.value" :value="w.value" :disabled="w.full" :aria-label="windowAriaLabel(w.label)" class="flex-1">
                  {{ w.label }}
                </ToggleGroupItem>
              </ToggleGroup>
            </Field>
          </FieldGroup>
        </TabsContent>
        <TabsContent v-if="expressNotice" value="express">
          <Alert variant="info">
            <template #icon><ZapIcon data-icon /></template>
            <AlertTitle>{{ expressNotice.title }}</AlertTitle>
            <AlertDescription v-if="expressNotice.description">{{ expressNotice.description }}</AlertDescription>
          </Alert>
        </TabsContent>
      </Tabs>
    </CardContent>
    <CardFooter class="flex-col items-stretch gap-4">
      <div v-if="slot || priceText" class="flex items-center justify-between gap-2">
        <span data-slot="hint" class="fui-hint">{{ slot }}</span>
        <span v-if="priceText" data-slot="delivery-slot-price" class="fui-delivery-slot-price">{{ priceText }}</span>
      </div>
      <Button color="checkout" class="w-full" @click="emit('confirm', { speed, day, window: timeWindow })">{{ confirmLabel }}</Button>
    </CardFooter>
  </Card>
</template>
```

`components/sections/delivery-slot/types.ts`

```ts
export interface DeliveryWindow {
  value: string
  /** The toggle's text, e.g. `8–12`. */
  label: string
  /** The summary's text, e.g. `08:00–12:00`; falls back to `label`. */
  detail?: string
  /** Booked out: shown, not selectable. */
  full?: boolean
}

export interface DeliveryNotice {
  title: string
  description?: string
}
```
