# Feature Comparison Table

> A table that lines up features across plans or models with clear check marks. Helps shoppers weigh every option and pick the right product fast.

A shadcn/ui block from the [@shadcn-ui-blocks](https://www.shadcn-ui-blocks.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/shadcn-ui-blocks/ecommerce-product-features-feature-comparison-table).

- Page, with a live preview: https://shadcn.exchange/r/shadcn-ui-blocks/ecommerce-product-features-feature-comparison-table
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/shadcn-ui-blocks/ecommerce-product-features-feature-comparison-table-light-45ff5397457bda1a.png
- Registry item: https://www.shadcn-ui-blocks.com/r/ecommerce-product-features-feature-comparison-table.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://www.shadcn-ui-blocks.com/r/ecommerce-product-features-feature-comparison-table.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `class-variance-authority`, `lucide-react`, `radix-ui`

## Files

### `components/blocks/ecommerce/product-features/feature-comparison-table.tsx`

```tsx
import { useState } from 'react';

import { ArrowRight, Check, HelpCircle, Info, X } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from '@/components/ui/tooltip';

export default function FeatureComparisonTable() {
  const [activeTier, setActiveTier] = useState<string>('premium');

  const tiers = [
    {
      id: 'basic',
      name: 'Basic',
      price: '$149',
      description: 'All the essentials for daily commuting',
      image:
        'https://images.unsplash.com/photo-1485965120184-e220f721d03e?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
      popular: false,
    },
    {
      id: 'premium',
      name: 'Premium',
      price: '$249',
      description: 'Advanced features for the cycling enthusiast',
      image:
        'https://images.unsplash.com/photo-1532298229144-0ec0c57515c7?q=80&w=1976&auto=format&fit=crop',
      popular: true,
    },
    {
      id: 'elite',
      name: 'Elite Carbon',
      price: '$399',
      description: 'Professional grade components for serious riders',
      image:
        'https://images.unsplash.com/photo-1507035895480-2b3156c31fc8?q=80&w=2070&auto=format&fit=crop',
      popular: false,
    },
  ];

  const featureCategories = [
    {
      name: 'Frame & Construction',
      features: [
        {
          name: 'Frame Material',
          values: {
            basic: 'Aluminum Alloy',
            premium: 'Hydroformed Aluminum',
            elite: 'Carbon Fiber Composite',
          },
          tooltip: 'The primary material used in the bike frame construction',
        },
        {
          name: 'Weight',
          values: {
            basic: '12.5 kg (27.5 lbs)',
            premium: '10.2 kg (22.5 lbs)',
            elite: '8.3 kg (18.3 lbs)',
          },
          tooltip: 'Total weight of the assembled bicycle',
        },
        {
          name: 'Internal Cable Routing',
          values: {
            basic: false,
            premium: true,
            elite: true,
          },
          tooltip:
            'Cables run inside the frame for a cleaner look and protection',
        },
      ],
    },
    {
      name: 'Components',
      features: [
        {
          name: 'Drivetrain',
          values: {
            basic: 'Shimano Altus (8-speed)',
            premium: 'Shimano Deore (10-speed)',
            elite: 'Shimano XT (12-speed)',
          },
          tooltip: 'The gear and shifting system',
        },
        {
          name: 'Brakes',
          values: {
            basic: 'Mechanical Disc',
            premium: 'Hydraulic Disc',
            elite: 'Shimano XT Hydraulic Disc',
          },
          tooltip: 'Brake system type and quality',
        },
        {
          name: 'Wheelset',
          values: {
            basic: 'Aluminum Double Wall',
            premium: 'Tubeless Ready Aluminum',
            elite: 'Carbon Fiber Tubeless Ready',
          },
          tooltip: 'Type and material of the wheels',
        },
      ],
    },
    {
      name: 'Comfort & Accessories',
      features: [
        {
          name: 'Suspension Fork',
          values: {
            basic: '100mm Travel',
            premium: '120mm Travel, Adjustable',
            elite: '120mm Travel, Fully Adjustable',
          },
          tooltip: 'Front suspension system for absorbing impacts',
        },
        {
          name: 'Dropper Seatpost',
          values: {
            basic: false,
            premium: true,
            elite: true,
          },
          tooltip:
            'Height-adjustable seatpost that can be controlled while riding',
        },
        {
          name: 'Tubeless Tires',
          values: {
            basic: false,
            premium: true,
            elite: true,
          },
          tooltip:
            "Tires that don't use inner tubes, providing better puncture resistance",
        },
      ],
    },
    {
      name: 'Support & Warranty',
      features: [
        {
          name: 'Frame Warranty',
          values: {
            basic: '5 Years',
            premium: 'Lifetime',
            elite: 'Lifetime',
          },
          tooltip: 'Length of manufacturer warranty on the frame',
        },
        {
          name: 'Components Warranty',
          values: {
            basic: '1 Year',
            premium: '2 Years',
            elite: '3 Years',
          },
          tooltip: 'Length of manufacturer warranty on components',
        },
        {
          name: 'Professional Bike Fitting',
          values: {
            basic: false,
            premium: true,
            elite: true,
          },
          tooltip: 'Expert service to adjust the bike to your body dimensions',
        },
      ],
    },
  ];

  return (
    <section className="bg-background w-full py-12 md:py-24 lg:py-32">
      <div className="container mx-auto px-4 md:px-6 2xl:max-w-[1400px]">
        <div className="mb-8 flex flex-col items-center space-y-4 text-center md:mb-16">
          <Badge className="px-3.5 py-1.5">Compare Models</Badge>
          <h2 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl">
            Find your perfect ride
          </h2>
          <p className="text-muted-foreground max-w-[700px] md:text-xl/relaxed">
            Compare our mountain bike models to find the right balance of
            performance and value for your riding style.
          </p>
        </div>

        {/* Product tier selector for mobile */}
        <div className="mb-8 md:hidden">
          <select
            className="w-full rounded-md border p-2"
            value={activeTier}
            onChange={(e) => setActiveTier(e.target.value)}
          >
            {tiers.map((tier) => (
              <option key={tier.id} value={tier.id}>
                {tier.name} - {tier.price}
              </option>
            ))}
          </select>
        </div>

        {/* Product tier cards */}
        <div className="mb-8 hidden grid-cols-4 gap-4 md:grid">
          <div className="flex items-end p-6">
            <h3 className="text-xl font-semibold">Mountain Bike Models</h3>
          </div>
          {tiers.map((tier) => (
            <div
              key={tier.id}
              className={`relative flex h-full flex-col rounded-xl border p-6 transition-all ${
                tier.id === activeTier
                  ? 'ring-primary shadow-lg ring-2'
                  : 'hover:border-primary/50 cursor-pointer hover:shadow-md'
              }`}
              onClick={() => setActiveTier(tier.id)}
            >
              {tier.popular && (
                <Badge className="absolute top-0 right-0 translate-x-1/4 -translate-y-1/2 px-3 py-1">
                  Most Popular
                </Badge>
              )}
              <div className="relative mb-4 aspect-[4/3] w-full overflow-hidden rounded-lg">
                <img
                  src={tier.image}
                  alt={tier.name}
                  className="object-cover"
                  sizes="(max-width: 768px) 100vw, 300px"
                />
              </div>
              <h3 className="text-lg font-semibold">{tier.name}</h3>
              <div className="mt-1 flex items-baseline gap-1">
                <span className="text-2xl font-bold">{tier.price}</span>
              </div>
              <p className="text-muted-foreground mt-2 flex-grow text-sm">
                {tier.description}
              </p>
              <Button
                variant={tier.id === activeTier ? 'default' : 'outline'}
                size="sm"
                className="mt-4"
                asChild
              >
                <a href="#">
                  View Details
                  <ArrowRight className="ml-2 h-4 w-4" />
                </a>
              </Button>
            </div>
          ))}
        </div>

        {/* Mobile comparison for selected tier */}
        <div className="space-y-8 md:hidden">
          {featureCategories.map((category) => (
            <div key={category.name} className="space-y-4">
              <h3 className="border-b pb-2 text-lg font-semibold">
                {category.name}
              </h3>
              <div className="space-y-4">
                {category.features.map((feature) => (
                  <div key={feature.name} className="grid grid-cols-2 gap-4">
                    <div className="flex items-center gap-2">
                      <span>{feature.name}</span>
                      <TooltipProvider>
                        <Tooltip>
                          <TooltipTrigger>
                            <HelpCircle className="text-muted-foreground h-4 w-4" />
                          </TooltipTrigger>
                          <TooltipContent>
                            <p className="max-w-xs text-sm">
                              {feature.tooltip}
                            </p>
                          </TooltipContent>
                        </Tooltip>
                      </TooltipProvider>
                    </div>
                    <div>
                      {typeof feature.values[
                        activeTier as keyof typeof feature.values
                      ] === 'boolean' ? (
                        feature.values[
                          activeTier as keyof typeof feature.values
                        ] ? (
                          <Check className="text-primary h-5 w-5" />
                        ) : (
                          <X className="text-muted-foreground h-5 w-5" />
                        )
                      ) : (
                        <span>
                          {
                            feature.values[
                              activeTier as keyof typeof feature.values
                            ]
                          }
                        </span>
                      )}
                    </div>
                  </div>
                ))}
              </div>
            </div>
          ))}
        </div>

        {/* Desktop comparison table */}
        <div className="hidden overflow-x-auto md:block">
          <TooltipProvider>
            <Table className="border-collapse">
              <TableHeader>
                <TableRow className="bg-muted/50">
                  <TableHead className="w-1/4">Feature Categories</TableHead>
                  {tiers.map((tier) => (
                    <TableHead
                      key={tier.id}
                      className={`text-center ${
                        tier.id === activeTier ? 'bg-primary/5' : ''
                      }`}
                    >
                      {tier.name}
                    </TableHead>
                  ))}
                </TableRow>
              </TableHeader>
              <TableBody>
                {featureCategories.map((category, categoryIndex) => (
                  <>
                    <TableRow className="bg-muted/20">
                      <TableCell colSpan={4} className="text-base font-medium">
                        {category.name}
                      </TableCell>
                    </TableRow>
                    {category.features.map((feature, featureIndex) => (
                      <TableRow
                        key={`${categoryIndex}-${featureIndex}`}
                        className={`${
                          featureIndex === category.features.length - 1 &&
                          categoryIndex !== featureCategories.length - 1
                            ? 'border-border border-b-2'
                            : ''
                        }`}
                      >
                        <TableCell className="flex items-center gap-2 font-medium">
                          {feature.name}
                          <Tooltip>
                            <TooltipTrigger>
                              <Info className="text-muted-foreground h-4 w-4" />
                            </TooltipTrigger>
                            <TooltipContent>
                              <p className="max-w-xs text-sm">
                                {feature.tooltip}
                              </p>
                            </TooltipContent>
                          </Tooltip>
                        </TableCell>
                        {tiers.map((tier) => (
                          <TableCell
                            key={`${feature.name}-${tier.id}`}
                            className={`text-center ${
                              tier.id === activeTier ? 'bg-primary/5' : ''
                            }`}
                          >
                            {typeof feature.values[
                              tier.id as keyof typeof feature.values
                            ] === 'boolean' ? (
                              feature.values[
                                tier.id as keyof typeof feature.values
                              ] ? (
                                <div className="flex justify-center">
                                  <Check className="text-primary h-5 w-5" />
                                </div>
                              ) : (
                                <div className="flex justify-center">
                                  <X className="text-muted-foreground h-5 w-5" />
                                </div>
                              )
                            ) : (
                              <span>
                                {
                                  feature.values[
                                    tier.id as keyof typeof feature.values
                                  ]
                                }
                              </span>
                            )}
                          </TableCell>
                        ))}
                      </TableRow>
                    ))}
                  </>
                ))}
              </TableBody>
            </Table>
          </TooltipProvider>
        </div>

        {/* CTA section */}
        <div className="mt-16 flex flex-col items-center justify-center space-y-4 text-center">
          <h3 className="text-xl font-semibold">Ready to hit the trails?</h3>
          <p className="text-muted-foreground max-w-[600px]">
            All bikes come with free shipping, professional assembly, and a
            30-day test ride guarantee.
          </p>
          <div className="mt-6 flex flex-col gap-4 sm:flex-row">
            <Button size="lg" asChild>
              <a href="#">
                Shop {tiers.find((t) => t.id === activeTier)?.name || 'Premium'}{' '}
                Model
                <ArrowRight className="ml-2 h-4 w-4" />
              </a>
            </Button>
            <Button size="lg" variant="outline" asChild>
              <a href="#">Schedule Test Ride</a>
            </Button>
          </div>
        </div>
      </div>
    </section>
  );
}
```

### `components/ui/badge.tsx`

```tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"

import { cn } from "@/lib/utils"

const badgeVariants = cva(
  "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
        secondary:
          "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
        destructive:
          "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
        outline:
          "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
        ghost:
          "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
        link: "text-primary underline-offset-4 hover:underline",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

function Badge({
  className,
  variant = "default",
  asChild = false,
  ...props
}: React.ComponentProps<"span"> &
  VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
  const Comp = asChild ? Slot.Root : "span"

  return (
    <Comp
      data-slot="badge"
      data-variant={variant}
      className={cn(badgeVariants({ variant }), className)}
      {...props}
    />
  )
}

export { Badge, badgeVariants }
```

### `components/ui/button.tsx`

```tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"

import { cn } from "@/lib/utils"

const buttonVariants = cva(
  "group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/80",
        outline:
          "border-border bg-background shadow-xs hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
        ghost:
          "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
        destructive:
          "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default:
          "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
        xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
        sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5",
        lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
        icon: "size-9",
        "icon-xs":
          "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
        "icon-sm":
          "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md",
        "icon-lg": "size-10",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

function Button({
  className,
  variant = "default",
  size = "default",
  asChild = false,
  ...props
}: React.ComponentProps<"button"> &
  VariantProps<typeof buttonVariants> & {
    asChild?: boolean
  }) {
  const Comp = asChild ? Slot.Root : "button"

  return (
    <Comp
      data-slot="button"
      data-variant={variant}
      data-size={size}
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    />
  )
}

export { Button, buttonVariants }
```

### `components/ui/table.tsx`

```tsx
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"

function Table({ className, ...props }: React.ComponentProps<"table">) {
  return (
    <div
      data-slot="table-container"
      className="relative w-full overflow-x-auto"
    >
      <table
        data-slot="table"
        className={cn("w-full caption-bottom text-sm", className)}
        {...props}
      />
    </div>
  )
}

function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
  return (
    <thead
      data-slot="table-header"
      className={cn("[&_tr]:border-b", className)}
      {...props}
    />
  )
}

function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
  return (
    <tbody
      data-slot="table-body"
      className={cn("[&_tr:last-child]:border-0", className)}
      {...props}
    />
  )
}

function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
  return (
    <tfoot
      data-slot="table-footer"
      className={cn(
        "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
        className
      )}
      {...props}
    />
  )
}

function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
  return (
    <tr
      data-slot="table-row"
      className={cn(
        "border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
        className
      )}
      {...props}
    />
  )
}

function TableHead({ className, ...props }: React.ComponentProps<"th">) {
  return (
    <th
      data-slot="table-head"
      className={cn(
        "h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
        className
      )}
      {...props}
    />
  )
}

function TableCell({ className, ...props }: React.ComponentProps<"td">) {
  return (
    <td
      data-slot="table-cell"
      className={cn(
        "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
        className
      )}
      {...props}
    />
  )
}

function TableCaption({
  className,
  ...props
}: React.ComponentProps<"caption">) {
  return (
    <caption
      data-slot="table-caption"
      className={cn("mt-4 text-sm text-muted-foreground", className)}
      {...props}
    />
  )
}

export {
  Table,
  TableHeader,
  TableBody,
  TableFooter,
  TableHead,
  TableRow,
  TableCell,
  TableCaption,
}
```

### `components/ui/tooltip.tsx`

```tsx
"use client"

import * as React from "react"
import { Tooltip as TooltipPrimitive } from "radix-ui"

import { cn } from "@/lib/utils"

function TooltipProvider({
  delayDuration = 0,
  ...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
  return (
    <TooltipPrimitive.Provider
      data-slot="tooltip-provider"
      delayDuration={delayDuration}
      {...props}
    />
  )
}

function Tooltip({
  ...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
  return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
}

function TooltipTrigger({
  ...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
  return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}

function TooltipContent({
  className,
  sideOffset = 0,
  arrow = true,
  children,
  ...props
}: React.ComponentProps<typeof TooltipPrimitive.Content> & {
  /** Hide the pointer arrow (custom-surfaced tooltips, e.g. the app's dark
   *  menu-styled ones, where the bg-foreground diamond clashes). */
  arrow?: boolean
}) {
  return (
    <TooltipPrimitive.Portal>
      <TooltipPrimitive.Content
        data-slot="tooltip-content"
        sideOffset={sideOffset}
        className={cn(
          "z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
          className
        )}
        {...props}
      >
        {children}
        {arrow && (
          <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
        )}
      </TooltipPrimitive.Content>
    </TooltipPrimitive.Portal>
  )
}

export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
```
