With the hardest parts of the storefront built in, ready to use, or easily integrated with Shopify, and significant performance advancements unlocked by Remix, the Hydrogen stack makes building storefronts fun.

Read documentation

Built for commerce

Starter templates

Two ways to get started: Fully built-out Demo Store template includes purchase journey and Hello World template offers minimal opinions with optional TypeScript support

/app/routes/($lang)/cart.jsx
import { CartLoading, Cart } from "~/components";
import { Await, useMatches } from "@remix-run/react";
import { Suspense } from "react";
import invariant from "tiny-invariant";
import { json } from "@shopify/remix-oxygen";
import { isLocalPath } from "~/lib/utils";
import { CartAction } from "~/lib/type";
export async function action({ request, context }) {
const { session, storefront } = context;
const headers = new Headers();
const [formData, storedCartId, customerAccessToken] = await Promise.all([
request.formData(),
session.get("cartId"),
session.get("customerAccessToken"),
]);
let cartId = storedCartId;
const cartAction = formData.get("cartAction");
invariant(cartAction, "No cartAction defined");
const countryCode = formData.get("countryCode")
? formData.get("countryCode")
: null;
let status = 200;
let result;
switch (cartAction) {
case CartAction.ADD_TO_CART:
const lines = formData.get("lines")
? JSON.parse(String(formData.get("lines")))
: [];
invariant(lines.length, "No lines to add");
/**
* If no previous cart exists, create one with the lines.
*/
if (!cartId) {
result = await cartCreate({
input: countryCode
? { lines, buyerIdentity: { countryCode } }
: { lines },
storefront,
});
} else {
result = await cartAdd({
cartId,
lines,
storefront,
});
}
cartId = result.cart.id;
break;
case CartAction.REMOVE_FROM_CART:
const lineIds = formData.get("linesIds")
? JSON.parse(String(formData.get("linesIds")))
: [];
invariant(lineIds.length, "No lines to remove");
result = await cartRemove({
cartId,
lineIds,
storefront,
});
cartId = result.cart.id;
break;
case CartAction.UPDATE_CART:
const updateLines = formData.get("lines")
? JSON.parse(String(formData.get("lines")))
: [];
invariant(updateLines.length, "No lines to update");
result = await cartUpdate({
cartId,
lines: updateLines,
storefront,
});
cartId = result.cart.id;
break;
case CartAction.UPDATE_DISCOUNT:
invariant(cartId, "Missing cartId");
const formDiscountCode = formData.get("discountCode");
const discountCodes = [formDiscountCode] || [""];
result = await cartDiscountCodesUpdate({
cartId,
discountCodes,
storefront,
});
cartId = result.cart.id;
break;
case CartAction.UPDATE_BUYER_IDENTITY:
const buyerIdentity = formData.get("buyerIdentity")
? JSON.parse(String(formData.get("buyerIdentity")))
: {};
result = cartId
? await cartUpdateBuyerIdentity({
cartId,
buyerIdentity: {
...buyerIdentity,
customerAccessToken,
},
storefront,
})
: await cartCreate({
input: {
buyerIdentity: {
...buyerIdentity,
customerAccessToken,
},
},
storefront,
});
cartId = result.cart.id;
break;
default:
invariant(false, `${cartAction} cart action is not defined`);
}
/**
* The Cart ID may change after each mutation. We need to update it each time in the session.
*/
session.set("cartId", cartId);
headers.set("Set-Cookie", await session.commit());
const redirectTo = formData.get("redirectTo") ?? null;
if (typeof redirectTo === "string" && isLocalPath(redirectTo)) {
status = 303;
headers.set("Location", redirectTo);
}
const { cart, errors } = result;
return json({ cart, errors }, { status, headers });
}
export default function CartRoute() {
const [root] = useMatches();
// @todo: finish on a separate PR
return (
<div className="grid w-full gap-8 p-6 py-8 md:p-8 lg:p-12 justify-items-start">
<Suspense fallback={<CartLoading />}>
<Await resolve={root.data?.cart}>
{(cart) => <Cart layout="page" cart={cart} />}
</Await>
</Suspense>
</div>
);
}
/*
Cart Queries
*/
const USER_ERROR_FRAGMENT = `#graphql
fragment ErrorFragment on CartUserError {
message
field
code
}
`;
const LINES_CART_FRAGMENT = `#graphql
fragment CartLinesFragment on Cart {
id
totalQuantity
}
`;
//! @see: https://shopify.dev/api/storefront/2022-01/mutations/cartcreate
const CREATE_CART_MUTATION = `#graphql
mutation ($input: CartInput!, $country: CountryCode = ZZ, $language: LanguageCode)
@inContext(country: $country, language: $language) {
cartCreate(input: $input) {
cart {
...CartLinesFragment
}
errors: userErrors {
...ErrorFragment
}
}
}
${LINES_CART_FRAGMENT}
${USER_ERROR_FRAGMENT}
`;
/**
* Create a cart with line(s) mutation
* @param input CartInput https://shopify.dev/api/storefront/2022-01/input-objects/CartInput
* @see https://shopify.dev/api/storefront/2022-01/mutations/cartcreate
* @returns result {cart, errors}
* @preserve
*/
export async function cartCreate({ input, storefront }) {
const { cartCreate } = await storefront.mutate(CREATE_CART_MUTATION, {
variables: { input },
});
invariant(cartCreate, "No data returned from cartCreate mutation");
return cartCreate;
}
const ADD_LINES_MUTATION = `#graphql
mutation ($cartId: ID!, $lines: [CartLineInput!]!, $country: CountryCode = ZZ, $language: LanguageCode)
@inContext(country: $country, language: $language) {
cartLinesAdd(cartId: $cartId, lines: $lines) {
cart {
...CartLinesFragment
}
errors: userErrors {
...ErrorFragment
}
}
}
${LINES_CART_FRAGMENT}
${USER_ERROR_FRAGMENT}
`;
/**
* Storefront API cartLinesAdd mutation
* @param cartId
* @param lines [CartLineInput!]! https://shopify.dev/api/storefront/2022-01/input-objects/CartLineInput
* @see https://shopify.dev/api/storefront/2022-01/mutations/cartLinesAdd
* @returns result {cart, errors}
* @preserve
*/
export async function cartAdd({ cartId, lines, storefront }) {
const { cartLinesAdd } = await storefront.mutate(ADD_LINES_MUTATION, {
variables: { cartId, lines },
});
invariant(cartLinesAdd, "No data returned from cartLinesAdd mutation");
return cartLinesAdd;
}
const REMOVE_LINE_ITEMS_MUTATION = `#graphql
mutation ($cartId: ID!, $lineIds: [ID!]!, $language: LanguageCode, $country: CountryCode)
@inContext(country: $country, language: $language) {
cartLinesRemove(cartId: $cartId, lineIds: $lineIds) {
cart {
id
totalQuantity
lines(first: 100) {
edges {
node {
id
quantity
merchandise {
...on ProductVariant {
id
}
}
}
}
}
}
errors: userErrors {
message
field
code
}
}
}
`;
/**
* Create a cart with line(s) mutation
* @param cartId the current cart id
* @param lineIds [ID!]! an array of cart line ids to remove
* @see https://shopify.dev/api/storefront/2022-07/mutations/cartlinesremove
* @returns mutated cart
* @preserve
*/
export async function cartRemove({ cartId, lineIds, storefront }) {
const { cartLinesRemove } = await storefront.mutate(
REMOVE_LINE_ITEMS_MUTATION,
{
variables: {
cartId,
lineIds,
},
}
);
invariant(cartLinesRemove, "No data returned from remove lines mutation");
return cartLinesRemove;
}
const LINES_UPDATE_MUTATION = `#graphql
${LINES_CART_FRAGMENT}
${USER_ERROR_FRAGMENT}
mutation ($cartId: ID!, $lines: [CartLineUpdateInput!]!, $language: LanguageCode, $country: CountryCode)
@inContext(country: $country, language: $language) {
cartLinesUpdate(cartId: $cartId, lines: $lines) {
cart {
...CartLinesFragment
}
errors: userErrors {
...ErrorFragment
}
}
}
`;
/**
* Update cart line(s) mutation
* @param cartId the current cart id
* @param lineIds [ID!]! an array of cart line ids to remove
* @see https://shopify.dev/api/storefront/2022-07/mutations/cartlinesremove
* @returns mutated cart
* @preserve
*/
export async function cartUpdate({ cartId, lines, storefront }) {
const { cartLinesUpdate } = await storefront.mutate(LINES_UPDATE_MUTATION, {
variables: { cartId, lines },
});
invariant(
cartLinesUpdate,
"No data returned from update lines items mutation"
);
return cartLinesUpdate;
}
/**
* @see https://shopify.dev/api/storefront/2022-10/mutations/cartBuyerIdentityUpdate
* @preserve
*/
const UPDATE_CART_BUYER_COUNTRY = `#graphql
mutation(
$cartId: ID!
$buyerIdentity: CartBuyerIdentityInput!
$country: CountryCode = ZZ
$language: LanguageCode
) @inContext(country: $country, language: $language) {
cartBuyerIdentityUpdate(cartId: $cartId, buyerIdentity: $buyerIdentity) {
cart {
id
buyerIdentity {
email
phone
countryCode
}
}
errors: userErrors {
message
field
code
}
}
}
`;
/**
* Mutation to update a cart buyerIdentity
* @param cartId Cart['id']
* @param buyerIdentity CartBuyerIdentityInput
* @returns {cart: Cart; errors: UserError[]}
* @see API https://shopify.dev/api/storefront/2022-10/mutations/cartBuyerIdentityUpdate
* @preserve
*/
export async function cartUpdateBuyerIdentity({
cartId,
buyerIdentity,
storefront,
}) {
const { cartBuyerIdentityUpdate } = await storefront.mutate(
UPDATE_CART_BUYER_COUNTRY,
{
variables: {
cartId,
buyerIdentity,
},
}
);
invariant(
cartBuyerIdentityUpdate,
"No data returned from cart buyer identity update mutation"
);
return cartBuyerIdentityUpdate;
}
const DISCOUNT_CODES_UPDATE = `#graphql
mutation cartDiscountCodesUpdate($cartId: ID!, $discountCodes: [String!], $country: CountryCode = ZZ)
@inContext(country: $country) {
cartDiscountCodesUpdate(cartId: $cartId, discountCodes: $discountCodes) {
cart {
id
discountCodes {
code
}
}
errors: userErrors {
field
message
}
}
}
`;
/**
* Mutation that updates the cart discounts
* @param discountCodes Array of discount codes
* @returns mutated cart
* @preserve
*/
export async function cartDiscountCodesUpdate({
cartId,
discountCodes,
storefront,
}) {
const { cartDiscountCodesUpdate } = await storefront.mutate(
DISCOUNT_CODES_UPDATE,
{
variables: {
cartId,
discountCodes,
},
}
);
invariant(
cartDiscountCodesUpdate,
"No data returned from the cartDiscountCodesUpdate mutation"
);
return cartDiscountCodesUpdate;
}

Bag (2 items)

Sweater

Sweater

1

$65

Hydrogen Frame

Hydrogen Frame

1

$15

Hydrogen components and hooks
AddToCartButton
BuyNowButton
CartCheckoutButton
CartCost
CartLinePrice
CartLineProvider
CartProvider
ExternalVideo
Image
MediaFile
ModelViewer
Money
ProductPrice
ProductProvider
ShopPayButton
ShopifyProvider
Video
useCart
useCartLine
useMoney
Analytics Constants
sendShopifyAnalytics
storefrontApiCustomScalars
flattenConnection
getClientBrowserParameters
getShopifyCookies
parseMetafield
Storefront API Types
createStorefrontClient
Storefront Schema
useShopifyCookies

Components and hooks

Mapped directly to the Storefront API, Hydrogen gives you components and hooks like customer accounts, product forms, search, pagination, and i18n—all pre-built to accelerate development

Compatible with any React framework

Bring the best parts of Hydrogen to more React frameworks, like Next.js and Gatsby, and accelerate headless development using Shopify’s pre-built React components including Cart, Shop Pay, and Shopify Analytics

A code editor and two browser windows showing an analytics page and a cart page are connected by dotted lines to represent Hydrogen's compatibility with any React framework.
Four modules are connected with dotted lines to represent the out-of-the-box features offered with the SEO component.

SEO ready

Includes pre-built SEO optimizations like an auto-generated sitemap, metadata values for every page, robust product feed support, as well as improved crawlability for all engines

Two browser windows displaying product detail pages are connected with dotted lines to represent the connection between Shopify&apos;s APIs and Hydrogen storefronts.

Storefront API

Built on top of a set of edge-rendered commerce APIs, delivering the best possible performance to your customers, no matter where they’re visiting from.

Performance

1/3

Streaming server-side rendering with Suspense

Streaming server-side rendering with Suspense allows page components to progressively load based on priority without sacrificing page interactivity

Oxygen deployment

Deployed at the edge on Oxygen, Hydrogen storefronts render static and dynamic content faster for performant storefronts worldwide

Caching strategies

Coming soon: Built-in stale-while-revalidate defaults and full-page and sub-request caching APIs optimize the initial load and render components fast

Developer experience

Hydrogen CLI

The Hydrogen CLI helps scaffold new projects, runs a local instance of Oxygen, and helps scaffold boilerplate like routes and components.

...
/app/routes/($lang)/products/$handle.tsx
export async function loader({params, context}: LoaderArgs) {
const {productHandle} = params;
const {product} =
await context.storefront.query<{
product: ProductType
}>(PRODUCT_QUERY, {
variables: {
handle: params.productHandle,
},
});
return json({
product,
});
}

TypeScript

Robust type support for the Storefront API and all Hydrogen components, with IntelliSense editing support and VS Code tooling, so writing code is more intuitive with fewer errors

A module with the Remix logo within it represents the Remix framework.

Powered by Remix

Unlock advancements in developer experience and performance, like optimistic UI, nested routes, progressive enhancement, and more

Several stacked modules, including one with the Tailwind CSS logo within it, represent the many CSS libraries that the framework supports.

Built-in CSS Support

Built-in support for CSS strategies like Tailwind, CSS Modules, and Vanilla Extract; a native feature with Remix.

Built-in hosting at the edge

Install the Hydrogen channel and get access to Oxygen deployments

Read Oxygen documentation

Analytics

Performance insights, analytics, and logs for every deployment

A browser window shows how the Hydrogen storefront is included in the Shopfy Admin, including graphs and analytics on the storefront's performance.
Hydrogen logo
Two browser windows, one with the GitHub logo within it, represent how Hydrogen storefronts and GitHub are connected.

GitHub connectivity

Oxygen connects directly to GitHub and uses GitHub Actions to automatically deploy commits, so deployment is as simple as a git push

A module with a graphic that represents a branch and a checkmark demonstrate how deployments can be previewed before they&apos;re made live.

Preview deployments

Every commit gets its own preview deployment, defaulting to private but easily made public

A browser window is surrounded with modules outlined with dotted lines to represent many custom environments.

Custom environments

Tied directly to a branch, custom environments get their own environment variables and a static URL

Floating Hydrogen logo symbols.
Chat icon

Get involved

Hydrogen GitHub

Conversation on the Hydrogen discussion forum

Hydrogen YouTube playlist

Hydrogen and Oxygen development tips and insights

ShopifyDevs Discord

Events and conversation

Shopify Partners blog

Resources about building with Shopify