Weaverse LogoWeaverse
All Articles
Paul Phan
6 mins read

How to Set Up Color Swatches in Your Shopify Hydrogen Store

Step-by-step guide to implement color swatches in Shopify Hydrogen using optionValues API, GraphQL fragments, and React components with availability states.
How to Set Up Color Swatches in Your Shopify Hydrogen Store
Table of Contents

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

  1. Log in to your Shopify Admin

  2. Navigate to Products → Select a product with color variants

  3. Scroll to the Variants section

  4. Click Edit Options next to the "Color" option

Step 2: Configure Swatch Data

For each color value, you can set:

Swatch TypeWhen to UseExample
Solid Color (Hex)Single, uniform colors#FF0000 for Red
Image UploadPatterns, textures, gradientsPlaid, Leopard print, Tie-dye
Named ColorStandard 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 {
name
optionValues {
name
firstSelectableVariant {
id
availableForSale
price {
amount
currencyCode
}
image {
url
altText
}
}
swatch {
color
image {
previewImage {
# Optimize: Resize swatch images server-side
url(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) {
id
title
options {
...ProductOption
}
variants(first: 100) {
nodes {
id
availableForSale
selectedOptions {
name
value
}
}
}
}
}

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.ts
import { 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.tsx
import { Image } from "@shopify/hydrogen";
import { cn } from "~/utils/cn";
import { isLightColor, isValidColor } from "~/utils/colors";
// Define types locally for better portability
export type SwatchOptionValue = {
name: string;
selected: boolean; // Computed prop
available: boolean; // Computed prop
swatch?: {
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) => (
<SwatchButton
key={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 name
const image = swatch?.image?.previewImage;
return (
<button
type="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 ? (
<Image
data={image}
className="h-full w-full object-cover"
width={32}
height={32}
sizes="32px"
/>
) : (
<span
className={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.tsx
import { 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 option
const 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 context
const swatches: SwatchOptionValue[] | undefined = colorOption?.optionValues.map((value) => {
// Check if this is the currently selected value
const 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 URL
if (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>
<ProductOptionSwatches
optionValues={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.

Reactions

Like
Love
Celebrate
Insightful
Cool!
Thinking

Join the Discussion

Never miss an update

Subscribe to get the latest insights, tutorials, and best practices for building high-performance headless stores delivered to your inbox.

Join the community of developers building with Weaverse.