Skip to main content
Create fully customized themes for your Paste application by extending base themes and overriding design tokens.

Overview

Custom themes let you:
  • Match your brand identity
  • Create product-specific variations
  • Support multiple brands in one application
  • Adjust tokens for accessibility or user preferences

Quick Start

import { CustomizationProvider } from '@twilio-paste/core/customization';

const myTheme = {
  backgroundColors: {
    colorBackgroundPrimary: '#FF6B6B',
  },
  fonts: {
    fontFamilyText: '"Inter", sans-serif',
  },
};

function App() {
  return (
    <CustomizationProvider baseTheme="default" theme={myTheme}>
      <YourApplication />
    </CustomizationProvider>
  );
}

Building a Custom Theme

Step 1: Choose a Base Theme

Start with either default or dark:
import { CustomizationProvider } from '@twilio-paste/core/customization';

<CustomizationProvider baseTheme="default">
  <App />
</CustomizationProvider>

Step 2: Define Your Overrides

Create an object with the tokens you want to customize:
const customTheme = {
  // Typography
  fonts: {
    fontFamilyText: '"Roboto", sans-serif',
  },
  fontSizes: {
    fontSize30: '1.0625rem',
  },
  
  // Colors
  backgroundColors: {
    colorBackgroundPrimary: '#0D9488',
    colorBackgroundPrimaryStrong: '#0F766E',
  },
  textColors: {
    colorTextLink: '#0D9488',
  },
  
  // Spacing & Borders
  radii: {
    borderRadius20: '6px',
    borderRadius30: '10px',
  },
};

Step 3: Apply Your Theme

<CustomizationProvider baseTheme="default" theme={customTheme}>
  <App />
</CustomizationProvider>

Complete Theme Example

Here’s a comprehensive custom theme:
import { CustomizationProvider } from '@twilio-paste/core/customization';

const brandTheme = {
  // Brand Typography
  fonts: {
    fontFamilyText: '"Inter", -apple-system, BlinkMacSystemFont, sans-serif',
    fontFamilyCode: '"Fira Code", "Courier New", monospace',
  },
  fontSizes: {
    fontSize10: '0.6875rem',   // 11px
    fontSize20: '0.8125rem',   // 13px
    fontSize30: '0.9375rem',   // 15px
    fontSize40: '1.0625rem',   // 17px
    fontSize50: '1.1875rem',   // 19px
    fontSize60: '1.375rem',    // 22px
    fontSize70: '1.75rem',     // 28px
    fontSize80: '2.125rem',    // 34px
  },
  fontWeights: {
    fontWeightNormal: '400',
    fontWeightMedium: '500',
    fontWeightSemibold: '600',
    fontWeightBold: '700',
  },
  lineHeights: {
    lineHeight20: '1.125rem',
    lineHeight30: '1.375rem',
    lineHeight40: '1.625rem',
    lineHeight50: '1.875rem',
  },

  // Brand Colors
  backgroundColors: {
    // Primary brand color
    colorBackgroundPrimary: '#0D9488',
    colorBackgroundPrimaryStrong: '#0F766E',
    colorBackgroundPrimaryStronger: '#115E59',
    colorBackgroundPrimaryStrongest: '#134E4A',
    colorBackgroundPrimaryWeakest: '#CCFBF1',
    
    // Body backgrounds
    colorBackgroundBody: '#FFFFFF',
    colorBackgroundBodyInverse: '#1F2937',
    
    // Status colors
    colorBackgroundDestructive: '#DC2626',
    colorBackgroundDestructiveWeakest: '#FEE2E2',
    colorBackgroundSuccess: '#059669',
    colorBackgroundSuccessWeakest: '#D1FAE5',
    colorBackgroundWarning: '#D97706',
    colorBackgroundWarningWeakest: '#FEF3C7',
  },
  
  textColors: {
    colorText: '#111827',
    colorTextWeak: '#4B5563',
    colorTextWeaker: '#6B7280',
    colorTextWeakest: '#9CA3AF',
    colorTextInverse: '#FFFFFF',
    
    colorTextLink: '#0D9488',
    colorTextLinkWeak: '#14B8A6',
    colorTextLinkStrong: '#0F766E',
    
    colorTextError: '#DC2626',
    colorTextSuccess: '#059669',
    colorTextWarning: '#D97706',
  },
  
  borderColors: {
    colorBorder: '#E5E7EB',
    colorBorderWeak: '#F3F4F6',
    colorBorderWeaker: '#F9FAFB',
    colorBorderStrong: '#D1D5DB',
    
    colorBorderPrimary: '#0D9488',
    colorBorderDestructive: '#DC2626',
    colorBorderSuccess: '#059669',
    colorBorderWarning: '#D97706',
  },

  // Spacing (optional - usually keep default)
  space: {
    space30: '0.5rem',
    space40: '0.75rem',
    space50: '1rem',
    space60: '1.5rem',
    space70: '2rem',
  },

  // Borders & Shadows
  radii: {
    borderRadius10: '3px',
    borderRadius20: '6px',
    borderRadius30: '10px',
    borderRadius40: '14px',
    borderRadius50: '18px',
  },
  borderWidths: {
    borderWidth10: '1px',
    borderWidth20: '2px',
    borderWidth30: '4px',
  },
  shadows: {
    shadow: '0 4px 16px rgba(0, 0, 0, 0.08)',
    shadowCard: '0 2px 8px rgba(0, 0, 0, 0.06)',
    shadowFocus: '0 0 0 3px rgba(13, 148, 136, 0.3)',
  },

  // Z-indices (rarely need customization)
  zIndices: {
    zIndex10: 10,
    zIndex20: 20,
    zIndex30: 30,
    zIndex40: 40,
    zIndex50: 50,
  },
};

function App() {
  return (
    <CustomizationProvider baseTheme="default" theme={brandTheme}>
      <YourApplication />
    </CustomizationProvider>
  );
}

export default App;

Advanced Patterns

Dynamic Themes

Switch themes based on user preferences:
import { useState } from 'react';
import { CustomizationProvider } from '@twilio-paste/core/customization';

const lightTheme = {
  backgroundColors: {
    colorBackgroundBody: '#FFFFFF',
    colorBackgroundPrimary: '#0D9488',
  },
  textColors: {
    colorText: '#111827',
  },
};

const darkTheme = {
  backgroundColors: {
    colorBackgroundBody: '#1F2937',
    colorBackgroundPrimary: '#14B8A6',
  },
  textColors: {
    colorText: '#F9FAFB',
  },
};

function App() {
  const [isDark, setIsDark] = useState(false);
  const theme = isDark ? darkTheme : lightTheme;
  const baseTheme = isDark ? 'dark' : 'default';

  return (
    <CustomizationProvider baseTheme={baseTheme} theme={theme}>
      <button onClick={() => setIsDark(!isDark)}>
        Toggle Theme
      </button>
      <YourApplication />
    </CustomizationProvider>
  );
}

Merging with Current Theme

Extend the current theme instead of replacing it:
import { useTheme } from '@twilio-paste/core/theme';
import { CustomizationProvider } from '@twilio-paste/core/customization';

function ThemedApp() {
  const currentTheme = useTheme();

  const customTheme = {
    ...currentTheme,
    backgroundColors: {
      ...currentTheme.backgroundColors,
      colorBackgroundPrimary: '#0D9488',
    },
    textColors: {
      ...currentTheme.textColors,
      colorTextLink: '#0D9488',
    },
  };

  return (
    <CustomizationProvider theme={customTheme}>
      <App />
    </CustomizationProvider>
  );
}

Multi-Brand Support

Support multiple brands in one application:
const themes = {
  brandA: {
    backgroundColors: {
      colorBackgroundPrimary: '#0D9488',
    },
    fonts: {
      fontFamilyText: '"Inter", sans-serif',
    },
  },
  brandB: {
    backgroundColors: {
      colorBackgroundPrimary: '#DC2626',
    },
    fonts: {
      fontFamilyText: '"Roboto", sans-serif',
    },
  },
};

function App({ brandId }) {
  return (
    <CustomizationProvider
      baseTheme="default"
      theme={themes[brandId]}
    >
      <YourApplication />
    </CustomizationProvider>
  );
}

Environment-Based Themes

const getTheme = () => {
  if (process.env.NODE_ENV === 'development') {
    return {
      backgroundColors: {
        colorBackgroundBody: '#FFF7ED', // Orange tint
      },
    };
  }
  
  if (process.env.REACT_APP_STAGING) {
    return {
      backgroundColors: {
        colorBackgroundBody: '#F0FDF4', // Green tint
      },
    };
  }
  
  return {}; // Production uses default
};

function App() {
  return (
    <CustomizationProvider theme={getTheme()}>
      <YourApplication />
    </CustomizationProvider>
  );
}

Theme Generation

From Design Tokens

If you have design tokens from a design tool:
import { generateThemeFromTokens } from '@twilio-paste/theme';

const designTokens = {
  backgroundColors: {
    colorBackgroundPrimary: '#0D9488',
    // ... other background colors
  },
  borderColors: {
    colorBorder: '#E5E7EB',
    // ... other border colors
  },
  borderWidths: {
    borderWidth10: '1px',
    // ... other border widths
  },
  radii: {
    borderRadius20: '6px',
    // ... other radii
  },
  fonts: {
    fontFamilyText: '"Inter", sans-serif',
  },
  fontSizes: {
    fontSize30: '1rem',
    // ... other font sizes
  },
  fontWeights: {
    fontWeightBold: '700',
    // ... other font weights
  },
  lineHeights: {
    lineHeight30: '1.5rem',
    // ... other line heights
  },
  boxShadows: {
    shadow: '0 4px 16px rgba(0, 0, 0, 0.08)',
    // ... other shadows
  },
  sizings: {
    size0: '0',
    size10: '5.5rem',
    // ... all required sizing tokens
  },
  spacings: {
    space0: '0',
    space10: '0.125rem',
    // ... other spacing values
  },
  textColors: {
    colorText: '#111827',
    // ... other text colors
  },
  zIndices: {
    zIndex10: 10,
    // ... other z-indices
  },
  colors: {},
  colorSchemes: {},
  dataVisualization: {},
};

const customTheme = generateThemeFromTokens(designTokens);

<CustomizationProvider theme={customTheme}>
  <App />
</CustomizationProvider>

Programmatic Token Generation

function generateColorScale(baseColor) {
  // Use a color library like polished or chroma-js
  return {
    colorBackgroundPrimary: baseColor,
    colorBackgroundPrimaryStrong: darken(0.1, baseColor),
    colorBackgroundPrimaryStronger: darken(0.2, baseColor),
    colorBackgroundPrimaryStrongest: darken(0.3, baseColor),
    colorBackgroundPrimaryWeakest: lighten(0.4, baseColor),
  };
}

const theme = {
  backgroundColors: generateColorScale('#0D9488'),
};

Combining with Element Customization

Themes work seamlessly with element customization:
<CustomizationProvider
  baseTheme="default"
  theme={{
    backgroundColors: {
      colorBackgroundPrimary: '#0D9488',
    },
    radii: {
      borderRadius20: '6px',
    },
  }}
  elements={{
    BUTTON: {
      borderRadius: 'borderRadius30', // Uses theme token
      fontWeight: 'fontWeightBold',
      variants: {
        primary: {
          backgroundColor: 'colorBackgroundPrimary', // Uses custom color
        },
      },
    },
  }}
>
  <App />
</CustomizationProvider>

Testing Custom Themes

Visual Testing

import { Theme } from '@twilio-paste/core/theme';
import { CustomizationProvider } from '@twilio-paste/core/customization';

function ThemeShowcase({ theme }) {
  return (
    <CustomizationProvider theme={theme}>
      <Box padding="space70">
        <Heading as="h1" variant="heading10">Heading</Heading>
        <Paragraph>This is paragraph text.</Paragraph>
        <Button variant="primary">Primary Button</Button>
        <Button variant="secondary">Secondary Button</Button>
        <Input placeholder="Input field" />
      </Box>
    </CustomizationProvider>
  );
}

Accessibility Testing

Ensure your custom theme maintains accessibility:
import { useThemeContrastCheck } from '@twilio-paste/theme';

function ThemeValidator({ theme }) {
  const contrastIssues = useThemeContrastCheck(theme);
  
  if (contrastIssues.length > 0) {
    console.warn('Contrast issues detected:', contrastIssues);
  }
  
  return <App />;
}

Best Practices

1. Start Small

Begin with minimal overrides:
// Good: Focused customization
const theme = {
  backgroundColors: {
    colorBackgroundPrimary: '#0D9488',
  },
};

// Avoid: Over-customizing initially
const theme = {
  // 500 lines of token overrides
};

2. Maintain Semantic Meaning

// Good: Colors match their semantic purpose
const theme = {
  backgroundColors: {
    colorBackgroundPrimary: '#0D9488',      // Brand color
    colorBackgroundDestructive: '#DC2626',  // Red for errors
    colorBackgroundSuccess: '#059669',      // Green for success
  },
};

// Avoid: Confusing semantics
const theme = {
  backgroundColors: {
    colorBackgroundPrimary: '#DC2626',      // Red as primary?
    colorBackgroundDestructive: '#059669',  // Green for errors?
  },
};

3. Test Across Components

Ensure your theme works with all components you use:
<CustomizationProvider theme={myTheme}>
  <ComponentShowcase>
    <Button variant="primary">Button</Button>
    <Input placeholder="Input" />
    <Alert variant="warning">Alert</Alert>
    <Card>Card content</Card>
    {/* Test all components */}
  </ComponentShowcase>
</CustomizationProvider>

4. Document Your Theme

/**
 * Brand Theme for Acme Corp
 * 
 * Key customizations:
 * - Primary brand color: Teal (#0D9488)
 * - Font family: Inter
 * - Border radius: Slightly more rounded (6px vs 4px)
 * 
 * Last updated: 2024-03-04
 * Designer: Jane Doe
 */
const acmeTheme = {
  // ...
};

5. Version Your Themes

export const acmeThemeV1 = { /* ... */ };
export const acmeThemeV2 = { /* ... */ };

// Allow gradual migration
const theme = useFeatureFlag('new-theme') ? acmeThemeV2 : acmeThemeV1;

Common Pitfalls

Missing Required Tokens

Some tokens depend on others:
// Problem: Incomplete color set
const theme = {
  backgroundColors: {
    colorBackgroundPrimary: '#0D9488',
    // Missing colorBackgroundPrimaryStrong, etc.
  },
};

// Solution: Define complete sets
const theme = {
  backgroundColors: {
    colorBackgroundPrimary: '#0D9488',
    colorBackgroundPrimaryStrong: '#0F766E',
    colorBackgroundPrimaryStronger: '#115E59',
    colorBackgroundPrimaryStrongest: '#134E4A',
    colorBackgroundPrimaryWeakest: '#CCFBF1',
  },
};

Hardcoded Units

Avoid mixing unit types:
// Problem: Mixed units
const theme = {
  space: {
    space30: '8px',
    space40: '0.75rem', // Different unit!
  },
};

// Solution: Consistent units
const theme = {
  space: {
    space30: '0.5rem',
    space40: '0.75rem',
  },
};

Poor Contrast

Always check accessibility:
// Problem: Low contrast
const theme = {
  backgroundColors: {
    colorBackgroundBody: '#F3F4F6',
  },
  textColors: {
    colorText: '#E5E7EB', // Too light!
  },
};

// Solution: Sufficient contrast (4.5:1 minimum)
const theme = {
  backgroundColors: {
    colorBackgroundBody: '#FFFFFF',
  },
  textColors: {
    colorText: '#111827',
  },
};

Next Steps

Build docs developers (and LLMs) love