How to Set Up Color Swatches in Your Shopify Hydrogen Store
If you're building a Hydrogen storefront with variant-heavy catalogs, swatches are one of the highest-impact UX improvements you can ship quickly. This guide walks through the full implementation path from Shopify Admin setup to production-ready React components.
Color swatches transform the shopping experience by allowing customers to visualize product variants at a glance. Instead of reading through dropdown menus, customers see the actual colors—making purchase decisions faster and more intuitive.
In this comprehensive tutorial, we'll walk through implementing color swatches in a Shopify Hydrogen store, from configuring the data in Shopify Admin to rendering the swatches in your React components using the modern optionValues API (released in 2024-07).
Part 1: Setting Up Swatches in Shopify Admin
Step 1: Access Product Variant Options
-
Log in to your Shopify Admin
-
Navigate to Products → Select a product with color variants
-
Scroll to the Variants section
-
Click Edit Options next to the "Color" option
Step 2: Configure Swatch Data
For each color value, you can set:
| Swatch Type | When to Use | Example |
|---|---|---|
| Solid Color (Hex) | Single, uniform colors | #FF0000 for Red |
| Image Upload | Patterns, textures, gradients | Plaid, Leopard print, Tie-dye |
| Named Color | Standard CSS colors | "Navy", "Tomato", "Teal" |
Best Practices for Swatch Configuration
✅ Use Hex Codes for Accuracy: #1E3A8A is more reliable than color names
✅ Optimize Swatch Images: Keep them under 200x200px
✅ Consistent Naming: Use "Navy Blue" across all products, not sometimes "Navy"
✅ Fallback Ready: If no hex is set, the code will attempt to parse the option name as a color
Part 2: Querying Swatch Data with GraphQL
The optionValues API
Shopify's Storefront API provides the optionValues field—a dedicated, efficient way to fetch product option data including swatches.
GraphQL Fragment
Add this fragment to your app/graphql/fragments.ts:
fragment ProductOption on ProductOption {nameoptionValues {namefirstSelectableVariant {idavailableForSaleprice {amountcurrencyCode}image {urlaltText}}swatch {colorimage {previewImage {# Optimize: Resize swatch images server-sideurl(transform: { width: 100, height: 100, crop: CENTER })altText}}}}}
Key fields:
-
swatch.color: Hex code (e.g.,#FF5733) -
swatch.image.previewImage: Optimized image for patterns/textures -
firstSelectableVariant: First available variant with this option—critical for navigation
Using the Fragment in Product Queries
query Product($handle: String!) {product(handle: $handle) {idtitleoptions {...ProductOption}variants(first: 100) {nodes {idavailableForSaleselectedOptions {namevalue}}}}}
Part 3: Building the React Components
1. Color Utility Functions
Create helpers to handle edge cases like light colors on white backgrounds.
// app/utils/colors.tsimport { colord } from "colord";/*** Validates if a string is a parseable color* Supports hex, RGB, HSL, and named colors*/export function isValidColor(color: string): boolean {return colord(color).isValid();}/*** Detects if a color is "light" (brightness > threshold)* Used to add borders to white/cream/yellow swatches*/export function isLightColor(color: string, threshold = 0.8): boolean {const c = colord(color);return c.isValid() && c.brightness() > threshold;}
Install the dependency:
npm install colord
2. Swatch Component
Create a reusable swatch component with local type definitions:
// app/components/product/ProductOptionSwatches.tsximport { Image } from "@shopify/hydrogen";import { cn } from "~/utils/cn";import { isLightColor, isValidColor } from "~/utils/colors";// Define types locally for better portabilityexport type SwatchOptionValue = {name: string;selected: boolean; // Computed propavailable: boolean; // Computed propswatch?: {color?: string | null;image?: {previewImage?: {url: string;altText?: string | null;} | null;} | null;} | null;};type SwatchProps = {optionValues: SwatchOptionValue[];onSelect: (value: SwatchOptionValue) => void;};export function ProductOptionSwatches({ optionValues, onSelect }: SwatchProps) {return (<div className="flex flex-wrap gap-3">{optionValues.map((value) => (<SwatchButtonkey={value.name}value={value}onClick={() => onSelect(value)}/>))}</div>);}function SwatchButton({value,onClick}: {value: SwatchOptionValue;onClick: () => void;}) {const { name, selected, available, swatch } = value;const hexColor = swatch?.color || name; // Fallback to nameconst image = swatch?.image?.previewImage;return (<buttontype="button"disabled={!available}onClick={onClick}title={name}className={cn("size-8 overflow-hidden rounded-full transition-all","outline-1 outline-offset-2",selected? "outline outline-gray-900": "outline-transparent hover:outline hover:outline-gray-400",!available && "opacity-50 cursor-not-allowed diagonal-strike")}>{image ? (<Imagedata={image}className="h-full w-full object-cover"width={32}height={32}sizes="32px"/>) : (<spanclassName={cn("block h-full w-full",(!isValidColor(hexColor) || isLightColor(hexColor)) &&"border border-gray-200")}style={{ backgroundColor: hexColor }}><span className="sr-only">{name}</span></span>)}</button>);}
3. Diagonal Strike-Through for Unavailable Variants
Add this CSS utility to your app/styles/app.css:
.diagonal-strike {position: relative;}.diagonal-strike::after {content: "";position: absolute;inset: 0;background: linear-gradient(to bottom right,transparent calc(50% - 1px),#999 calc(50% - 1px),#999 calc(50% + 1px),transparent calc(50% + 1px));pointer-events: none;}
4. Using the Component
In your route file, map the raw GraphQL data to the SwatchOptionValue type by calculating selected and available status.
// app/routes/products.$handle.tsximport { useNavigate, useLoaderData } from "@remix-run/react";import { ProductOptionSwatches, type SwatchOptionValue } from "~/components/product/ProductOptionSwatches";export default function ProductPage() {const { product, selectedVariant } = useLoaderData<typeof loader>();const navigate = useNavigate();// 1. Find the color optionconst colorOption = product.options.find((opt) => ["Color", "Colors", "Colour", "Colours"].includes(opt.name));// 2. Map raw data to component props// We need to calculate 'selected' and 'available' based on current contextconst swatches: SwatchOptionValue[] | undefined = colorOption?.optionValues.map((value) => {// Check if this is the currently selected valueconst isSelected = selectedVariant?.selectedOptions.some((opt) => opt.name === colorOption.name && opt.value === value.name);return {...value,selected: !!isSelected,available: !!value.firstSelectableVariant?.availableForSale,};});const handleSwatchSelect = (value: SwatchOptionValue) => {// Navigate to the corresponding variant URLif (value.firstSelectableVariant) {navigate(`?variant=${value.firstSelectableVariant.id.split('/').pop()}`, {preventScrollReset: true,replace: true});}};return (<div>{colorOption && swatches && (<div className="space-y-2"><h3 className="font-medium">Color: {selectedVariant?.selectedOptions.find(o => o.name === colorOption.name)?.value}</h3><ProductOptionSwatchesoptionValues={swatches}onSelect={handleSwatchSelect}/></div>)}</div>);}
Final checklist
Before shipping, verify:
- Swatch data is configured for every color option in Shopify Admin
- Variant availability is reflected visually (disabled + strike-through)
- Light colors have visible borders
- Swatch images are optimized and transformed server-side
- URL/state updates correctly when users change swatches
With this setup, shoppers can scan options faster, reduce misclicks, and reach purchase decisions with less friction.



