# Testimonials

> A public form for people to leave a testimonial with a rating and consent, and a display as masonry wall, equal grid or spotlight, with a menu for moderation.

Source: https://docs.nasaqui.com/components/testimonials

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/testimonials.json
```

Two pieces. `TestimonialForm` collects a testimonial from a customer: name, optional role and company, a star rating,
their words, consent to publish, and a hidden honeypot. `TestimonialWall` shows the approved ones as a masonry
**wall**, an equal **grid**, or a **spotlight** that steps through one large quote at a time.

## When to use

- A "share your experience" page and the social-proof section of a landing page.

## When not to use

- Product reviews with votes and replies: build those on `Field` and lists.
- An average score in a header: use [Rating](https://docs.nasaqui.com/components/rating).

## Import

```tsx
import { TestimonialForm, TestimonialWall, validateTestimonial } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { TestimonialForm, TestimonialWall } from "@fadymondy/nasaq/web";

export const Page = ({ items }: { items: React.ComponentProps<typeof TestimonialWall>["items"] }) => (
  <>
    <TestimonialWall items={items} layout="wall" />
    <TestimonialForm onSubmit={(s) => fetch("/api/testimonials", { method: "POST", body: JSON.stringify(s) })} />
  </>
);
```

## Anatomy

```
TestimonialForm     data-slot="testimonial-form": name, role, company, (email), rating group, quote + counter, consent, honeypot
TestimonialWall     data-slot="testimonial-wall" data-layout="wall|grid|spotlight"
└─ figure           data-slot="testimonial": stars, blockquote, figcaption (Avatar, name, role and company)
```

## API

### `TestimonialForm`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `onSubmit?` | `(submission) => void \| Promise<void>` | none | `{ name, role, company, email, quote, rating, consent }`, trimmed. Bots never reach it. |
| `askRating?` / `requireRating?` | `boolean` | `true` / `false` | |
| `askEmail?` / `askRole?` | `boolean` | `false` / `true` | |
| `requireConsent?` | `boolean` | `true` | Must agree before sending. |
| `thanks?` | `ReactNode` | localised | Shown after sending. |
| `locale?` / `labels?` | | provider locale | |

### `TestimonialWall`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `Testimonial[]` | required | `{ id, name, role?, company?, quote, rating?, avatarUrl?, featured?, date? }`. |
| `layout?` | `"wall" \| "grid" \| "spotlight"` | `"wall"` | Spotlight orders featured first, then rating, then newest. |
| `itemActions?` | `(item) => ContextMenuAction[]` | none | Approve, feature, hide, delete. Opens on context-click, long-press, Shift+F10 or the Menu key. |
| `autoAdvance?` | `number` (ms) | off | Spotlight only. Pauses on hover and focus, never runs with reduced motion. |
| `empty?` | `ReactNode` | localised | |

### Logic (exported)

`validateTestimonial(input, { requireConsent?, requireRating? })`, `testimonialStats(list)`, `testimonialOrder(list)`, `testimonialStep(index, step, length)`, `TESTIMONIAL_QUOTE_MIN` and `TESTIMONIAL_QUOTE_MAX`.

## Examples

### Moderation menu

```tsx
import { TestimonialWall, type Testimonial } from "@fadymondy/nasaq/web";

export const Moderate = ({ items, hide }: { items: Testimonial[]; hide: (id: string) => void }) => (
  <TestimonialWall items={items} layout="grid" itemActions={(t) => [{ id: "hide", label: "Hide", danger: true, onSelect: () => hide(t.id) }]} />
);
```

## Accessibility

- The rating is a group of five labelled toggle buttons ("3 stars"); a rated testimonial reads "Rated 5 out of 5".
- Cards with actions are focusable so the keyboard can open their menu.
- The spotlight is a carousel with previous and next buttons and one dot button per quote, each named.

## RTL & i18n

- All text has Arabic and English defaults; quotes are shown as written, in their own direction.
- Previous and next arrows flip in Arabic. The quote length counts characters, so Arabic is not penalised.

## Styling & tokens

Cards use `--nq-card`/border tokens; stars use the accent token. The wall uses CSS columns (1, 2, then 3 from `lg`).

## Do / Don't

- **Do** ask for consent and only show approved testimonials.
- **Do** keep the honeypot.
- **Don't** invent testimonials or ratings.

## Related

- [PublicForm](https://docs.nasaqui.com/components/public-form) · [Avatar](https://docs.nasaqui.com/components/avatar) · [Rating](https://docs.nasaqui.com/components/rating)

## Lab

https://docs.nasaqui.com/?path=/docs/components-website-testimonials--docs

## Code

### React

```tsx
import { TestimonialForm, TestimonialWall } from "@fadymondy/nasaq/web";

export const Page = ({ items }: { items: React.ComponentProps<typeof TestimonialWall>["items"] }) => (
  <>
    <TestimonialWall items={items} layout="wall" />
    <TestimonialForm onSubmit={(s) => fetch("/api/testimonials", { method: "POST", body: JSON.stringify(s) })} />
  </>
);
```

### shadcn

```tsx
import { TestimonialForm, TestimonialWall } from "@/components/ui/testimonials";

export const Page = ({ items }: { items: React.ComponentProps<typeof TestimonialWall>["items"] }) => (
  <>
    <TestimonialWall items={items} layout="wall" />
    <TestimonialForm onSubmit={(s) => fetch("/api/testimonials", { method: "POST", body: JSON.stringify(s) })} />
  </>
);
```

### Vue

```vue
<script setup lang="ts">
import { NqTestimonialForm, NqTestimonialWall, type Testimonial, type TestimonialSubmission } from "@fadymondy/nasaq/vue";

const items: Testimonial[] = [
  { id: "1", name: "Layla Hassan", role: "CTO", company: "Acme", quote: "We shipped our dashboard in a week instead of a month.", rating: 5, date: "2026-09-01", featured: true },
  { id: "2", name: "Omar Khalil", role: "Designer", company: "Studio Nine", quote: "Arabic and English both just work, mirrored correctly.", rating: 5, date: "2026-08-14" },
  { id: "3", name: "Nora Adel", quote: "Solid components and clear docs.", rating: 4, date: "2026-07-02" },
];

// Your API call. Throw to keep the form.
async function send(submission: TestimonialSubmission) {
  await fetch("/api/testimonials", { method: "POST", body: JSON.stringify(submission) });
}
</script>

<template>
  <div class="grid max-w-3xl gap-8">
    <NqTestimonialWall :items="items" layout="wall" />
    <NqTestimonialForm :on-submit="send" />
  </div>
</template>
```

### Blade

```blade
@php
    $items = [
        ['id' => '1', 'name' => 'Layla Hassan', 'role' => 'CTO', 'company' => 'Acme', 'quote' => 'We shipped our dashboard in a week instead of a month.', 'rating' => 5, 'date' => '2026-09-01', 'featured' => true],
        ['id' => '2', 'name' => 'Omar Khalil', 'role' => 'Designer', 'company' => 'Studio Nine', 'quote' => 'Arabic and English both just work, mirrored correctly.', 'rating' => 5, 'date' => '2026-08-14'],
        ['id' => '3', 'name' => 'Nora Adel', 'quote' => 'Solid components and clear docs.', 'rating' => 4, 'date' => '2026-07-02'],
    ];
@endphp
<div class="grid max-w-3xl gap-8">
    <x-nq::testimonials :items="$items" layout="wall" />
    <x-nq::testimonials.form
        x-on:nq-testimonial="$event.detail.waitUntil(fetch('/api/testimonials', { method: 'POST', body: JSON.stringify($event.detail.data) }))" />
</div>
```

### HTML + Alpine

```html
<div class="grid max-w-3xl gap-8">
    <div data-slot="testimonial-wall" data-layout="wall" x-data="nqTestimonialWall(JSON.parse('{\u0022count\u0022:3,\u0022autoAdvance\u0022:0}'))"
        class="columns-1 gap-4 sm:columns-2 lg:columns-3 [&>*]:mb-4 [&>*]:break-inside-avoid w-full">
                    <figure data-slot="testimonial" data-id="1" class="flex min-w-0 flex-col gap-4 rounded-card border border-border bg-card outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus p-5">
        <span role="img" aria-label="Rated 5 out of 5" class="inline-flex gap-0.5">
                    <svg aria-hidden="true" class="size-4 fill-nq-accent text-nq-accent" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
</svg>                    <svg aria-hidden="true" class="size-4 fill-nq-accent text-nq-accent" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
</svg>                    <svg aria-hidden="true" class="size-4 fill-nq-accent text-nq-accent" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
</svg>                    <svg aria-hidden="true" class="size-4 fill-nq-accent text-nq-accent" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
</svg>                    <svg aria-hidden="true" class="size-4 fill-nq-accent text-nq-accent" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
</svg>            </span>
<blockquote class="text-pretty text-body">We shipped our dashboard in a week instead of a month.</blockquote>
<figcaption class="flex min-w-0 items-center gap-3">
    <span data-slot="avatar" x-data="nqAvatar(0)"
    class="inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-secondary align-middle font-medium text-secondary-foreground size-8 text-caption rounded-full">
                    <span data-slot="avatar-fallback" class="flex size-full items-center justify-center" x-bind="fallback"
             role="img" aria-label="Layla Hassan" >LH</span>
    </span>
    <span class="flex min-w-0 flex-col">
        <span class="truncate font-medium text-body-sm">Layla Hassan</span>
        <span class="truncate text-caption text-muted-foreground">CTO, Acme</span>    </span>
</figcaption>
    </figure>
                    <figure data-slot="testimonial" data-id="2" class="flex min-w-0 flex-col gap-4 rounded-card border border-border bg-card outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus p-5">
        <span role="img" aria-label="Rated 5 out of 5" class="inline-flex gap-0.5">
                    <svg aria-hidden="true" class="size-4 fill-nq-accent text-nq-accent" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
</svg>                    <svg aria-hidden="true" class="size-4 fill-nq-accent text-nq-accent" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
</svg>                    <svg aria-hidden="true" class="size-4 fill-nq-accent text-nq-accent" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
</svg>                    <svg aria-hidden="true" class="size-4 fill-nq-accent text-nq-accent" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
</svg>                    <svg aria-hidden="true" class="size-4 fill-nq-accent text-nq-accent" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
</svg>            </span>
<blockquote class="text-pretty text-body">Arabic and English both just work, mirrored correctly.</blockquote>
<figcaption class="flex min-w-0 items-center gap-3">
    <span data-slot="avatar" x-data="nqAvatar(0)"
    class="inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-secondary align-middle font-medium text-secondary-foreground size-8 text-caption rounded-full">
                    <span data-slot="avatar-fallback" class="flex size-full items-center justify-center" x-bind="fallback"
             role="img" aria-label="Omar Khalil" >OK</span>
    </span>
    <span class="flex min-w-0 flex-col">
        <span class="truncate font-medium text-body-sm">Omar Khalil</span>
        <span class="truncate text-caption text-muted-foreground">Designer, Studio Nine</span>    </span>
</figcaption>
    </figure>
                    <figure data-slot="testimonial" data-id="3" class="flex min-w-0 flex-col gap-4 rounded-card border border-border bg-card outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus p-5">
        <span role="img" aria-label="Rated 4 out of 5" class="inline-flex gap-0.5">
                    <svg aria-hidden="true" class="size-4 fill-nq-accent text-nq-accent" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
</svg>                    <svg aria-hidden="true" class="size-4 fill-nq-accent text-nq-accent" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
</svg>                    <svg aria-hidden="true" class="size-4 fill-nq-accent text-nq-accent" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
</svg>                    <svg aria-hidden="true" class="size-4 fill-nq-accent text-nq-accent" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
</svg>                    <svg aria-hidden="true" class="size-4 text-nq-line-strong" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
</svg>            </span>
<blockquote class="text-pretty text-body">Solid components and clear docs.</blockquote>
<figcaption class="flex min-w-0 items-center gap-3">
    <span data-slot="avatar" x-data="nqAvatar(0)"
    class="inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-secondary align-middle font-medium text-secondary-foreground size-8 text-caption rounded-full">
                    <span data-slot="avatar-fallback" class="flex size-full items-center justify-center" x-bind="fallback"
             role="img" aria-label="Nora Adel" >NA</span>
    </span>
    <span class="flex min-w-0 flex-col">
        <span class="truncate font-medium text-body-sm">Nora Adel</span>
            </span>
</figcaption>
    </figure>
            </div>
    <form data-slot="testimonial-form" novalidate x-data="nqTestimonialForm(JSON.parse('{\u0022requireConsent\u0022:true,\u0022requireRating\u0022:false,\u0022errors\u0022:{\u0022name\u0022:\u0022Enter your name.\u0022,\u0022quote-short\u0022:\u0022Write a little more.\u0022,\u0022quote-long\u0022:\u0022That is too long.\u0022,\u0022rating\u0022:\u0022Pick a rating from 1 to 5.\u0022,\u0022email\u0022:\u0022Enter a valid email address.\u0022,\u0022consent\u0022:\u0022Please agree so we can show it.\u0022},\u0022failed\u0022:\u0022Something went wrong. Please try again.\u0022,\u0022locale\u0022:\u0022en\u0022}'))" x-bind:data-state="done ? 'done' : null"
    x-on:submit.prevent="onSubmit()" x-on:nq-testimonial="$event.detail.waitUntil(fetch(&#039;/api/testimonials&#039;, { method: &#039;POST&#039;, body: JSON.stringify($event.detail.data) }))" class="relative flex w-full flex-col gap-4">
    <div x-show="! done" class="flex flex-col gap-4">
        <div data-slot="field" x-data="nqField(false)" x-modelable="invalid" x-id="['nq-field']"
         data-valid     x-model="bad.name" data-field="name" class="flex flex-col gap-1.5">
    <label data-slot="field-label"  class="text-label text-foreground data-disabled:opacity-50">Your name</label>
            <input data-slot="input" type="text"
                    name="name" autocomplete="name" x-model="v.name" class="w-full min-w-0 rounded-control border border-input bg-card px-3 text-body text-foreground min-h-[var(--nq-touch-min,0px)] transition-colors duration-150 ease-nq outline-none placeholder:text-muted-foreground focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] h-control">
            <div data-slot="field-error" role="alert" x-show="invalid"  style="display: none"  class="text-caption text-nq-danger-text"><span x-text="fe.name"></span></div>
</div>
                    <div class="grid gap-4 sm:grid-cols-2">
                <div data-slot="field" x-data="nqField(false)" x-modelable="invalid" x-id="['nq-field']"
         data-valid     class="flex flex-col gap-1.5">
    <label data-slot="field-label"  class="text-label text-foreground data-disabled:opacity-50">Role</label>
                    <input data-slot="input" type="text"
                    name="role" x-model="v.role" class="w-full min-w-0 rounded-control border border-input bg-card px-3 text-body text-foreground min-h-[var(--nq-touch-min,0px)] transition-colors duration-150 ease-nq outline-none placeholder:text-muted-foreground focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] h-control">
</div>
                <div data-slot="field" x-data="nqField(false)" x-modelable="invalid" x-id="['nq-field']"
         data-valid     class="flex flex-col gap-1.5">
    <label data-slot="field-label"  class="text-label text-foreground data-disabled:opacity-50">Company</label>
                    <input data-slot="input" type="text"
                    name="company" autocomplete="organization" x-model="v.company" class="w-full min-w-0 rounded-control border border-input bg-card px-3 text-body text-foreground min-h-[var(--nq-touch-min,0px)] transition-colors duration-150 ease-nq outline-none placeholder:text-muted-foreground focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] h-control">
</div>
            </div>
                                    <div data-slot="field" x-data="nqField(false)" x-modelable="invalid" x-id="['nq-field']"
         data-valid     x-model="bad.rating" data-field="rating" class="flex flex-col gap-1.5">
    <label data-slot="field-label"  class="text-label text-foreground data-disabled:opacity-50">Rating</label>
                <div role="group" aria-label="Rating" class="flex gap-1">
                                            <button type="button" aria-label="1 star" x-bind:aria-pressed="v.rating === 1 ? 'true' : 'false'" x-on:click="setRating(1)"
                            class="rounded-control p-1 outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus">
                            <svg aria-hidden="true" x-bind:class="starClass(1)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
</svg>                        </button>
                                            <button type="button" aria-label="2 stars" x-bind:aria-pressed="v.rating === 2 ? 'true' : 'false'" x-on:click="setRating(2)"
                            class="rounded-control p-1 outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus">
                            <svg aria-hidden="true" x-bind:class="starClass(2)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
</svg>                        </button>
                                            <button type="button" aria-label="3 stars" x-bind:aria-pressed="v.rating === 3 ? 'true' : 'false'" x-on:click="setRating(3)"
                            class="rounded-control p-1 outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus">
                            <svg aria-hidden="true" x-bind:class="starClass(3)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
</svg>                        </button>
                                            <button type="button" aria-label="4 stars" x-bind:aria-pressed="v.rating === 4 ? 'true' : 'false'" x-on:click="setRating(4)"
                            class="rounded-control p-1 outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus">
                            <svg aria-hidden="true" x-bind:class="starClass(4)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
</svg>                        </button>
                                            <button type="button" aria-label="5 stars" x-bind:aria-pressed="v.rating === 5 ? 'true' : 'false'" x-on:click="setRating(5)"
                            class="rounded-control p-1 outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus">
                            <svg aria-hidden="true" x-bind:class="starClass(5)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
</svg>                        </button>
                                    </div>
                <input type="hidden" name="rating" x-bind:value="v.rating ?? ''">
                <div data-slot="field-error" role="alert" x-show="invalid"  style="display: none"  class="text-caption text-nq-danger-text"><span x-text="fe.rating"></span></div>
</div>
                <div data-slot="field" x-data="nqField(false)" x-modelable="invalid" x-id="['nq-field']"
         data-valid     x-model="bad.quote" data-field="quote" class="flex flex-col gap-1.5">
    <label data-slot="field-label"  class="text-label text-foreground data-disabled:opacity-50">Your testimonial</label>
            <textarea data-slot="textarea"
                name="quote" rows="5" x-model="v.quote" class="w-full min-w-0 rounded-control border border-input bg-card px-3 text-body text-foreground transition-colors duration-150 ease-nq outline-none placeholder:text-muted-foreground focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] min-h-20 py-2"></textarea>
            <p data-slot="field-description" class="text-caption text-muted-foreground"><span class="tabular-nums" x-text="num(length())">0</span> / Up to 600 characters.</p>
            <div data-slot="field-error" role="alert" x-show="invalid"  style="display: none"  class="text-caption text-nq-danger-text"><span x-text="fe.quote"></span></div>
</div>
                    <div data-slot="field" x-data="nqField(false)" x-modelable="invalid" x-id="['nq-field']"
         data-valid     x-model="bad.consent" data-field="consent" class="flex flex-col gap-1.5">
    <label class="flex items-start gap-2 text-body">
                    <button type="button" role="checkbox" data-slot="checkbox" x-data="nqCheckbox(false, false)" x-modelable="checked" x-bind="root"
    aria-checked="false"
     data-unchecked                 x-model="v.consent" class="relative inline-flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-nq-line-strong bg-card text-primary-foreground outline-none transition-colors duration-150 ease-nq data-checked:border-primary data-checked:bg-primary data-indeterminate:border-primary data-indeterminate:bg-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus data-disabled:cursor-not-allowed data-disabled:opacity-50 after:absolute after:-inset-1 mt-1">
    <span data-slot="checkbox-indicator" class="flex items-center justify-center [&_svg]:size-3 [&_svg]:stroke-3" x-show="checked || indeterminate"  style="display: none" >
        <svg aria-hidden="true" x-show="indeterminate" style="display: none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M5 12h14"/>
</svg>        <svg aria-hidden="true" x-show="! indeterminate" style="" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg>    </span>
    <input type="hidden" name="consent" value="1" x-bind:disabled="! checked"  disabled ></button>
                    <span>You may show my name and words on your website.</span>
                </label>
                <div data-slot="field-error" role="alert" x-show="invalid"  style="display: none"  class="text-caption text-nq-danger-text"><span x-text="fe.consent"></span></div>
</div>
                <div aria-hidden="true" class="pointer-events-none absolute -z-10 h-0 w-0 overflow-hidden opacity-0">
            <label>
                Leave this field empty
                <input type="text" name="website_url" tabindex="-1" autocomplete="off" x-model="trap">
            </label>
        </div>
        <p role="alert" class="text-body-sm text-nq-danger-text" x-show="error" style="display: none" x-text="error"></p>
        <div>
            <button data-slot="button"
     type="submit"                         x-bind:disabled="pending" x-bind:aria-busy="pending ? &#039;true&#039; : null" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)]">
        Send testimonial</button>
        </div>
    </div>
    <div data-slot="testimonial-form-done" data-state="done" role="status" x-show="done" style="display: none" class="flex flex-col items-start gap-3 rounded-card border border-border bg-card p-5">
        <svg aria-hidden="true" class="size-6 text-nq-success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <path d="m9 12 2 2 4-4"/>
</svg>        <p class="text-body">Thank you! Your testimonial is on its way to be reviewed.</p>
        <button data-slot="button"
     type="button"                         x-on:click="again()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground underline decoration-nq-line underline-offset-4 hover:decoration-current h-auto px-0">
        Send another</button>
    </div>
</form>
</div>
```
