10.1k
New Components: Field, Input Group, Item and more

Building Blocks for the Web

Clean, modern building blocks. Copy and paste into your apps. Works with all Vue frameworks. Open Source. Free forever.

Files
components/CategoryPreviewCards.vue
<script setup lang="ts">
import { Image } from '@/components/ui/image'

interface CategoryItem {
  title: string
  href: string
  image: string
  imageAlt?: string
}

withDefaults(defineProps<{
  heading?: string
  browseAllLabel?: string
  browseAllHref?: string
  categories?: CategoryItem[]
}>(), {
  heading: 'Shop by Category',
  browseAllLabel: 'Browse all categories',
  browseAllHref: '/categories',
  categories: () => [
    {
      title: 'New Arrivals',
      href: '/category/new-arrivals',
      image: '/placeholder.svg',
      imageAlt: 'New arrivals collection',
    },
    {
      title: 'Productivity',
      href: '/category/productivity',
      image: '/placeholder.svg',
      imageAlt: 'Productivity collection',
    },
    {
      title: 'Workspace',
      href: '/category/workspace',
      image: '/placeholder.svg',
      imageAlt: 'Workspace collection',
    },
    {
      title: 'Accessories',
      href: '/category/accessories',
      image: '/placeholder.svg',
      imageAlt: 'Accessories collection',
    },
    {
      title: 'Sale',
      href: '/category/sale',
      image: '/placeholder.svg',
      imageAlt: 'Sale collection',
    },
  ],
})
</script>

<template>
  <section class="bg-background">
    <div class="mx-auto max-w-7xl px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
      <div class="flex items-center justify-between">
        <h2 class="text-2xl font-bold tracking-tight">
          {{ heading }}
        </h2>
        <a
          :href="browseAllHref"
          class="hidden text-sm font-semibold text-primary hover:text-primary/80 sm:block"
        >
          {{ browseAllLabel }}
          <span aria-hidden="true"> &rarr;</span>
        </a>
      </div>

      <div class="mt-4 flow-root">
        <div class="-my-2">
          <div class="relative box-content h-80 overflow-x-auto py-2 xl:overflow-visible">
            <div class="absolute flex gap-x-8 px-4 sm:px-6 lg:px-8 xl:relative xl:grid xl:grid-cols-5 xl:gap-x-8 xl:px-0">
              <a
                v-for="category in categories"
                :key="category.title"
                :href="category.href"
                class="group relative flex h-80 w-56 flex-col overflow-hidden rounded-lg p-6 xl:w-auto"
              >
                <span aria-hidden="true" class="absolute inset-0">
                  <Image
                    :src="category.image"
                    :alt="category.imageAlt || category.title"
                    class="size-full object-cover transition-opacity group-hover:opacity-75"
                  />
                </span>
                <span
                  aria-hidden="true"
                  class="absolute inset-x-0 bottom-0 h-2/3 bg-gradient-to-t from-background/80 to-transparent"
                />
                <span class="relative mt-auto text-center text-xl font-bold text-foreground">
                  {{ category.title }}
                </span>
              </a>
            </div>
          </div>
        </div>
      </div>

      <div class="mt-6 sm:hidden">
        <a
          :href="browseAllHref"
          class="block text-sm font-semibold text-primary hover:text-primary/80"
        >
          {{ browseAllLabel }}
          <span aria-hidden="true"> &rarr;</span>
        </a>
      </div>
    </div>
  </section>
</template>
A horizontally scrollable category preview with image cards, gradient overlays, and a 5-column grid on desktop.
category-preview-cards
Files
components/CategoryPreviewGrid.vue
<script setup lang="ts">
import { Image } from '@/components/ui/image'

interface CategoryItem {
  title: string
  href: string
  image: string
  imageAlt?: string
}

withDefaults(defineProps<{
  heading?: string
  browseAllLabel?: string
  browseAllHref?: string
  categories?: CategoryItem[]
}>(), {
  heading: 'Shop by Category',
  browseAllLabel: 'Browse all categories',
  browseAllHref: '/categories',
  categories: () => [
    {
      title: 'New Arrivals',
      href: '/category/new-arrivals',
      image: '/placeholder.svg',
      imageAlt: 'New arrivals collection',
    },
    {
      title: 'Accessories',
      href: '/category/accessories',
      image: '/placeholder.svg',
      imageAlt: 'Accessories collection',
    },
    {
      title: 'Workspace',
      href: '/category/workspace',
      image: '/placeholder.svg',
      imageAlt: 'Workspace collection',
    },
  ],
})
</script>

<template>
  <section class="bg-muted/40">
    <div class="mx-auto max-w-7xl px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
      <div class="sm:flex sm:items-baseline sm:justify-between">
        <h2 class="text-2xl font-bold tracking-tight">
          {{ heading }}
        </h2>
        <a
          :href="browseAllHref"
          class="hidden text-sm font-semibold text-primary hover:text-primary/80 sm:block"
        >
          {{ browseAllLabel }}
          <span aria-hidden="true"> &rarr;</span>
        </a>
      </div>

      <div class="mt-6 grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:grid-rows-2 sm:gap-x-6 lg:gap-8">
        <a
          v-for="(category, index) in categories.slice(0, 3)"
          :key="category.title"
          :href="category.href"
          class="group relative overflow-hidden rounded-lg"
          :class="[
            index === 0
              ? 'aspect-[2/1] sm:row-span-2 sm:aspect-square'
              : 'aspect-[2/1] sm:aspect-auto',
          ]"
        >
          <Image
            :src="category.image"
            :alt="category.imageAlt || category.title"
            class="absolute inset-0 size-full object-cover transition-opacity group-hover:opacity-75"
          />
          <div
            aria-hidden="true"
            class="absolute inset-0 bg-gradient-to-b from-transparent to-black/50"
          />
          <div class="absolute inset-0 flex items-end p-6">
            <div>
              <h3 class="font-semibold text-white">
                {{ category.title }}
              </h3>
              <p aria-hidden="true" class="mt-1 text-sm text-white/80">
                Shop now
              </p>
            </div>
          </div>
        </a>
      </div>

      <div class="mt-6 sm:hidden">
        <a
          :href="browseAllHref"
          class="block text-sm font-semibold text-primary hover:text-primary/80"
        >
          {{ browseAllLabel }}
          <span aria-hidden="true"> &rarr;</span>
        </a>
      </div>
    </div>
  </section>
</template>
A category preview section with image backgrounds, featuring a large hero category and two smaller category cards in a grid layout.
category-preview-grid
Files
components/HeroCentered.vue
<script setup lang="ts">
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'

interface Cta {
  text: string
  href: string
}

withDefaults(defineProps<{
  badge?: string
  headline?: string
  description?: string
  image?: string
  primaryCta?: Cta
  secondaryCta?: Cta
}>(), {
  badge: 'New Arrival',
  headline: 'Engineered for the Mountains',
  description: 'Our premium collection of outdoor jackets combines cutting-edge technology with timeless design. Built to withstand the elements while keeping you comfortable on every adventure.',
  image: 'https://cdn.demo-shop.com/media/1366/768/e0/86/58/1740128566/mann_jacken.png?ts=1740128566',
  primaryCta: () => ({ text: 'Shop Collection', href: '/collection/outdoor' }),
  secondaryCta: () => ({ text: 'Learn More', href: '/about' }),
})
</script>

<template>
  <section class="grid min-h-[500px] grid-cols-1 lg:grid-cols-2">
    <div class="flex flex-col justify-center p-8 md:p-12 lg:p-16">
      <Badge v-if="badge" variant="secondary" class="w-fit">
        {{ badge }}
      </Badge>
      <h1 class="mt-4 text-3xl font-bold tracking-tight md:text-5xl">
        {{ headline }}
      </h1>
      <p class="mt-4 max-w-lg text-lg text-muted-foreground">
        {{ description }}
      </p>
      <div class="mt-8 flex flex-col gap-4 sm:flex-row">
        <Button as="a" :href="primaryCta.href" size="lg">
          {{ primaryCta.text }}
        </Button>
        <Button as="a" :href="secondaryCta.href" variant="outline" size="lg">
          {{ secondaryCta.text }}
        </Button>
      </div>
    </div>
    <div class="relative order-first min-h-[300px] lg:order-none lg:min-h-0">
      <img
        :src="image"
        :alt="headline"
        class="absolute inset-0 h-full w-full object-cover"
      />
    </div>
  </section>
</template>
A split hero section with image on the right, badge, headline, description, and dual call-to-action buttons on the left.
hero-centered
Files
components/HeroOverlay.vue
<script setup lang="ts">
import { Button } from '@/components/ui/button'

interface Cta {
  text: string
  href: string
}

withDefaults(defineProps<{
  headline?: string
  subheadline?: string
  cta?: Cta
}>(), {
  headline: 'Gear Up for Your Next Adventure',
  subheadline: 'Explore our curated collection of premium outdoor equipment. From summit to trail, we have everything you need.',
  cta: () => ({ text: 'Browse All Products', href: '/products' }),
})
</script>

<template>
  <section class="bg-muted">
    <div class="mx-auto max-w-3xl px-6 py-16 text-center md:py-24">
      <h1 class="text-4xl font-bold tracking-tight md:text-6xl">
        {{ headline }}
      </h1>
      <p class="mx-auto mt-4 max-w-2xl text-lg text-muted-foreground">
        {{ subheadline }}
      </p>
      <div class="mt-8">
        <Button as="a" :href="cta.href" size="lg">
          {{ cta.text }}
        </Button>
      </div>
    </div>
  </section>
</template>
A minimal text-only hero section with large headline, subheadline, and a single call-to-action button on a muted background.
hero-overlay
Files
components/HeroSplit.vue
<script setup lang="ts">
import { Button } from '@/components/ui/button'

interface Cta {
  text: string
  href: string
}

withDefaults(defineProps<{
  backgroundImage?: string
  headline?: string
  subheadline?: string
  primaryCta?: Cta
  secondaryCta?: Cta
}>(), {
  backgroundImage: 'https://cdn.demo-shop.com/media/1366/768/64/de/79/1741783686/u3398487194_men_on_a_mountain_wearing_ski_helmets_--ar_169_--_336a76fb-ec05-4251-93d8-7c2c9428e902_3.png?ts=1741783686',
  headline: 'Winter Collection 2025',
  subheadline: 'Discover our latest outdoor gear built for the mountains. Premium quality, engineered for adventure.',
  primaryCta: () => ({ text: 'Shop Now', href: '/collection/winter' }),
  secondaryCta: () => ({ text: 'Learn More', href: '/about' }),
})
</script>

<template>
  <section class="relative flex min-h-[500px] items-center justify-center overflow-hidden md:min-h-[600px]">
    <img
      :src="backgroundImage"
      :alt="headline"
      class="absolute inset-0 h-full w-full object-cover"
    />
    <div class="absolute inset-0 bg-black/50" />
    <div class="relative z-10 mx-auto max-w-3xl px-6 text-center">
      <h1 class="text-4xl font-bold tracking-tight text-white md:text-6xl">
        {{ headline }}
      </h1>
      <p class="mx-auto mt-4 max-w-2xl text-lg text-white/90 md:text-xl">
        {{ subheadline }}
      </p>
      <div class="mt-8 flex flex-col justify-center gap-4 sm:flex-row">
        <Button as="a" :href="primaryCta.href" size="lg" class="bg-white text-black hover:bg-white/90">
          {{ primaryCta.text }}
        </Button>
        <Button as="a" :href="secondaryCta.href" variant="outline" size="lg" class="!border-white !bg-transparent !text-white hover:!bg-white/10">
          {{ secondaryCta.text }}
        </Button>
      </div>
    </div>
  </section>
</template>
A full-width hero banner with background image, dark overlay, centered headline, and dual call-to-action buttons.
hero-split
Files
components/IncentiveBar.vue
<script lang="ts">
import { TruckIcon, ShieldCheckIcon, RefreshCcwIcon } from 'lucide-vue-next'
import type { Component } from 'vue'

interface IncentiveItem {
  name: string
  description: string
  icon: Component
}

const defaultIncentives: IncentiveItem[] = [
  {
    name: 'Free shipping over €150',
    description: 'All orders over €150 ship free across Europe. Most orders arrive within 2–4 business days so you can hit the slopes sooner.',
    icon: TruckIcon,
  },
  {
    name: '2-year warranty',
    description: 'Every product is covered by our 2-year warranty against manufacturing defects. If something breaks, we replace it — no questions asked.',
    icon: ShieldCheckIcon,
  },
  {
    name: 'Easy returns',
    description: 'Changed your mind? Return any unused item within 30 days for a full refund. We even cover return shipping on defective products.',
    icon: RefreshCcwIcon,
  },
]
</script>

<script setup lang="ts">
import Image from '@/components/ui/image/Image.vue'

withDefaults(
  defineProps<{
    title?: string
    description?: string
    image?: string
    imageAlt?: string
    incentives?: IncentiveItem[]
  }>(),
  {
    title: 'We built our business on great customer service',
    description: 'From expert gear advice to hassle-free returns, we stand behind every product we sell. Our team of riders and outdoor enthusiasts is here to help you find the right gear and keep it performing season after season.',
    image: 'https://cdn.demo-shop.com/media/1366/768/64/de/79/1741783686/u3398487194_men_on_a_mountain_wearing_ski_helmets_--ar_169_--_336a76fb-ec05-4251-93d8-7c2c9428e902_3.png?ts=1741783686',
    imageAlt: 'Skiers on a mountain wearing helmets',
    incentives: () => defaultIncentives,
  },
)
</script>

<template>
  <div class="bg-muted/50">
    <div class="mx-auto max-w-7xl px-4 py-24 sm:px-6 sm:py-32 lg:px-8">
      <!-- Split header -->
      <div class="grid grid-cols-1 items-center gap-x-16 gap-y-10 lg:grid-cols-2">
        <div>
          <h2 class="text-4xl font-bold tracking-tight">{{ title }}</h2>
          <p class="mt-4 text-muted-foreground">{{ description }}</p>
        </div>
        <Image
          :src="image"
          :alt="imageAlt"
          class="block aspect-3/2 w-full rounded-lg"
        />
      </div>

      <!-- 3-col incentives grid -->
      <div class="mt-16 grid grid-cols-1 gap-x-8 gap-y-10 lg:grid-cols-3">
        <div v-for="incentive in incentives" :key="incentive.name" class="sm:flex lg:block">
          <div class="sm:shrink-0">
            <component
              :is="incentive.icon"
              class="size-10 text-primary"
            />
          </div>
          <div class="mt-4 sm:mt-0 sm:ml-6 lg:mt-6 lg:ml-0">
            <h3 class="text-sm font-medium">{{ incentive.name }}</h3>
            <p class="mt-2 text-sm text-muted-foreground">{{ incentive.description }}</p>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
Trust-builder section with split header and 3-col incentive icons.
incentive-bar
Files
components/PromoBanner.vue
<script setup lang="ts">
import { Button } from '@/components/ui/button'
import { Image } from '@/components/ui/image'

withDefaults(defineProps<{
  heading?: string
  description?: string
  ctaLabel?: string
  ctaHref?: string
  backgroundImage?: string
}>(), {
  heading: 'Level up your winter gear',
  description: 'Explore our latest collection of premium outdoor jackets, designed for the harshest conditions while keeping you stylish on the slopes.',
  ctaLabel: 'Shop Collection',
  ctaHref: '/collections/winter',
  backgroundImage: '/placeholder.svg',
})
</script>

<template>
  <section class="bg-background">
    <div class="mx-auto max-w-7xl px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
      <div class="relative overflow-hidden rounded-lg">
        <div class="absolute inset-0">
          <Image
            :src="backgroundImage"
            alt=""
            class="size-full object-cover"
          />
        </div>
        <div class="relative bg-foreground/75 px-6 py-32 sm:px-12 sm:py-40 lg:px-16">
          <div class="relative mx-auto flex max-w-3xl flex-col items-center text-center">
            <h2 class="text-3xl font-bold tracking-tight text-background sm:text-4xl">
              {{ heading }}
            </h2>
            <p class="mt-3 text-xl text-background/80">
              {{ description }}
            </p>
            <Button
              as="a"
              :href="ctaHref"
              color="inverted"
              size="lg"
              class="mt-8"
            >
              {{ ctaLabel }}
            </Button>
          </div>
        </div>
      </div>
    </div>
  </section>
</template>
A promotional banner with background image, dark overlay, centered headline, and call-to-action button.
promo-banner
Files
components/PromoFeature.vue
<script setup lang="ts">
import { Button } from '@/components/ui/button'
import { Image } from '@/components/ui/image'

withDefaults(defineProps<{
  heading?: string
  description?: string
  ctaLabel?: string
  ctaHref?: string
  backgroundImage?: string
}>(), {
  heading: 'New arrivals are here',
  description: 'The new arrivals have, well, newly arrived. Check out the latest options from our summer small-batch release while they\'re still in stock.',
  ctaLabel: 'Shop New Arrivals',
  ctaHref: '/collections/new-arrivals',
  backgroundImage: '/placeholder.svg',
})
</script>

<template>
  <section class="relative bg-foreground">
    <div aria-hidden="true" class="absolute inset-0 overflow-hidden">
      <Image
        :src="backgroundImage"
        alt=""
        class="size-full object-cover"
      />
    </div>
    <div aria-hidden="true" class="absolute inset-0 bg-foreground/50" />

    <div class="relative mx-auto flex max-w-3xl flex-col items-center px-6 py-32 text-center sm:py-64 lg:px-0">
      <h1 class="text-4xl font-bold tracking-tight text-background lg:text-6xl">
        {{ heading }}
      </h1>
      <p class="mt-4 text-xl text-background/80">
        {{ description }}
      </p>
      <Button
        as="a"
        :href="ctaHref"
        color="inverted"
        size="lg"
        class="mt-8"
      >
        {{ ctaLabel }}
      </Button>
    </div>
  </section>
</template>
A full-width hero section with background image, dark overlay, large centered headline, and call-to-action button.
promo-feature