> ## Documentation Index
> Fetch the complete documentation index at: https://primer.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Styles

> Customizable styling variables for Primer Checkout

export const DEFAULT_THEME = {
  primerColorBrand: '#2f98ff',
  primerColorFocus: '#2f98ff',
  primerColorLoader: '#2f98ff',
  primerColorBackgroundPrimary: '#ffffff',
  primerColorBackgroundSecondary: 'rgba(33, 33, 33, 0.04)',
  primerColorTextPrimary: '#212121',
  primerColorTextSecondary: 'rgba(33, 33, 33, 0.62)',
  primerColorTextPlaceholder: 'rgba(33, 33, 33, 0.44)',
  primerColorTextDisabled: 'rgba(33, 33, 33, 0.3)',
  primerColorTextNegative: '#b4324b',
  primerColorBorderOutlinedDefault: 'rgba(33, 33, 33, 0.14)',
  primerColorBorderOutlinedHover: 'rgba(33, 33, 33, 0.3)',
  primerColorBorderOutlinedError: '#ff7279',
  primerColorIconPrimary: '#212121',
  primerColorIconPositive: '#3eb68f',
  primerColorIconNegative: '#ff7279',
  primerSpaceXsmall: '4px',
  primerSpaceSmall: '8px',
  primerSpaceMedium: '12px',
  primerSpaceLarge: '16px',
  primerSpaceXlarge: '20px',
  primerRadiusSmall: '4px',
  primerRadiusMedium: '8px',
  primerRadiusLarge: '12px',
  primerWidthDefault: '1',
  primerWidthFocus: '2',
  primerTypographyBrand: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif",
  primerTypographyBodyLargeSize: '16px',
  primerTypographyBodyMediumSize: '14px',
  primerTypographyBodySmallSize: '12px',
  primerTypographyTitleLargeWeight: '550',
  primerTypographyBodyLargeWeight: '400',
  primerAnimationDuration: '200ms',
  primerAnimationEasing: 'cubic-bezier(0.44, 0, 0.4, 1)'
};

export const createTheme = overrides => Object.assign({}, DEFAULT_THEME, overrides);

export const PRESETS = {
  default: {
    name: 'Default',
    theme: DEFAULT_THEME
  },
  dark: {
    name: 'Dark Mode',
    theme: createTheme({
      primerColorBackgroundPrimary: '#171619',
      primerColorBackgroundSecondary: 'rgba(239, 239, 239, 0.04)',
      primerColorTextPrimary: '#efefef',
      primerColorTextSecondary: 'rgba(239, 239, 239, 0.62)',
      primerColorTextPlaceholder: 'rgba(239, 239, 239, 0.44)',
      primerColorTextDisabled: 'rgba(239, 239, 239, 0.3)',
      primerColorTextNegative: '#f6bfbf',
      primerColorBorderOutlinedDefault: 'rgba(239, 239, 239, 0.14)',
      primerColorBorderOutlinedHover: 'rgba(239, 239, 239, 0.3)',
      primerColorBorderOutlinedError: '#e46d70',
      primerColorIconPrimary: '#efefef',
      primerColorIconPositive: '#27b17d',
      primerColorIconNegative: '#e46d70'
    })
  },
  minimal: {
    name: 'Minimal',
    theme: createTheme({
      primerRadiusSmall: '0px',
      primerRadiusMedium: '0px',
      primerRadiusLarge: '0px',
      primerWidthDefault: '1'
    })
  },
  rounded: {
    name: 'Rounded',
    theme: createTheme({
      primerRadiusSmall: '8px',
      primerRadiusMedium: '16px',
      primerRadiusLarge: '24px',
      primerColorBrand: '#8b5cf6',
      primerColorFocus: '#8b5cf6',
      primerColorLoader: '#8b5cf6'
    })
  },
  bold: {
    name: 'Bold',
    theme: createTheme({
      primerColorBrand: '#000000',
      primerColorFocus: '#000000',
      primerColorLoader: '#000000',
      primerWidthDefault: '2',
      primerTypographyTitleLargeWeight: '700'
    })
  },
  nature: {
    name: 'Nature',
    theme: createTheme({
      primerColorBrand: '#059669',
      primerColorFocus: '#059669',
      primerColorLoader: '#059669',
      primerColorIconPositive: '#059669',
      primerColorBackgroundSecondary: 'rgba(5, 150, 105, 0.08)',
      primerRadiusMedium: '12px'
    })
  }
};

export const TOKEN_GROUPS = [{
  name: 'Brand & Focus',
  tokens: [{
    key: 'primerColorBrand',
    label: 'Brand',
    type: 'color'
  }, {
    key: 'primerColorFocus',
    label: 'Focus',
    type: 'color'
  }, {
    key: 'primerColorLoader',
    label: 'Loader',
    type: 'color'
  }]
}, {
  name: 'Background',
  tokens: [{
    key: 'primerColorBackgroundPrimary',
    label: 'Primary',
    type: 'color'
  }, {
    key: 'primerColorBackgroundSecondary',
    label: 'Secondary',
    type: 'color'
  }]
}, {
  name: 'Text',
  tokens: [{
    key: 'primerColorTextPrimary',
    label: 'Primary',
    type: 'color'
  }, {
    key: 'primerColorTextSecondary',
    label: 'Secondary',
    type: 'color'
  }, {
    key: 'primerColorTextPlaceholder',
    label: 'Placeholder',
    type: 'color'
  }, {
    key: 'primerColorTextDisabled',
    label: 'Disabled',
    type: 'color'
  }, {
    key: 'primerColorTextNegative',
    label: 'Error',
    type: 'color'
  }]
}, {
  name: 'Border',
  tokens: [{
    key: 'primerColorBorderOutlinedDefault',
    label: 'Default',
    type: 'color'
  }, {
    key: 'primerColorBorderOutlinedHover',
    label: 'Hover',
    type: 'color'
  }, {
    key: 'primerColorBorderOutlinedError',
    label: 'Error',
    type: 'color'
  }]
}, {
  name: 'Icon',
  tokens: [{
    key: 'primerColorIconPrimary',
    label: 'Primary',
    type: 'color'
  }, {
    key: 'primerColorIconPositive',
    label: 'Success',
    type: 'color'
  }, {
    key: 'primerColorIconNegative',
    label: 'Error',
    type: 'color'
  }]
}, {
  name: 'Spacing',
  tokens: [{
    key: 'primerSpaceXsmall',
    label: 'X-Small (4px)',
    type: 'size',
    min: 0,
    max: 16
  }, {
    key: 'primerSpaceSmall',
    label: 'Small (8px)',
    type: 'size',
    min: 0,
    max: 24
  }, {
    key: 'primerSpaceMedium',
    label: 'Medium (12px)',
    type: 'size',
    min: 0,
    max: 32
  }, {
    key: 'primerSpaceLarge',
    label: 'Large (16px)',
    type: 'size',
    min: 0,
    max: 48
  }, {
    key: 'primerSpaceXlarge',
    label: 'X-Large (20px)',
    type: 'size',
    min: 0,
    max: 64
  }]
}, {
  name: 'Border Radius',
  tokens: [{
    key: 'primerRadiusSmall',
    label: 'Small (4px)',
    type: 'size',
    min: 0,
    max: 16
  }, {
    key: 'primerRadiusMedium',
    label: 'Medium (8px)',
    type: 'size',
    min: 0,
    max: 24
  }, {
    key: 'primerRadiusLarge',
    label: 'Large (12px)',
    type: 'size',
    min: 0,
    max: 32
  }]
}, {
  name: 'Border Width',
  tokens: [{
    key: 'primerWidthDefault',
    label: 'Default (1)',
    type: 'size',
    min: 1,
    max: 4
  }, {
    key: 'primerWidthFocus',
    label: 'Focus (2)',
    type: 'size',
    min: 1,
    max: 4
  }]
}, {
  name: 'Typography',
  tokens: [{
    key: 'primerTypographyBrand',
    label: 'Font Family',
    type: 'text'
  }, {
    key: 'primerTypographyBodySmallSize',
    label: 'Body Small',
    type: 'size',
    min: 10,
    max: 16
  }, {
    key: 'primerTypographyBodyMediumSize',
    label: 'Body Medium',
    type: 'size',
    min: 12,
    max: 20
  }, {
    key: 'primerTypographyBodyLargeSize',
    label: 'Body Large',
    type: 'size',
    min: 14,
    max: 24
  }, {
    key: 'primerTypographyTitleLargeWeight',
    label: 'Title Weight',
    type: 'size',
    min: 400,
    max: 700
  }]
}];

export const TokenInput = ({token, value, onChange}) => {
  if (token.type === 'color') {
    return <div style={{
      display: 'flex',
      alignItems: 'center',
      gap: '8px'
    }}>
        <input type="color" value={value} onChange={e => onChange(token.key, e.target.value)} style={{
      width: '40px',
      height: '32px',
      border: 'none',
      cursor: 'pointer',
      borderRadius: '4px'
    }} />
        <input type="text" value={value} onChange={e => onChange(token.key, e.target.value)} className="theme-builder-input-text monospace" style={{
      width: '90px'
    }} />
      </div>;
  }
  if (token.type === 'size') {
    const numValue = parseInt(value) || 0;
    const isUnitless = token.key.includes('Width') || token.key.includes('Weight');
    return <div style={{
      display: 'flex',
      alignItems: 'center',
      gap: '8px'
    }}>
        <input type="range" min={token.min || 0} max={token.max || 32} value={numValue} onChange={e => onChange(token.key, isUnitless ? e.target.value : `${e.target.value}px`)} style={{
      width: '80px'
    }} />
        <span style={{
      fontSize: '12px',
      fontFamily: 'monospace',
      minWidth: '40px'
    }}>{value}</span>
      </div>;
  }
  return <input type="text" value={value} onChange={e => onChange(token.key, e.target.value)} className="theme-builder-input-text" style={{
    width: '140px'
  }} />;
};

export const CheckoutPreview = ({theme}) => {
  const isDark = theme.primerColorBackgroundPrimary !== '#ffffff';
  const borderWidth = String(theme.primerWidthDefault).includes('px') ? theme.primerWidthDefault : `${theme.primerWidthDefault}px`;
  const focusBorderWidth = String(theme.primerWidthFocus).includes('px') ? theme.primerWidthFocus : `${theme.primerWidthFocus}px`;
  return <div style={{
    background: theme.primerColorBackgroundPrimary,
    padding: theme.primerSpaceLarge,
    borderRadius: theme.primerRadiusLarge,
    fontFamily: theme.primerTypographyBrand
  }}>
      <div style={{
    marginBottom: theme.primerSpaceLarge
  }}>
        <h3 style={{
    color: theme.primerColorTextPrimary,
    fontSize: theme.primerTypographyBodyLargeSize,
    fontWeight: theme.primerTypographyTitleLargeWeight,
    margin: '0 0 4px 0'
  }}>Payment Details</h3>
        <p style={{
    color: theme.primerColorTextSecondary,
    fontSize: theme.primerTypographyBodySmallSize,
    margin: 0
  }}>Enter your card information</p>
      </div>
      <div style={{
    marginBottom: theme.primerSpaceMedium
  }}>
        <label style={{
    display: 'block',
    color: theme.primerColorTextPrimary,
    fontSize: theme.primerTypographyBodySmallSize,
    fontWeight: theme.primerTypographyBodyLargeWeight,
    marginBottom: theme.primerSpaceXsmall
  }}>Card Number</label>
        <div style={{
    background: theme.primerColorBackgroundPrimary,
    border: `${focusBorderWidth} solid ${theme.primerColorFocus}`,
    borderRadius: theme.primerRadiusSmall,
    padding: theme.primerSpaceMedium,
    display: 'flex',
    alignItems: 'center',
    gap: theme.primerSpaceSmall,
    boxShadow: `0 0 0 1px ${theme.primerColorFocus}20`
  }}>
          <span style={{
    color: theme.primerColorTextPrimary,
    fontSize: theme.primerTypographyBodyLargeSize
  }}>4242 4242 4242 4242</span>
          <svg style={{
    marginLeft: 'auto',
    width: '24px',
    height: '24px',
    fill: theme.primerColorIconPrimary
  }} viewBox="0 0 24 24"><rect x="2" y="5" width="20" height="14" rx="2" stroke="currentColor" strokeWidth="1.5" fill="none" /><line x1="2" y1="10" x2="22" y2="10" stroke="currentColor" strokeWidth="1.5" /></svg>
        </div>
      </div>
      <div style={{
    display: 'grid',
    gridTemplateColumns: '1fr 1fr',
    gap: theme.primerSpaceMedium,
    marginBottom: theme.primerSpaceMedium
  }}>
        <div>
          <label style={{
    display: 'block',
    color: theme.primerColorTextPrimary,
    fontSize: theme.primerTypographyBodySmallSize,
    fontWeight: theme.primerTypographyBodyLargeWeight,
    marginBottom: theme.primerSpaceXsmall
  }}>Expiry</label>
          <div style={{
    background: theme.primerColorBackgroundPrimary,
    border: `${borderWidth} solid ${theme.primerColorBorderOutlinedDefault}`,
    borderRadius: theme.primerRadiusSmall,
    padding: theme.primerSpaceMedium
  }}>
            <span style={{
    color: theme.primerColorTextPlaceholder,
    fontSize: theme.primerTypographyBodyLargeSize
  }}>12/25</span>
          </div>
        </div>
        <div>
          <label style={{
    display: 'block',
    color: theme.primerColorTextPrimary,
    fontSize: theme.primerTypographyBodySmallSize,
    fontWeight: theme.primerTypographyBodyLargeWeight,
    marginBottom: theme.primerSpaceXsmall
  }}>CVV</label>
          <div style={{
    background: theme.primerColorBackgroundPrimary,
    border: `${borderWidth} solid ${theme.primerColorBorderOutlinedDefault}`,
    borderRadius: theme.primerRadiusSmall,
    padding: theme.primerSpaceMedium
  }}>
            <span style={{
    color: theme.primerColorTextPlaceholder,
    fontSize: theme.primerTypographyBodyLargeSize
  }}>•••</span>
          </div>
        </div>
      </div>
      <button style={{
    width: '100%',
    padding: theme.primerSpaceMedium,
    background: theme.primerColorBrand,
    color: '#ffffff',
    border: 'none',
    borderRadius: theme.primerRadiusMedium,
    fontSize: theme.primerTypographyBodyLargeSize,
    fontWeight: theme.primerTypographyTitleLargeWeight,
    cursor: 'pointer',
    transition: `all ${theme.primerAnimationDuration} ${theme.primerAnimationEasing}`
  }}>
        Pay
      </button>
    </div>;
};

export const toCssVar = key => '--' + key.replace(/([A-Z])/g, '-$1').toLowerCase();

export const KOTLIN_MAP = {
  primerColorBrand: {
    g: 'color',
    p: 'primerColorBrand'
  },
  primerColorFocus: {
    g: 'color',
    p: 'primerColorFocus'
  },
  primerColorLoader: {
    g: 'color',
    p: 'primerColorLoader'
  },
  primerColorBackgroundPrimary: {
    g: 'color',
    p: 'primerColorBackground'
  },
  primerColorTextPrimary: {
    g: 'color',
    p: 'primerColorTextPrimary'
  },
  primerColorTextSecondary: {
    g: 'color',
    p: 'primerColorTextSecondary'
  },
  primerColorTextPlaceholder: {
    g: 'color',
    p: 'primerColorTextPlaceholder'
  },
  primerColorTextDisabled: {
    g: 'color',
    p: 'primerColorTextDisabled'
  },
  primerColorTextNegative: {
    g: 'color',
    p: 'primerColorTextNegative'
  },
  primerColorBorderOutlinedDefault: {
    g: 'color',
    p: 'primerColorBorderOutlinedDefault'
  },
  primerColorBorderOutlinedHover: {
    g: 'color',
    p: 'primerColorBorderOutlinedHover'
  },
  primerColorBorderOutlinedError: {
    g: 'color',
    p: 'primerColorBorderOutlinedError'
  },
  primerColorIconPrimary: {
    g: 'color',
    p: 'primerColorIconPrimary'
  },
  primerColorIconPositive: {
    g: 'color',
    p: 'primerColorIconPositive'
  },
  primerColorIconNegative: {
    g: 'color',
    p: 'primerColorIconNegative'
  },
  primerSpaceXsmall: {
    g: 'spacing',
    p: 'xsmall'
  },
  primerSpaceSmall: {
    g: 'spacing',
    p: 'small'
  },
  primerSpaceMedium: {
    g: 'spacing',
    p: 'medium'
  },
  primerSpaceLarge: {
    g: 'spacing',
    p: 'large'
  },
  primerSpaceXlarge: {
    g: 'spacing',
    p: 'xlarge'
  },
  primerRadiusSmall: {
    g: 'radius',
    p: 'small'
  },
  primerRadiusMedium: {
    g: 'radius',
    p: 'medium'
  },
  primerRadiusLarge: {
    g: 'radius',
    p: 'large'
  },
  primerWidthDefault: {
    g: 'borderWidth',
    p: 'thin'
  },
  primerWidthFocus: {
    g: 'borderWidth',
    p: 'medium'
  }
};

export const SWIFT_MAP = {
  primerColorBrand: {
    g: 'colors',
    p: 'primerColorBrand'
  },
  primerColorFocus: {
    g: 'colors',
    p: 'primerColorFocus'
  },
  primerColorLoader: {
    g: 'colors',
    p: 'primerColorLoader'
  },
  primerColorBackgroundPrimary: {
    g: 'colors',
    p: 'primerColorBackground'
  },
  primerColorTextPrimary: {
    g: 'colors',
    p: 'primerColorTextPrimary'
  },
  primerColorTextSecondary: {
    g: 'colors',
    p: 'primerColorTextSecondary'
  },
  primerColorTextPlaceholder: {
    g: 'colors',
    p: 'primerColorTextPlaceholder'
  },
  primerColorTextDisabled: {
    g: 'colors',
    p: 'primerColorTextDisabled'
  },
  primerColorTextNegative: {
    g: 'colors',
    p: 'primerColorTextNegative'
  },
  primerColorBorderOutlinedDefault: {
    g: 'colors',
    p: 'primerColorBorderOutlinedDefault'
  },
  primerColorBorderOutlinedHover: {
    g: 'colors',
    p: 'primerColorBorderOutlinedHover'
  },
  primerColorBorderOutlinedError: {
    g: 'colors',
    p: 'primerColorBorderOutlinedError'
  },
  primerColorIconPrimary: {
    g: 'colors',
    p: 'primerColorIconPrimary'
  },
  primerColorIconPositive: {
    g: 'colors',
    p: 'primerColorIconPositive'
  },
  primerColorIconNegative: {
    g: 'colors',
    p: 'primerColorIconNegative'
  },
  primerRadiusSmall: {
    g: 'radius',
    p: 'primerRadiusSmall'
  },
  primerRadiusMedium: {
    g: 'radius',
    p: 'primerRadiusMedium'
  },
  primerRadiusLarge: {
    g: 'radius',
    p: 'primerRadiusLarge'
  },
  primerSpaceXsmall: {
    g: 'spacing',
    p: 'primerSpaceXsmall'
  },
  primerSpaceSmall: {
    g: 'spacing',
    p: 'primerSpaceSmall'
  },
  primerSpaceMedium: {
    g: 'spacing',
    p: 'primerSpaceMedium'
  },
  primerSpaceLarge: {
    g: 'spacing',
    p: 'primerSpaceLarge'
  },
  primerSpaceXlarge: {
    g: 'spacing',
    p: 'primerSpaceXlarge'
  },
  primerWidthDefault: {
    g: 'borderWidth',
    p: 'primerBorderWidthThin'
  },
  primerWidthFocus: {
    g: 'borderWidth',
    p: 'primerBorderWidthMedium'
  }
};

export const toKotlinColor = hex => {
  if (!hex.startsWith('#')) return null;
  return 'Color(0xFF' + hex.replace('#', '').toUpperCase() + ')';
};

export const toSwiftColor = hex => {
  if (!hex.startsWith('#')) return null;
  var r = parseInt(hex.slice(1, 3), 16);
  var g = parseInt(hex.slice(3, 5), 16);
  var b = parseInt(hex.slice(5, 7), 16);
  return 'Color(red: ' + (r / 255).toFixed(2) + ', green: ' + (g / 255).toFixed(2) + ', blue: ' + (b / 255).toFixed(2) + ')';
};

export const toKotlinCode = entries => {
  if (entries.length === 0) return '// No changes from default theme';
  const groups = {};
  entries.forEach(function (pair) {
    var m = KOTLIN_MAP[pair[0]];
    if (!m) return;
    if (!groups[m.g]) groups[m.g] = [];
    groups[m.g].push({
      p: m.p,
      v: pair[1]
    });
  });
  if (Object.keys(groups).length === 0) return '// These tokens have no Android equivalent';
  var lines = ['val theme = PrimerTheme('];
  if (groups.color) {
    lines.push('    lightColorTokens = object : LightColorTokens() {');
    groups.color.forEach(function (item) {
      var c = toKotlinColor(item.v);
      if (c) {
        lines.push('        override val ' + item.p + ': Color = ' + c);
      } else {
        lines.push('        // ' + item.p + ': convert ' + item.v + ' to Color(0xAARRGGBB)');
      }
    });
    lines.push('    },');
  }
  if (groups.spacing) {
    lines.push('    spacingTokens = SpacingTokens(');
    groups.spacing.forEach(function (item) {
      lines.push('        ' + item.p + ' = ' + parseInt(item.v) + '.dp,');
    });
    lines.push('    ),');
  }
  if (groups.radius) {
    lines.push('    radiusTokens = RadiusTokens(');
    groups.radius.forEach(function (item) {
      lines.push('        ' + item.p + ' = ' + parseInt(item.v) + '.dp,');
    });
    lines.push('    ),');
  }
  if (groups.borderWidth) {
    lines.push('    borderWidthTokens = BorderWidthTokens(');
    groups.borderWidth.forEach(function (item) {
      lines.push('        ' + item.p + ' = ' + parseInt(item.v) + '.dp,');
    });
    lines.push('    ),');
  }
  lines.push(')');
  return lines.join('\n');
};

export const toSwiftCode = entries => {
  if (entries.length === 0) return '// No changes from default theme';
  var groups = {};
  entries.forEach(function (pair) {
    var m = SWIFT_MAP[pair[0]];
    if (!m) return;
    if (!groups[m.g]) groups[m.g] = [];
    groups[m.g].push({
      p: m.p,
      v: pair[1]
    });
  });
  if (Object.keys(groups).length === 0) return '// These tokens have no iOS equivalent';
  var sections = [];
  if (groups.colors) {
    var colorLines = groups.colors.map(function (item) {
      var c = toSwiftColor(item.v);
      return c ? '    ' + item.p + ': ' + c : '    // ' + item.p + ': convert ' + item.v + ' manually';
    });
    sections.push('  colors: ColorOverrides(\n' + colorLines.join(',\n') + '\n  )');
  }
  if (groups.radius) {
    var radiusLines = groups.radius.map(function (item) {
      return '    ' + item.p + ': ' + parseInt(item.v);
    });
    sections.push('  radius: RadiusOverrides(\n' + radiusLines.join(',\n') + '\n  )');
  }
  if (groups.spacing) {
    var spacingLines = groups.spacing.map(function (item) {
      return '    ' + item.p + ': ' + parseInt(item.v);
    });
    sections.push('  spacing: SpacingOverrides(\n' + spacingLines.join(',\n') + '\n  )');
  }
  if (groups.borderWidth) {
    var bwLines = groups.borderWidth.map(function (item) {
      return '    ' + item.p + ': ' + parseInt(item.v);
    });
    sections.push('  borderWidth: BorderWidthOverrides(\n' + bwLines.join(',\n') + '\n  )');
  }
  return 'let theme = PrimerCheckoutTheme(\n' + sections.join(',\n') + '\n)\n\nPrimerCheckout(\n  clientToken: clientToken,\n  primerTheme: theme\n)';
};

export const CodePreview = ({theme}) => {
  const [format, setFormat] = useState('css');
  const [copied, setCopied] = useState(false);
  const changedEntries = Object.entries(theme).filter(([key, value]) => DEFAULT_THEME[key] !== value);
  const hasChanges = changedEntries.length > 0;
  const cssOutput = changedEntries.map(([key, value]) => '  ' + toCssVar(key) + ': ' + value + ';').join('\n');
  const cssCode = hasChanges ? 'primer-checkout {\n' + cssOutput + '\n}' : '/* No changes from default theme */';
  const changedTheme = Object.fromEntries(changedEntries);
  const jsonCode = hasChanges ? JSON.stringify(changedTheme, null, 2) : '{}';
  const kotlinCode = toKotlinCode(changedEntries);
  const swiftCode = toSwiftCode(changedEntries);
  const code = format === 'css' ? cssCode : format === 'json' ? jsonCode : format === 'kotlin' ? kotlinCode : swiftCode;
  const handleCopy = () => {
    navigator.clipboard.writeText(code);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };
  return <div className="code-preview-container">
      <div className="code-preview-header">
        <div className="code-preview-tabs">
          <button onClick={() => setFormat('css')} className={`code-preview-tab ${format === 'css' ? 'active' : ''}`}>
            styles.css
          </button>
          <button onClick={() => setFormat('json')} className={`code-preview-tab ${format === 'json' ? 'active' : ''}`}>
            theme.json
          </button>
          <button onClick={() => setFormat('kotlin')} style={{
    padding: '10px 16px',
    background: 'transparent',
    border: 'none',
    borderBottom: format === 'kotlin' ? '2px solid #2f98ff' : '2px solid transparent',
    color: format === 'kotlin' ? '#2f98ff' : '#6b7280',
    cursor: 'pointer',
    fontSize: '13px',
    fontWeight: '500',
    fontFamily: 'monospace'
  }}>
            Theme.kt
          </button>
          <button onClick={() => setFormat('swift')} style={{
    padding: '10px 16px',
    background: 'transparent',
    border: 'none',
    borderBottom: format === 'swift' ? '2px solid #2f98ff' : '2px solid transparent',
    color: format === 'swift' ? '#2f98ff' : '#6b7280',
    cursor: 'pointer',
    fontSize: '13px',
    fontWeight: '500',
    fontFamily: 'monospace'
  }}>
            Theme.swift
          </button>
        </div>
        <button onClick={handleCopy} className={`code-preview-copy-btn ${copied ? 'copied' : ''}`} title="Copy to clipboard">
          {copied ? <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg> : <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>}
          {copied ? 'Copied' : 'Copy'}
        </button>
      </div>
      <pre className="code-preview-code">{code}</pre>
    </div>;
};

export const ThemeBuilder = () => {
  const [theme, setTheme] = useState(DEFAULT_THEME);
  const [activePreset, setActivePreset] = useState('default');
  const [expandedGroups, setExpandedGroups] = useState(['Brand & Focus']);
  const [previewDark, setPreviewDark] = useState(false);
  const previewTheme = useMemo(() => {
    if (!previewDark) return theme;
    const darkBase = Object.assign({}, PRESETS.dark.theme);
    const userChanges = Object.fromEntries(Object.entries(theme).filter(([key, value]) => DEFAULT_THEME[key] !== value));
    return Object.assign({}, darkBase, userChanges);
  }, [theme, previewDark]);
  const handleTokenChange = (key, value) => {
    setTheme(prev => ({
      ...prev,
      [key]: value
    }));
    setActivePreset(null);
  };
  const applyPreset = presetKey => {
    setTheme(PRESETS[presetKey].theme);
    setActivePreset(presetKey);
  };
  const toggleGroup = groupName => {
    setExpandedGroups(prev => prev.includes(groupName) ? prev.filter(g => g !== groupName) : [...prev, groupName]);
  };
  return <div>
      <div className="theme-builder-container">
        <div className="theme-builder-header">
          <h3 className="theme-builder-title">Theme Builder</h3>
          <div className="theme-builder-presets">
            {Object.entries(PRESETS).map(([key, preset]) => <button key={key} onClick={() => applyPreset(key)} className={`theme-builder-preset-btn ${activePreset === key ? 'active' : ''}`}>
                {preset.name}
              </button>)}
          </div>
        </div>
        <div className="theme-builder-grid">
          <div className="theme-builder-tokens">
            {TOKEN_GROUPS.map(group => <div key={group.name} className="theme-builder-group">
                <button onClick={() => toggleGroup(group.name)} className="theme-builder-group-header">
                  {group.name}
                  <span style={{
    transform: expandedGroups.includes(group.name) ? 'rotate(180deg)' : 'none',
    transition: 'transform 0.2s'
  }}>▼</span>
                </button>
                {expandedGroups.includes(group.name) && <div className="theme-builder-group-content">
                    {group.tokens.map(token => <div key={token.key} className="theme-builder-token-row">
                        <label className="theme-builder-token-label">{token.label}</label>
                        <TokenInput token={token} value={theme[token.key]} onChange={handleTokenChange} />
                      </div>)}
                  </div>}
              </div>)}
          </div>
          <div className={`theme-builder-preview-panel ${previewDark ? 'dark-bg' : ''}`}>
            <div className="theme-builder-preview-header">
              <span className="theme-builder-preview-label" style={{
    color: previewDark ? '#e5e7eb' : '#1a1a1a'
  }}>Preview</span>
              <button onClick={() => setPreviewDark(d => !d)} className={`theme-builder-theme-toggle ${previewDark ? 'dark' : ''}`}>
                {previewDark ? 'Dark' : 'Light'}
              </button>
            </div>
            <CheckoutPreview theme={previewTheme} />
          </div>
        </div>
      </div>
      <CodePreview theme={theme} />
    </div>;
};

Primer Checkout is fully customizable. You can customize colors, typography, spacing, borders, and component layout (down to the smallest visual detail) without touching internal component code or breaking upgrades.

## Styling methods

<Tabs>
  <Tab title="Web">
    On the web, Primer Checkout uses **CSS variables** for theming. You can apply styling in two ways - directly in your CSS or through component attributes.

    <Tabs>
      <Tab title="CSS">
        ```css styles.css theme={"dark"}
        primer-checkout {
          --primer-color-brand: #2f98ff;
          --primer-color-focus: #2f98ff;
          --primer-radius-medium: 8px;
          --primer-typography-brand: 'Inter', sans-serif;
        }
        ```
      </Tab>

      <Tab title="HTML">
        ```html index.html theme={"dark"}
        <primer-checkout
          customStyles='{"primerColorBrand":"#2f98ff","primerColorFocus":"#2f98ff","primerRadiusMedium":"8px"}'
        ></primer-checkout>
        ```

        When passing through a component attribute, convert kebab-case CSS variables to camelCase properties:

        1. Remove the leading dashes (`--`)
        2. Convert hyphens to camelCase by removing the hyphen and capitalizing the next letter

        **Examples:**

        * `--primer-color-brand` → `primerColorBrand`
        * `--primer-space-medium` → `primerSpaceMedium`
        * `--primer-typography-body-large-font` → `primerTypographyBodyLargeFont`
      </Tab>
    </Tabs>
  </Tab>

  <Tab title="Android">
    On Android, Primer Checkout uses a token-based design system through `PrimerTheme`. Pass a custom `PrimerTheme` to your checkout component:

    ```kotlin theme={"dark"}
    val theme = PrimerTheme(
        lightColorTokens = object : LightColorTokens() {
            override val primerColorBrand: Color = Color(0xFF6C5CE7)
        },
    )

    PrimerCheckoutSheet(
        checkout = checkout,
        theme = theme,
    )
    ```
  </Tab>

  <Tab title="iOS">
    On iOS, Primer Checkout uses `PrimerCheckoutTheme` for theming. Pass a custom theme to your checkout view:

    ```swift theme={"dark"}
    let theme = PrimerCheckoutTheme(
      colors: ColorOverrides(
        primerColorBrand: .purple
      ),
      radius: RadiusOverrides(
        primerRadiusMedium: 12
      )
    )

    PrimerCheckout(
      clientToken: clientToken,
      primerTheme: theme
    )
    ```
  </Tab>
</Tabs>

## Using styling variables in practice

<Tabs>
  <Tab title="Web">
    <Tabs>
      <Tab title="With CSS Variables">
        ```css theme={"dark"}
        primer-checkout {
          --primer-color-brand: #663399;
          --primer-color-focus: #663399;
          --primer-color-loader: #663399;
          --primer-color-text-primary: #333333;
          --primer-color-text-secondary: rgba(51, 51, 51, 0.62);
          --primer-typography-brand: 'Helvetica Neue', sans-serif;
          --primer-space-medium: 16px;
          --primer-radius-medium: 8px;
        }
        ```
      </Tab>

      <Tab title="With customStyles property">
        ```javascript theme={"dark"}
        const myCustomStyles = {
          primerColorBrand: '#663399',
          primerColorFocus: '#663399',
          primerColorLoader: '#663399',
          primerColorTextPrimary: '#333333',
          primerTypographyBrand: 'Helvetica Neue, sans-serif',
          primerSpaceMedium: '16px',
          primerRadiusMedium: '8px',
        };

        const checkoutElement = document.querySelector('primer-checkout');
        checkoutElement.setAttribute('customStyles', JSON.stringify(myCustomStyles));
        ```

        This approach is particularly useful when working with JavaScript frameworks or when you need to apply styling variables programmatically.

        The component will parse the JSON and automatically apply the styles as CSS Variables internally.
      </Tab>
    </Tabs>

    You can see all the available styling variables on [the dedicated reference page](/sdk/primer-checkout-web/styling-variables).
  </Tab>

  <Tab title="Android">
    `PrimerTheme` contains five token groups:

    | Token Group   | Class               | What It Controls                        |
    | ------------- | ------------------- | --------------------------------------- |
    | Light colors  | `LightColorTokens`  | All colors in light mode                |
    | Dark colors   | `DarkColorTokens`   | All colors in dark mode                 |
    | Spacing       | `SpacingTokens`     | Padding and margins                     |
    | Typography    | `TypographyTokens`  | Font sizes, weights, line heights       |
    | Radius        | `RadiusTokens`      | Corner radius for cards, inputs, sheets |
    | Border widths | `BorderWidthTokens` | Border widths for inputs, focus rings   |
    | Sizes         | `SizeTokens`        | Icon sizes and touch targets            |

    ```kotlin theme={"dark"}
    val brandTheme = PrimerTheme(
        lightColorTokens = object : LightColorTokens() {
            override val primerColorBrand: Color = Color(0xFF6C5CE7)
            override val primerColorGray100: Color = Color(0xFFF7F7F8)
        },
        darkColorTokens = object : DarkColorTokens() {
            override val primerColorBrand: Color = Color(0xFFA29BFE)
        },
        spacingTokens = SpacingTokens(
            small = 6.dp,
            medium = 10.dp,
            large = 14.dp,
        ),
        radiusTokens = RadiusTokens(
            medium = 12.dp,
            large = 20.dp,
        ),
    )

    PrimerCheckoutSheet(
        checkout = checkout,
        theme = brandTheme,
    )
    ```

    See the [Design Tokens Reference](/checkout/primer-checkout/build-your-ui/design-tokens-explorer) for a complete list of all available tokens.
  </Tab>

  <Tab title="iOS">
    `PrimerCheckoutTheme` is organized into six override categories:

    | Category         | Type                   | Controls                                             |
    | ---------------- | ---------------------- | ---------------------------------------------------- |
    | **Colors**       | `ColorOverrides`       | Brand, text, backgrounds, borders, icons             |
    | **Radius**       | `RadiusOverrides`      | Corner radius for inputs, buttons, cards             |
    | **Spacing**      | `SpacingOverrides`     | Padding and margins                                  |
    | **Sizes**        | `SizeOverrides`        | Component dimensions (icons, buttons, touch targets) |
    | **Typography**   | `TypographyOverrides`  | Font family, size, weight, line height               |
    | **Border width** | `BorderWidthOverrides` | Stroke widths for borders                            |

    ```swift theme={"dark"}
    let theme = PrimerCheckoutTheme(
      colors: ColorOverrides(
        primerColorBrand: .indigo,
        primerColorTextPrimary: Color(.label),
        primerColorBackground: Color(.systemBackground),
        primerColorBorderOutlinedFocus: .indigo
      ),
      radius: RadiusOverrides(
        primerRadiusMedium: 10,
        primerRadiusLarge: 16
      ),
      spacing: SpacingOverrides(
        primerSpaceMedium: 12,
        primerSpaceLarge: 16
      ),
      typography: TypographyOverrides(
        bodyMedium: .init(font: "Inter", size: 14)
      )
    )

    PrimerCheckout(
      clientToken: clientToken,
      primerTheme: theme
    )
    ```

    See the [Design Tokens Reference](/checkout/primer-checkout/build-your-ui/design-tokens-explorer) for a complete list of all available tokens.
  </Tab>
</Tabs>

## Theme support

Primer Checkout supports both light and dark themes.

<Tabs>
  <Tab title="Web">
    The default option is light. To use a dark theme, pass a `primer-dark-theme` CSS class:

    ```html theme={"dark"}
    <primer-checkout client-token="your-client-token" class="primer-dark-theme">
    </primer-checkout>
    ```

    You can toggle between themes using JavaScript:

    ```javascript theme={"dark"}
    function toggleTheme(isDark) {
      const container = document.querySelector('primer-checkout');
      container.className = isDark ? 'primer-dark-theme' : 'primer-light-theme';
    }

    toggleTheme(true);
    toggleTheme(false);
    ```
  </Tab>

  <Tab title="Android">
    `DarkColorTokens` extends `LightColorTokens` and overrides base color values. Semantic tokens automatically resolve to the correct values through inheritance:

    ```
    Light mode:  primerColorBackground → primerColorGray000 → #FFFFFF
    Dark mode:   primerColorBackground → primerColorGray000 → #171619
    ```

    The SDK detects the system theme automatically via `isSystemInDarkTheme()`.

    ```kotlin theme={"dark"}
    val theme = PrimerTheme(
        lightColorTokens = object : LightColorTokens() {
            override val primerColorBrand: Color = Color(0xFFE53E3E)
            override val primerColorBackground: Color = Color(0xFFF8F8F8)
        },
        darkColorTokens = object : DarkColorTokens() {
            override val primerColorBrand: Color = Color(0xFFFF6B6B)
        },
    )
    ```

    `PrimerTheme` automatically maps color tokens to a Material 3 `ColorScheme`. Material components used within the checkout (like `ModalBottomSheet`) inherit your Primer color tokens without additional configuration.
  </Tab>

  <Tab title="iOS">
    The built-in theme automatically adapts to the system appearance (light/dark mode). Use SwiftUI adaptive colors to support both modes:

    ```swift theme={"dark"}
    let theme = PrimerCheckoutTheme(
      colors: ColorOverrides(
        primerColorBrand: Color("BrandColor"),        // From asset catalog
        primerColorBackground: Color(.systemBackground) // Adapts automatically
      )
    )
    ```

    <Tip>
      System colors like `Color(.label)` and `Color(.systemBackground)` automatically adapt to light and dark mode without additional configuration.
    </Tip>
  </Tab>
</Tabs>

## Try it out

Select a preset or adjust individual tokens to see how styling variables affect the checkout appearance.

<Note>This preview shows a subset of styling variables. Interactive states (hover, focus) are not displayed. See [Styling Variables](/sdk/primer-checkout-web/styling-variables) for a complete reference.</Note>

<ThemeBuilder />

## Try it live in StackBlitz

See styling in action with these interactive examples:

<table>
  <thead>
    <tr>
      <th width="200">Example</th>
      <th>Description</th>

      <th width="150" />
    </tr>
  </thead>

  <tbody>
    <tr>
      <td style={{ verticalAlign: 'middle' }}><strong>Theme variations</strong></td>
      <td style={{ verticalAlign: 'middle' }}>Explore light, dark and more</td>

      <td style={{ verticalAlign: 'middle' }}>
        <a href="https://stackblitz.com/fork/github/primer-io/examples/tree/main/examples/primer-checkout-themes" target="_blank">
          <img src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" alt="Open in StackBlitz" noZoom />
        </a>
      </td>
    </tr>

    <tr>
      <td style={{ verticalAlign: 'middle' }}><strong>Custom form styling</strong></td>
      <td style={{ verticalAlign: 'middle' }}>See custom CSS styling applied to a reordered payment form layout</td>

      <td style={{ verticalAlign: 'middle' }}>
        <a href="https://stackblitz.com/fork/github/primer-io/examples/tree/main/examples/primer-checkout-custom-form" target="_blank">
          <img src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" alt="Open in StackBlitz" noZoom />
        </a>
      </td>
    </tr>
  </tbody>
</table>
