---
name: return-request
title: Return Request
url: https://ui.frontic.com/components/sections/return-request.md
tier: sections
install: npx @frontic/ui add return-request
files:
  - components/sections/return-request/ReturnRequest.vue
  - components/sections/return-request/index.ts
  - components/sections/return-request/types.ts
registryDependencies:
  - button
  - card
  - checkbox
  - field
  - format
  - native-select
  - price
  - radio-group
  - textarea
---

# Return Request

Return form card that picks the items to send back, skipping final-sale ones, with a reason, a note, where the refund goes, the refund total and a request action.

## Install

```sh
npx @frontic/ui add return-request
```

Built from: [Button](https://ui.frontic.com/components/ui/button.md), [Card](https://ui.frontic.com/components/ui/card.md), [Checkbox](https://ui.frontic.com/components/ui/checkbox.md), [Field](https://ui.frontic.com/components/ui/field.md), [Format](https://ui.frontic.com/lib/format.md), [Native Select](https://ui.frontic.com/components/ui/native-select.md), [Price](https://ui.frontic.com/components/ui/price.md), [Radio Group](https://ui.frontic.com/components/ui/radio-group.md), [Textarea](https://ui.frontic.com/components/ui/textarea.md)

## Examples

```vue
<script setup lang="ts">
import { ref } from 'vue'
import { ReturnRequest, type ReturnChoice, type ReturnItem } from '@/components/sections/return-request'

const items: ReturnItem[] = [
  { id: 'jacket', name: 'Northern Light Hybrid Jacket', detail: 'Lavender · S', price: { amount: 11500, currency: 'EUR' } },
  { id: 'backpack', name: 'TrailMaster 22', detail: 'Brown · 20L', price: { amount: 14100, currency: 'EUR' } },
  { id: 'goggles', name: 'Mountain View Ski Goggles', detail: 'Final sale, not returnable', price: { amount: 6695, currency: 'EUR' }, returnable: false },
]

const refundMethods: ReturnChoice[] = [
  { value: 'original', label: 'Visa •• 4242' },
  { value: 'credit', label: 'Store credit' },
]

const selected = ref(['jacket'])
const reason = ref('fit')
const comment = ref('Runs a little small, swapping for an M.')
const refundTo = ref('original')
</script>

<template>
  <div class="mx-auto w-full max-w-md">
    <ReturnRequest
      v-model:selected="selected"
      v-model:reason="reason"
      v-model:comment="comment"
      v-model:refund-to="refundTo"
      description="Order #FR-20291 · delivered Sep 26"
      :items="items"
      :refund-methods="refundMethods"
    />
  </div>
</template>
```

## API reference

### ReturnRequest

Lets the shopper pick items to return, give a reason and choose the refund.

Styling: [`meta`](https://ui.frontic.com/DESIGN.md#meta). Change it there and every part that extends it follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` | `string` | `'Start a return'` | Shown as the card's title. |
| `description` | `string` |  | The order the return is for, such as its number and delivery date. |
| `items` (required) | `ReturnItem[]` |  | The order's items, each with a checkbox to pick it for the return. |
| `reasons` | `ReturnChoice[]` | `[     { value: 'fit', label: 'Doesn't fit' },     { value: 'quality', label: 'Not as expected' },     { value: 'damaged', label: 'Arrived damaged' },     { value: 'wrong', label: 'Wrong item sent' },     { value: 'other', label: 'Something else' }, ]` | The return reasons the shopper picks from; the reason field hides when empty. |
| `refundMethods` | `ReturnChoice[]` | `[]` | Where the refund can go: the original payment, store credit. |
| `currency` | `string` | `'EUR'` | ISO 4217 code the refund total is shown in while there are no items; otherwise the items' own. |
| `itemsLabel` | `string` | `'Items to return'` | Shown above the list of items. |
| `reasonLabel` | `string` | `'Reason'` | Shown above the reason select. |
| `commentLabel` | `string` | `'Anything we should know?'` | Shown above the comment field. |
| `commentPlaceholder` | `string` |  | Shown in the empty comment field. |
| `refundToLabel` | `string` | `'Refund to'` | Shown above the refund methods. |
| `refundLabel` | `string` | `'Refund'` | Shown above the refund total. |
| `submitLabel` | `string` | `'Request return'` | Shown on the button that sends the return request. |
| `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. |
| `selected` | `string[]` | `[]` | The ids of the items picked for the return. |
| `reason` | `string` |  | The chosen return reason, by value; the first reason shows when none is. |
| `comment` | `string` | `''` | What the shopper typed in the comment field. |
| `refundTo` | `string` |  | The chosen refund method, by value; the first method shows when none is. |

| Event | Payload | Description |
| --- | --- | --- |
| `submit` | `request: ReturnRequestValue` | Fires with the selected item IDs, reason, comment and refund method when the customer requests the return. |
| `update:selected` | `value: string[]` | Fires with the new value when `selected` changes; `v-model:selected` binds it. |
| `update:reason` | `value: string \| undefined` | Fires with the new value when `reason` changes; `v-model:reason` binds it. |
| `update:comment` | `value: string` | Fires with the new value when `comment` changes; `v-model:comment` binds it. |
| `update:refundTo` | `value: string \| undefined` | Fires with the new value when `refundTo` changes; `v-model:refundTo` binds it. |

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

## Exports

```ts
import { type ReturnChoice, type ReturnItem, type ReturnRequestValue } from '@/components/sections/return-request'
```

- `ReturnChoice`: below
- `ReturnItem`: below
- `ReturnRequestValue`: below

```ts
interface ReturnChoice {
  value: string
  label: string
}

interface ReturnItem {
  id: string
  name: string
  /** The variant bought, or why the item cannot go back. */
  detail?: string
  price: PriceType
  /** False for an item that cannot be returned, such as a final-sale one. */
  returnable?: boolean
}

interface ReturnRequestValue {
  items: string[]
  reason?: string
  comment?: string
  refundTo?: string
}
```

## Styling

| Part | Recipe |
| --- | --- |
| `option` | [`return-request-option`](https://ui.frontic.com/DESIGN.md#return-request-option) |
| `meta` | [`meta`](https://ui.frontic.com/DESIGN.md#meta) |

## Source

`components/sections/return-request/ReturnRequest.vue`

```vue
<script lang="ts">
/** Lets the shopper pick items to return, give a reason and choose the refund. */
export default {}
</script>

<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { ReturnChoice, ReturnItem, ReturnRequestValue } from './types'
import type { PriceType } from '@library/lib/format'
import { computed, useId } from 'vue'
import { Button } from '@library/components/ui/button'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@library/components/ui/card'
import { Checkbox } from '@library/components/ui/checkbox'
import { Field, FieldContent, FieldDescription, FieldLabel, FieldLegend, FieldSet } from '@library/components/ui/field'
import { NativeSelect, NativeSelectOption } from '@library/components/ui/native-select'
import { Price } from '@library/components/ui/price'
import { RadioGroup, RadioGroupItem } from '@library/components/ui/radio-group'
import { Textarea } from '@library/components/ui/textarea'

const props = withDefaults(
  defineProps<{
    /** Shown as the card's title. */
    title?: string
    /** The order the return is for, such as its number and delivery date. */
    description?: string
    /** The order's items, each with a checkbox to pick it for the return. */
    items: ReturnItem[]
    /** The return reasons the shopper picks from; the reason field hides when empty. */
    reasons?: ReturnChoice[]
    /** Where the refund can go: the original payment, store credit. */
    refundMethods?: ReturnChoice[]
    /** ISO 4217 code the refund total is shown in while there are no items; otherwise the items' own. */
    currency?: string
    /** Shown above the list of items. */
    itemsLabel?: string
    /** Shown above the reason select. */
    reasonLabel?: string
    /** Shown above the comment field. */
    commentLabel?: string
    /** Shown in the empty comment field. */
    commentPlaceholder?: string
    /** Shown above the refund methods. */
    refundToLabel?: string
    /** Shown above the refund total. */
    refundLabel?: string
    /** Shown on the button that sends the return request. */
    submitLabel?: string
    /** Bare drops the edge, shadow and inset, for a section placed inside another Card. */
    surface?: 'raised' | 'bare'
    class?: HTMLAttributes['class']
  }>(),
  {
    surface: 'raised',
    title: 'Start a return',
    reasons: () => [
      { value: 'fit', label: "Doesn't fit" },
      { value: 'quality', label: 'Not as expected' },
      { value: 'damaged', label: 'Arrived damaged' },
      { value: 'wrong', label: 'Wrong item sent' },
      { value: 'other', label: 'Something else' },
    ],
    refundMethods: () => [],
    currency: 'EUR',
    itemsLabel: 'Items to return',
    reasonLabel: 'Reason',
    commentLabel: 'Anything we should know?',
    refundToLabel: 'Refund to',
    refundLabel: 'Refund',
    submitLabel: 'Request return',
  },
)
const emit = defineEmits<{
  /** Fires with the selected item IDs, reason, comment and refund method when the customer requests the return. */
  submit: [request: ReturnRequestValue]
}>()

/** The ids of the items picked for the return. */
const selected = defineModel<string[]>('selected', { default: () => [] })
/** The chosen return reason, by value; the first reason shows when none is. */
const reason = defineModel<string>('reason')
/** What the shopper typed in the comment field. */
const comment = defineModel<string>('comment', { default: '' })
/** The chosen refund method, by value; the first method shows when none is. */
const refundTo = defineModel<string>('refundTo')

const id = useId()

const returnable = (item: ReturnItem) => item.returnable !== false
const isSelected = (item: ReturnItem) => returnable(item) && selected.value.includes(item.id)

function setSelected(item: ReturnItem, on: boolean | 'indeterminate') {
  const others = selected.value.filter((value) => value !== item.id)
  selected.value = on === true ? [...others, item.id] : others
}

const refundTotal = computed(() => props.items.filter(isSelected).reduce((sum, item) => sum + item.price.amount, 0))
// The items' currency and precision; `currency` while there are none.
const refund = computed((): PriceType => ({ ...(props.items[0]?.price ?? { currency: props.currency }), ref: undefined, amount: refundTotal.value }))

function submit() {
  emit('submit', { items: props.items.filter(isSelected).map((item) => item.id), reason: reason.value, comment: comment.value, refundTo: refundTo.value })
}
</script>

<template>
  <Card :surface="surface" :class="props.class">
    <CardHeader>
      <CardTitle>{{ title }}</CardTitle>
      <CardDescription v-if="description">{{ description }}</CardDescription>
    </CardHeader>
    <CardContent class="flex flex-col gap-6">
      <FieldSet>
        <FieldLegend variant="label">{{ itemsLabel }}</FieldLegend>
        <div data-slot="checkbox-group" class="flex flex-col gap-3">
          <Field v-for="item in items" :key="item.id" orientation="horizontal" :data-disabled="!returnable(item) || undefined">
            <Checkbox :id="`${id}-item-${item.id}`" :model-value="isSelected(item)" :disabled="!returnable(item)" @update:model-value="setSelected(item, $event)" />
            <FieldContent>
              <FieldLabel :for="`${id}-item-${item.id}`">{{ item.name }}</FieldLabel>
              <FieldDescription v-if="item.detail">{{ item.detail }}</FieldDescription>
            </FieldContent>
            <Price :price="item.price" size="sm" :show-discount="false" align="end" />
          </Field>
        </div>
      </FieldSet>
      <Field v-if="reasons.length">
        <FieldLabel :for="`${id}-reason`">{{ reasonLabel }}</FieldLabel>
        <NativeSelect :id="`${id}-reason`" :model-value="reason ?? reasons[0]?.value" class="w-full" @update:model-value="reason = String($event)">
          <NativeSelectOption v-for="option in reasons" :key="option.value" :value="option.value">{{ option.label }}</NativeSelectOption>
        </NativeSelect>
      </Field>
      <Field>
        <FieldLabel :for="`${id}-comment`">{{ commentLabel }}</FieldLabel>
        <Textarea :id="`${id}-comment`" v-model="comment" :placeholder="commentPlaceholder" rows="2" />
      </Field>
      <FieldSet v-if="refundMethods.length">
        <FieldLegend variant="label">{{ refundToLabel }}</FieldLegend>
        <RadioGroup :model-value="refundTo ?? refundMethods[0]?.value" class="grid-cols-2" @update:model-value="refundTo = $event as string">
          <Field v-for="method in refundMethods" :key="method.value" orientation="horizontal">
            <RadioGroupItem :id="`${id}-refund-${method.value}`" :value="method.value" />
            <FieldLabel :for="`${id}-refund-${method.value}`">
              <span data-slot="return-request-option" class="fui-return-request-option">{{ method.label }}</span>
            </FieldLabel>
          </Field>
        </RadioGroup>
      </FieldSet>
    </CardContent>
    <CardFooter class="justify-between gap-4">
      <div class="flex flex-col">
        <span data-slot="meta" class="fui-meta">{{ refundLabel }}</span>
        <Price :price="refund" />
      </div>
      <Button :disabled="!refundTotal" @click="submit">{{ submitLabel }}</Button>
    </CardFooter>
  </Card>
</template>
```

`components/sections/return-request/types.ts`

```ts
import type { PriceType } from '@library/components/ui/price'

export interface ReturnItem {
  id: string
  name: string
  /** The variant bought, or why the item cannot go back. */
  detail?: string
  price: PriceType
  /** False for an item that cannot be returned, such as a final-sale one. */
  returnable?: boolean
}

export interface ReturnChoice {
  value: string
  label: string
}

export interface ReturnRequestValue {
  items: string[]
  reason?: string
  comment?: string
  refundTo?: string
}
```
