Guide to starting an email design system

Introduction to email design systems

Email design systems can help speed up the email creation process, bring more consistency to the designs, and simplify maintenance, updates, and redesigns.

Let's start by taking a look at a design system in action.

Breaking down designs into components

Looking over the emails you are sending, you’ll likely see many repeated patterns, such as headers, footers, call to action(CTA), product cards, and highlighted sections. Start by listing these out.

Then, for each of these components, think about variants and group those together. For example, on a CTA, you may have primary, secondary, and tertiary variants. They may look quite different, but the function is the same.

The purpose of grouping these together is so a variant can be quickly changed as an email design evolves. For example, a heading could be quickly changed from level 2 to level 3 without having to replace it and redo any content or settings.

Plan for styling options

Think about how much flexibility you want to give the marketers. Should all styles be locked down, or should they be limited to being fully flexible?

For example is the font-size for this block fixed at 16px, is it a choice between 16px, 24px, 32px or 40px, perhaps it’s a range of any number between 14px and 40px or maybe there are no restrictions and they can do whatever they want?


Setting up the base component and style context

The first component we’re going to look at is a base (or root) component. This will be the starting point of every email, and it will only be used once per email.

From a code point of view this is where we will set things like <!DOCTYPE html> <html> <head> and <body>.

Below is an example of a base component based on the customer.io branding.

<script>
  export const config = { 
    label: "CIO Base" ,
    componentType: "root" 
  };
  export const slots = Component.defineSlots({
    default: Component.slots.any(),
  });
  const colors = {
    evergreen: '#0e464d', 
    verdant: '#E4FFCE',
    charcoal: '#151515',
    cosmic : '#FFF5C1',
    coconut: '#27201D',
    muave: '#FCECFF',
    sangria: '#3B0B22',
    lavender: '#D6D8FF',
    indigo: '#2C155D',
    arctic: '#C0F8F8',
    moss: '#2E340C',
    warmGray: '#FBF9F8',
    charcoal: '#151515'
  };
  const brand = {
    name: 'cio',
    base:{
      lang: 'en', 
      dir: 'ltr', 
      title: 'Email from Customer.io',
      'font-family':"Instrument Sans, helvetica, arial, sans-serif", 
      color:colors.charcoal, 
      'background-color':colors.warmGray
    },
    section:{
      width: '600px',
      padding: '0'
    },
    box:{
      normal:{
        padding: '20px'
      },
      highlight:{
        padding: '20px',
        color: colors.evergreen,
        'background-color': colors.verdant,
        'border-style': 'solid',
        'border-width': '8px 0 0 0'
      }
    },
    heading:{
      color: colors.evergreen,
    },
    cta:{
      primary:{
        align: 'left',
        color: colors.verdant,
        'background-color': colors.evergreen,
        'border-color': colors.evergreen,
        'border-style': 'solid',
        'border-width': '2px',
        'border-radius': '30px',
        padding: '10px 30px',
        'hover-background-color': 'transparent',
        'hover-color': colors.evergreen
      },
      secondary:{
        align: 'left',
        color: colors.evergreen,
        'background-color': '',
        'border-style': 'solid',
        'border-width': '2px',
        'border-color': colors.evergreen,
        'border-radius': '30px',
        padding: '10px 30px',
        'hover-background-color': colors.evergreen,
        'hover-color': colors.verdant
      },
      tertiary:{
        align: 'left',
        color: colors.evergreen,
        'text-decoration': 'underline',
        'background-color': '',
        padding: '',
        'hover-color': colors.charcoal 
      }
    }
  };
  
  Component.context.set('brand', brand);
  
</script>
<template>
<x-base #set="brand.base" #root>
<x-section #set="brand.section"> 
  <slot/>
</x-section>
</x-base>
</template>

Setting component type

Because this is our root component we need to ensure it’s set to componentType: "root" in the config. This means it can be used as a starting point for the visual editor.

  export const config = { 
    label: "CIO Base" ,
    componentType: "root" 
  };

Setting a slot

To allow us to add components to this component, we need to set up a slot. Here, we’re setting it to accept any components. However, you can add some restrictions or limitations here if you wish.

  export const slots = Component.defineSlots({
    default: Component.slots.any(),
  });

Setting design system styles

Firstly, we set up const colors = {} to create a shortcut we can use for our color pallet of the design. You could also set up other shortcuts like this if you feel it’s needed or skip over this completely.

Then, we set up the design system styles in

const brand = {
    name: 'cio',
    base:{
      lang: 'en', 
      dir: 'ltr', 
      title: 'Email from Customer.io',
      'font-family':"Instrument Sans, helvetica, arial, sans-serif", 
      color:colors.charcoal, 
      'background-color':colors.warmGray
    },
    section:{
      width: '600px',
      padding: '0'
    },
    ...
}

We’re building our design system on top of the standard components already built into Parcel. So here, it’s important that we use the same names for the properties. That way, we can easily pass these directly to the components that we’re using. The component names don’t need to match exactly, as these are for our reference, so rather than x-base, we’re just using base.

Adding the code for the base

Here, we are using the standard components, x-base and x-section however, you could code this with HTML.

<x-base #set="brand.base" #root>
  <x-section #set="brand.section"> 
    <slot/>
  </x-section>
</x-base>

Using #set, we can set multiple properties for the component at once, and here, we are using the styles we defined before.

<x-base lang='en' dir='ltr' title='Email from Customer.io' font-family="Instrument Sans, helvetica, arial, sans-serif" color="#151515" background-color="#FBF9F8">
  <x-section width="600px" padding="0">
    <slot/>
  </x-section>
</x-base>

For this particular setup, we’re including both the <x-base> and <x-section> in our <cio-base> component. This means we can’t add sections from the drag-and-drop editor (a section can’t contain another section). If you want that as an option, you can leave <x-section> out.

You could also make this more restrictive but also more controlled by adding the header and footer into your base component. If they are always the same in every email and you never want to remove or replace them, this could be a good option.

Video walkthrough


Creating styled-components

This can be applied to a number of components that you use. In this example, we’ll look at a box component, used for grouping elements together, similar to how you may use a <div> in web code or maybe a <table> in email code. For this design system we have 2 types of box, normal and highlight

<script>
  export const config = { 
    label: "CIO Box",
    presets: [
      {
        icon: 'select',
        section: 'Layout',
        label: 'Box',
        content: '<cio-box></cio-box>',
      },
      {
        icon: 'select',
        section: 'Layout',
        label: 'Highlight Box',
        content: '<cio-box variant="highlight"></cio-box>',
      },
    ],
  };
  export const slots = Component.defineSlots({
    default: Component.slots.any(),
  });
  export const props = Component.defineProps({
    variant: {
      core: true,
      schema: Component.props.enum(['normal', 'highlight']).default('normal'),
      options: [
        { label: 'Normal', value: 'normal' },
        { label: 'Highlight', value: 'highlight' },
      ],
    },
    style:{
      schema: Component.props.string().optional(),
      type: 'hidden',
    }
  });
  const brand = Component.context.get('brand');
</script>
<template>
  <x-box #set="props.variant == 'highlight' ? brand.box.highlight : brand.box.normal"  #set:style="props.style">
    <slot/>
  </x-box>
</template>

Setting presets on a component

Presets are what gets shown in the components menu in the visual editor. You’ll notice nothing was set in the base component, as that is the starting point of an email and should never be dragged in.

Here, we’ve set 2 presets to make it quicker and more visible to access the 2 variations of this box component that we are creating. However, if you have a large number of components, this may overcrowd the menu.

Setting up variants

Inside the props, we are adding a variant option. This is set to enum so in the visual editor, the user will have a drop-down menu to choose between the 2 options normal or highlight. We’ll add these styles in a moment.

Setting a hidden style property

Hidden properties are not shown in the visual editor but can be accessed from the code editor. This can be good for anything that requires a little coding knowledge to use. Here, we allow a user to set additional inline styles, so this could go very wrong if used by anyone not familiar with the specifics of CSS in email clients.

Other options

Rather than a variant that we’ve used here, you may prefer more flexibility with a range of properties. For example;

  • A background property with a predefined list of 4 color options set with enum
  • A border property that is either on or off set with a boolean
  • A font-family property that can be anything you like and is passed straight through to the x-box in the same way we’ve set up the style property in this example.

Adding the code for the box

Near the end of our base component, we are setting a context with the name 'brand' to the value of the brand style object we created.

Component.context.set('brand', brand);

This means this style object can now be pulled in to any of the components nested inside the base.

Inside our box component, we’re then pulling that context in so we can access it from this component.

const brand = Component.context.get('brand');

We then use the same method to #set the styles on the standard <x-box> component; however, here, we’re also querying the value set in the variant to pull in the different styles.

<template>
  <x-box #set="props.variant == 'highlight' ? brand.box.highlight : brand.box.normal"  #set:style="props.style">
    <slot/>
  </x-box>
</template>

Repeat

You can repeat this process for any other components as you may need. But before that, be sure to look at non-editable components and presets to help decide what might be the best option for each case.

Video walkthrough


Non editable components

Sometimes, you may want to create a component that isn’t editable. For example, the header or footer are unlikely to change from one email to the next, so they probably don’t need any editing options added.

To make a component non-editable, we simply don’t add those features when we build it—no properties and no slots.

Below is an example of our footer component

<script>
    export const config = { 
      label: "CIO footer",
      presets: [
        {
          label: "Footer",
          icon: "toast",
          allowedParents: ['cio-base'],
          content: "<cio-footer/>"
        }
      ]
    }
  const brand = Component.context.get('brand');
</script>
<template>
  <x-box #set="brand.box.normal" :font-size="14">
    <x-image src="/CIO components/assets/footer-logo.png" alt="Customer.io" href="https://customer.io" width="48px" margin="20px 0"/>
    <a href="https://www.linkedin.com/company/customer-io/" style="color:#138563"><img src="/CIO components/assets/cio-in.png" alt="LinkedIn" width="32"></a> &nbsp;
    <a href="https://twitter.com/customerio"><img src="/CIO components/assets/cio-x.png" alt="X Twitter" width="32" style="color:#138563"></a> &nbsp;
    <a href="https://www.instagram.com/customer.io/"><img src="/CIO components/assets/cio-insta.png" alt="Instagram" width="32" style="color:#138563"></a> &nbsp;
    <a href="https://www.youtube.com/@customerio"><img src="/CIO components/assets/cio-youtube.png" alt="YouTube" width="32" style="color:#138563"></a>
    <x-paragraph margin="1em 0 0">
      Don't want to recive updates on content and events? <a href="https://customer.io" style="color:#138563">Update your email preferences</a>.
      <br>
      Or you can <a href="https://customer.io" style="color:#138563">unsubscribe from everything here</a>.
    </x-paragraph>
    <x-paragraph margin="0"><a href="https://customer.io" style="color:#138563">View in your browser</a></x-paragraph>
    <x-paragraph>
    © 2025 Peaberry Software, Inc., 9450 SW Gemini Dr., Suite 43920, Beaverton, Oregon 97008
    </x-paragraph>
  </x-box>
</template>

Video walkthrough


Presets

Presets are pre-written bits of code that we can quickly insert into our emails from the visual editor. We’ve already seen how they can be used to insert custom components, but they don’t need to be attached to any component code.

These presets can include a range of things, including customer components, standard components, HTML, and text. They can be small and simple or large and complex layouts.

When we nest a component inside the <template> part of a component we no longer have access to the properties of the components via the visual editor, however if a component is inserted from a preset, then we will have full access to it.

Below is an example of some presets, to keep things tidy it can be a good idea to put all the presets that are not directly associated with a component into one file.

export const config = { 
    label: "presets",
    presets: [
      {
        label: "Hero",
        icon: "landscape",
        content: '<x-image src="https://fakeimg.pl/1200x560/0B353B/E4FFCE?text=Hero+Image&font=lobster" alt="" margin="0"/>'
      },
      {
        label: "Hello world",
        icon: "waving_hand",
        content:'<h1>Hello world</h1>'
      },
      {
        label: "Photo testimonial",
        icon: "id_card",
        content: '<cio-box variant="highlight" style="display:table;border-spacing:20px"> <temp-column width="30%"><x-image src="/CIO components/assets/placeholder-testimonial.png" width="200px"/> </temp-column><temp-column width="70%"><cio-heading element="x-heading-2" margin="0 0 1em" color="#0B353B">A catchy phrase about this use case to grab people\'s attention</cio-heading><x-paragraph>Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptatibus recusandae ad veritatis modi autem maxime! Id eius fuga libero dignissimos labore aliquid dicta sint inventore? Expedita ad laboriosam facilis ipsam.</x-paragraph><cio-cta variant="secondary">Secondary CTA</cio-cta></temp-column></cio-box>'
      },
    ]
  }

Video walkthrough


Setting up multiple brands in one EDS

There are a few ways to go about managing multiple brands. If the brands are very separate in there designs and layouts it may make sense to create a new workspace for each brand. However a lot of the time, brands, sub-brands and sibling brands follow a very similar brand architecture. If that’s the case then you can include multiple brands in one email design system.

Firstly we can create a property on the base component to select the brand:

  export const props = Component.defineProps({
    'brand': {
      core: true,
      label: 'Brand',
      schema: Component.props.enum(['cio', 'parcel']).default('cio'),
    }
  });

Here we’ve created 2 options cio and parcel. And set the default to be cio.

We then need to create the styles for Parcel in the same way we have for CIO.

const parcelColors = {
    black: '#0D1117',
    white: '#FCFDFF',
    blue: '#198CFF',
    darkblue: '#0c1927'
  };
  const parcelStyles = {
    name: 'parcel',
    base:{
      title: 'Email from Parcel.io', 
      'font-family':"Inter, sans-serif", 
      color:parcelColors.white, 
      'background-color':parcelColors.black
    },
    section:{
      'outer-background-color': parcelColors.blue,
      'background-color': parcelColors.black,
    },
    box:{
      highlight:{
        color: '',
        'background-color': parcelColors.darkblue,
        'border-color': parcelColors.white,
      }
    },
    heading:{
      color: parcelColors.white,
    },
    cta:{
      primary:{
        color: parcelColors.white,
        'background-color': parcelColors.blue,
        'border-color': parcelColors.blue,
        'hover-background-color': parcelColors.white,
        'hover-color': parcelColors.blue
      },
      secondary:{
        'background-color': '',
        'border-style': 'solid', 
        'border-width': '2px',
        'color': parcelColors.white,
        'border-color': parcelColors.white,
        'hover-background-color': '',
        'hover-color': parcelColors.blue
      },
      tertiary:{
        'text-decoration': 'underline',
        'border-radius': '30px',
        padding: '',
        'background-color': '', 
        color: parcelColors.white,
        'hover-color': parcelColors.blue
      }
    }
  };

We could set all the styles here, but in this case we want to inherit things from the CIO styles, so we’re only adding styles that change. Either options is valid, it just depends on what’s best for you.

We then need to blend the styles together to using spread operators.

let brand = cio;
  if (props.brand == 'parcel'){
    brand = {
      ...cio,
      ...parcelStyles,
      base: {
        ...cio.base,
        ...parcelStyles.base,
      },
      section:{
        ...cio.section,
        ...parcelStyles.section,
      },
      box:{
        normal:{
          ...cio.box.normal,
          ...parcelStyles.box.normal,
        },
        highlight:{
          ...cio.box.highlight,
          ...parcelStyles.box.highlight,
        }
      },
      heading:{
        ...cio.heading,
        ...parcelStyles.heading,
      },
      cta:{
        primary:{
          ...cio.cta.primary,
          ...parcelStyles.cta.primary,
        },
        secondary:{
          ...cio.cta.secondary,
          ...parcelStyles.cta.secondary,
        },
        tertiary:{
          ...cio.cta.tertiary,
          ...parcelStyles.cta.tertiary,
        }
      }
    }
  };


Spread operators only work on a single level, so we need to set it for every nested object in our style object.

Applying these styles to nested components

Most of the work should already be done here. When ours styles are merged together in the base component, that will update those styles on each component. However some times we may need to apply bigger changes. For example our footer designs are quite different so rather than restyling, we need to replace the content.

  <x-box #if="brand.name == 'cio'" #set="brand.box.normal" :font-size="14">
    <x-image src="/CIO components/assets/footer-logo.png" alt="Customer.io" href="https://customer.io" width="48px" margin="20px 0"/>
    <a href="https://www.linkedin.com/company/customer-io/" style="color:#138563"><img src="/CIO components/assets/cio-in.png" alt="LinkedIn" width="32"></a> &nbsp;
    <a href="https://twitter.com/customerio"><img src="/CIO components/assets/cio-x.png" alt="X Twitter" width="32" style="color:#138563"></a> &nbsp;
    <a href="https://www.instagram.com/customer.io/"><img src="/CIO components/assets/cio-insta.png" alt="Instagram" width="32" style="color:#138563"></a> &nbsp;
    <a href="https://www.youtube.com/@customerio"><img src="/CIO components/assets/cio-youtube.png" alt="YouTube" width="32" style="color:#138563"></a>
    <x-paragraph margin="1em 0 0">
      Don't want to recive updates on content and events? <a href="https://customer.io" style="color:#138563">Update your email preferences</a>.
      <br>
      Or you can <a href="https://customer.io" style="color:#138563">unsubscribe from everything here</a>.
    </x-paragraph>
    <x-paragraph margin="0"><a href="https://customer.io" style="color:#138563">View in your browser</a></x-paragraph>
    <x-paragraph>
    © 2025 Peaberry Software, Inc., 9450 SW Gemini Dr., Suite 43920, Beaverton, Oregon 97008
    </x-paragraph>
  </x-box>
  <x-box #else #set="brand.box.normal" font-family="DM mono, monospace">
    <p style="margin:1em 0;  font-size:0.8em; text-align:left">
		I get too much email, <a href="https://parcel.io" style="color: #ffffff; text-decoration: underline;">unsubscribe</a> please<br>
		Or, update your email preferences... <a  href="https://parcel.io" style="color: #ffffff; text-decoration: underline;">pause emails</a> FTW! 
	</p>
	<p style="margin:1em 0; font-size:0.8em; text-align:left" class="footer">
	Peaberry Software, Inc.<br> 9450&zwnj; SW Gemini Dr&zwnj;., Suite&zwnj; 43920&zwnj;, &zwnj;Beaverton&zwnj;, &zwnj;Oregon&zwnj; 97008&zwnj; <br>© 2012-2024 Peaberry</p>
	</p> 
  </x-box>
</template>

Here, we are using #if="brand.name == 'cio'" to check the brand. If this doesn’t match, then the element where we set that won’t be included, and neither will any elements inside it. We are then using #else on the Parcel version of the footer. When the #if returns as false then the #else content will be displayed. This must be a direct sibling of the element containing the #if to work correctly.


Templates

The final steps is to create some templates to help your team start using your design system.

To do this

  • create we a new email
  • add the base component that we made
  • make the email as a template

The end code can be a simple as:

<cio-base></cio-base>

We can then also create some templates based on some common layouts that are being used as starting points.