Container Settings in Pack: Implementation and Usage
This guide explains how to implement and use container settings in your Pack-powered Hydrogen storefront. Container settings provide consistent spacing, width, and alignment controls that can be applied to any section in your storefront.
What Are Container Settings?
Container settings are a reusable group of fields that control the layout container for sections throughout your storefront. They typically include:
- Section padding (top, bottom)
- Container width
- Container alignment
- Background color or image
- Content width constraints
By implementing container settings as a reusable function, you ensure consistent layout options across all your sections.
Implementing Container Settings
Here's how to implement container settings as a reusable module:
// app/settings/container.ts
import { COLOR_PICKER_DEFAULTS } from '~/settings/common';
export function containerSettings() {
return {
label: 'Container Settings',
name: 'container',
component: 'group',
description: 'Section padding, width, background',
fields: [
// Padding settings
{
label: 'Section Padding',
name: 'padding',
component: 'group',
fields: [
{
label: 'Top Padding (desktop)',
name: 'topDesktop',
component: 'select',
options: [
{ label: 'None', value: 'md:pt-0' },
{ label: 'Small', value: 'md:pt-4' },
{ label: 'Medium', value: 'md:pt-8' },
{ label: 'Large', value: 'md:pt-16' },
{ label: 'Extra Large', value: 'md:pt-24' },
],
defaultValue: 'md:pt-16',
},
{
label: 'Bottom Padding (desktop)',
name: 'bottomDesktop',
component: 'select',
options: [
{ label: 'None', value: 'md:pb-0' },
{ label: 'Small', value: 'md:pb-4' },
{ label: 'Medium', value: 'md:pb-8' },
{ label: 'Large', value: 'md:pb-16' },
{ label: 'Extra Large', value: 'md:pb-24' },
],
defaultValue: 'md:pb-16',
},
{
label: 'Top Padding (mobile)',
name: 'topMobile',
component: 'select',
options: [
{ label: 'None', value: 'pt-0' },
{ label: 'Small', value: 'pt-4' },
{ label: 'Medium', value: 'pt-6' },
{ label: 'Large', value: 'pt-10' },
{ label: 'Extra Large', value: 'pt-16' },
],
defaultValue: 'pt-10',
},
{
label: 'Bottom Padding (mobile)',
name: 'bottomMobile',
component: 'select',
options: [
{ label: 'None', value: 'pb-0' },
{ label: 'Small', value: 'pb-4' },
{ label: 'Medium', value: 'pb-6' },
{ label: 'Large', value: 'pb-10' },
{ label: 'Extra Large', value: 'pb-16' },
],
defaultValue: 'pb-10',
},
],
},
// Background settings
{
label: 'Background Settings',
name: 'background',
component: 'group',
fields: [
{
label: 'Background Type',
name: 'type',
component: 'radio-group',
direction: 'horizontal',
variant: 'radio',
options: [
{ label: 'None', value: 'none' },
{ label: 'Color', value: 'color' },
{ label: 'Image', value: 'image' },
],
defaultValue: 'none',
},
{
label: 'Background Color',
name: 'color',
component: 'color',
colors: COLOR_PICKER_DEFAULTS,
condition: { field: 'type', value: 'color' },
},
{
label: 'Background Image',
name: 'image',
component: 'image',
condition: { field: 'type', value: 'image' },
},
{
label: 'Background Image Position',
name: 'position',
component: 'select',
options: [
{ label: 'Top', value: 'bg-top' },
{ label: 'Center', value: 'bg-center' },
{ label: 'Bottom', value: 'bg-bottom' },
],
defaultValue: 'bg-center',
condition: { field: 'type', value: 'image' },
},
{
label: 'Background Image Size',
name: 'size',
component: 'select',
options: [
{ label: 'Cover', value: 'bg-cover' },
{ label: 'Contain', value: 'bg-contain' },
{ label: 'Auto', value: 'bg-auto' },
],
defaultValue: 'bg-cover',
condition: { field: 'type', value: 'image' },
},
],
},
// Container width settings
{
label: 'Container Width',
name: 'width',
component: 'select',
options: [
{ label: 'Full Width', value: 'w-full' },
{ label: 'Extra Large (1280px)', value: 'max-w-screen-xl mx-auto' },
{ label: 'Large (1024px)', value: 'max-w-screen-lg mx-auto' },
{ label: 'Medium (768px)', value: 'max-w-screen-md mx-auto' },
{ label: 'Small (640px)', value: 'max-w-screen-sm mx-auto' },
],
defaultValue: 'max-w-screen-xl mx-auto',
},
// Content alignment
{
label: 'Content Alignment',
name: 'alignment',
component: 'select',
options: [
{ label: 'Left', value: 'text-left' },
{ label: 'Center', value: 'text-center' },
{ label: 'Right', value: 'text-right' },
],
defaultValue: 'text-left',
},
],
defaultValue: {
padding: {
topDesktop: 'md:pt-16',
bottomDesktop: 'md:pb-16',
topMobile: 'pt-10',
bottomMobile: 'pb-10',
},
background: { type: 'none' },
width: 'max-w-screen-xl mx-auto',
alignment: 'text-left',
},
};
}
Using Container Settings in Section Schemas
// app/sections/HeroSection/HeroSection.schema.ts
import { containerSettings } from '~/settings/container';
export function Schema() {
return {
category: 'Hero',
label: 'Hero Section',
key: 'hero-section',
previewSrc: 'https://example.com/preview.jpg',
fields: [
{
label: 'Heading',
name: 'heading',
component: 'text',
defaultValue: 'Welcome to our store',
},
{
label: 'Subheading',
name: 'subheading',
component: 'text',
defaultValue: 'Discover our amazing products',
},
containerSettings(),
],
};
}
Applying Container Settings in Components
// app/sections/HeroSection/HeroSection.tsx
import React from 'react';
import type { SectionProps } from '~/lib/types';
export function HeroSection({ data }: SectionProps) {
const { heading, subheading, container } = data;
const { padding, background, width, alignment } = container;
const containerClasses = [
padding.topDesktop,
padding.bottomDesktop,
padding.topMobile,
padding.bottomMobile,
width,
alignment,
].join(' ');
let backgroundStyle = {};
if (background.type === 'color') {
backgroundStyle = { backgroundColor: background.color! };
} else if (background.type === 'image' && background.image) {
backgroundStyle = {
backgroundImage: `url(${background.image.url})`,
backgroundPosition: background.position,
backgroundSize: background.size,
};
}
return (
<section className={`hero-section ${containerClasses}`} style={backgroundStyle}>
<div className="hero-content">
<h1>{heading}</h1>
<p>{subheading}</p>
</div>
</section>
);
}
Reusable SectionContainer Component
// app/components/SectionContainer.tsx
import React from 'react';
interface SectionContainerProps {
container: {
padding: {
topDesktop: string;
bottomDesktop: string;
topMobile: string;
bottomMobile: string;
};
background: {
type: 'none' | 'color' | 'image';
color?: string;
image?: { url: string };
position?: string;
size?: string;
};
width: string;
alignment: string;
};
className?: string;
children: React.ReactNode;
}
export function SectionContainer({
container,
className = '',
children,
}: SectionContainerProps) {
const { padding, background, width, alignment } = container;
const containerClasses = [
padding.topDesktop,
padding.bottomDesktop,
padding.topMobile,
padding.bottomMobile,
width,
alignment,
className,
].join(' ');
let backgroundStyle = {};
if (background.type === 'color') {
backgroundStyle = { backgroundColor: background.color! };
} else if (background.type === 'image' && background.image) {
backgroundStyle = {
backgroundImage: `url(${background.image.url})`,
backgroundPosition: background.position,
backgroundSize: background.size,
};
}
return (
<section className={containerClasses} style={backgroundStyle}>
{children}
</section>
);
}
Extending Container Settings
export function extendedContainerSettings(additionalOptions = {}) {
const baseSettings = containerSettings();
const extendedFields = [
...baseSettings.fields!,
{
label: 'Content Width',
name: 'contentWidth',
component: 'select',
options: [
{ label: 'Full Width', value: 'w-full' },
{ label: 'Three-quarters', value: 'w-3/4 mx-auto' },
{ label: 'Two-thirds', value: 'w-2/3 mx-auto' },
{ label: 'Half', value: 'w-1/2 mx-auto' },
],
defaultValue: 'w-full',
},
...(additionalOptions.fields || []),
];
return {
...baseSettings,
fields: extendedFields,
defaultValue: {
...baseSettings.defaultValue!,
contentWidth: 'w-full',
...(additionalOptions.defaultValue || {}),
},
};
}
Theme Presets
// app/settings/theme-presets.ts
export const LIGHT_PRESET = {
background: { type: 'color', color: '#ffffff' },
textColor: '#000000',
accentColor: '#3b82f6',
};
export const DARK_PRESET = {
background: { type: 'color', color: '#111827' },
textColor: '#ffffff',
accentColor: '#60a5fa',
};
export const BRAND_PRESET = {
background: { type: 'color', color: '#f8fafc' },
textColor: '#334155',
accentColor: '#0891b2',
};
export function applyThemePreset(preset) {
return {
label: 'Theme Preset',
name: 'themePreset',
component: 'select',
options: [
{ label: 'Default', value: 'default' },
{ label: 'Light', value: 'light' },
{ label: 'Dark', value: 'dark' },
{ label: 'Brand', value: 'brand' },
],
defaultValue: 'default',
onChange: (field, form) => {
if (field.value === 'light') {
form.mutators.setValues({
container: {
...form.values.container,
background: LIGHT_PRESET.background,
},
textColor: LIGHT_PRESET.textColor,
accentColor: LIGHT_PRESET.accentColor,
});
} else if (field.value === 'dark') {
form.mutators.setValues({
container: {
...form.values.container,
background: DARK_PRESET.background,
},
textColor: DARK_PRESET.textColor,
accentColor: DARK_PRESET.accentColor,
});
} else if (field.value === 'brand') {
form.mutators.setValues({
container: {
...form.values.container,
background: BRAND_PRESET.background,
},
textColor: BRAND_PRESET.textColor,
accentColor: BRAND_PRESET.accentColor,
});
}
},
};
}
Common Settings Module
// app/settings/common.ts
export const COLOR_PICKER_DEFAULTS = [
{ label: 'White', color: '#ffffff' },
{ label: 'Black', color: '#000000' },
{ label: 'Brand Primary', color: '#0891b2' },
{ label: 'Brand Secondary', color: '#60a5fa' },
{ label: 'Gray 100', color: '#f3f4f6' },
{ label: 'Gray 200', color: '#e5e7eb' },
{ label: 'Gray 500', color: '#6b7280' },
{ label: 'Gray 900', color: '#111827' },
{ label: 'Red', color: '#ef4444' },
{ label: 'Yellow', color: '#eab308' },
{ label: 'Green', color: '#22c55e' },
{ label: 'Blue', color: '#3b82f6' },
{ label: 'Purple', color: '#a855f7' },
];
export const COLOR_SCHEMA_DEFAULT_VALUE = {
white: '#ffffff',
black: '#000000',
brandPrimary: '#0891b2',
brandSecondary: '#60a5fa',
};
export const BUTTONS = [
{ label: 'Primary', value: 'btn-primary' },
{ label: 'Secondary', value: 'btn-secondary' },
{ label: 'Tertiary', value: 'btn-tertiary' },
{ label: 'Outline', value: 'btn-outline' },
{ label: 'Accent', value: 'btn-accent' },
{ label: 'Link', value: 'btn-link' },
];
export const FLEX_POSITIONS = {
desktop: [
{ label: 'Top Left', value: 'md:justify-start md:items-start' },
{ label: 'Top Center', value: 'md:justify-center md:items-start' },
{ label: 'Top Right', value: 'md:justify-end md:items-start' },
{ label: 'Middle Left', value: 'md:justify-start md:items-center' },
{ label: 'Middle Center', value: 'md:justify-center md:items-center' },
{ label: 'Middle Right', value: 'md:justify-end md:items-center' },
{ label: 'Bottom Left', value: 'md:justify-start md:items-end' },
{ label: 'Bottom Center', value: 'md:justify-center md:items-end' },
{ label: 'Bottom Right', value: 'md:justify-end md:items-end' },
],
mobile: [
{ label: 'Top Left', value: 'justify-start items-start' },
{ label: 'Top Center', value: 'justify-center items-start' },
{ label: 'Top Right', value: 'justify-end items-start' },
{ label: 'Middle Left', value: 'justify-start items-center' },
{ label: 'Middle Center', value: 'justify-center items-center' },
{ label: 'Middle Right', value: 'justify-end items-center' },
{ label: 'Bottom Left', value: 'justify-start items-end' },
{ label: 'Bottom Center', value: 'justify-center items-end' },
{ label: 'Bottom Right', value: 'justify-end items-end' },
],
};
export const OBJECT_POSITIONS = {
desktop: [
{ label: 'Top', value: 'md:object-top' },
{ label: 'Center', value: 'md:object-center' },
{ label: 'Bottom', value: 'md:object-bottom' },
{ label: 'Left', value: 'md:object-left' },
{ label: 'Right', value: 'md:object-right' },
{ label: 'Top Left', value: 'md:object-left-top' },
{ label: 'Top Right', value: 'md:object-right-top' },
{ label: 'Bottom Left', value: 'md:object-left-bottom' },
{ label: 'Bottom Right', value: 'md:object-right-bottom' },
],
mobile: [
{ label: 'Top', value: 'object-top' },
{ label: 'Center', value: 'object-center' },
{ label: 'Bottom', value: 'object-bottom' },
{ label: 'Left', value: 'object-left' },
{ label: 'Right', value: 'object-right' },
{ label: 'Top Left', value: 'object-left-top' },
{ label: 'Top Right', value: 'object-right-top' },
{ label: 'Bottom Left', value: 'object-left-bottom' },
{ label: 'Bottom Right', value: 'object-right-bottom' },
],
};
Best Practices for Container Settings
- Use the same container settings function in all sections
- Keep class names consistent for spacing, alignment, and other styles
- Define section-specific container extensions in separate functions
Performance Considerations
- Optimize class generation to avoid unnecessary concatenations
- Consider memoizing container classes when they don't change
- Use utility classes rather than inline styles when possible
Accessibility Considerations
- Ensure background and text colors have sufficient contrast
- Include appropriate accessibility attributes in container elements
- Test container layouts with screen readers and keyboard navigation
Responsive Design
- Include separate settings for mobile and desktop layouts
- Use a mobile-first approach with responsive class modifiers
- Test containers at various viewport sizes
Troubleshooting
Settings Not Appearing
- Verify the
containerSettings()
function is included in your fields array - Check that it returns the correct schema structure
- Ensure you’re not exceeding field limits in your schema
Styles Not Applying
- Confirm you’re extracting container values correctly in your component
- Verify class names match your CSS framework (e.g., Tailwind CSS)
- Inspect the HTML to ensure classes are applied
Default Values Not Working
- Ensure
defaultValue
object matches your fields - Check that the component handles missing or undefined values
- Provide fallback values in your component if needed
Conclusion
Container settings provide a powerful way to ensure layout consistency across your Pack-powered Hydrogen storefront. By implementing them as reusable functions, you create a design system that can be easily maintained, extended, and customized by content editors without developer intervention.