Using ColdFusion Custom Tags To Create An HTML Email DSL In Lucee CFML 5.3.7.47, Part V
As I've been testing my ColdFusion custom tag DSL (Domain Specific Language) for HTML emails in Litmus, I noticed that they offer previews for email clients running in "Dark Mode". Dark mode, like other responsive design elements, can be controlled on the web using @media
queries. Yesterday, I added two @media
abstractions for max-width
and min-width
. As such, I wanted to see if I could do the same thing for dark mode.
View this code in my ColdFusion Custom Tag Emails project on GitHub.
In modern browsers, you can progressively enhance your web application to add special styles for both light and dark theme user preferences using the following @media
CSS bocks:
@media ( prefers-color-scheme: light ) { ... }
@media ( prefers-color-scheme: dark ) { ... }
Now, yesterday, I already added a <core:MediaQueryStyles>
tag to my DSL; so, I think I can add support for dark by with thin layer above this tag, <core:DarkModeStyles>
, that just sets the appropriate media query name/value pair:
<!--- Import custom tag libraries. --->
<cfimport prefix="core" taglib="./" />
<!--- Define custom tag attributes. --->
<cfparam name="attributes.injectImportant" type="boolean" default="true" />
<!--- // ------------------------------------------------------------------------- // --->
<!--- // ------------------------------------------------------------------------- // --->
<cfswitch expression="#thistag.executionMode#">
<cfcase value="end">
<cfoutput>
<cfset theme = getBaseTagData( "cf_email" ).theme />
<cfset theme.enableDarkModeSupport = true />
<core:MediaQueryStyles
name="prefers-color-scheme"
value="dark"
injectImportant="#attributes.injectImportant#">
#thistag.generatedContent#
</core:MediaQueryStyles>
<!--- Reset the generated content since we're overriding the output. --->
<cfset thistag.generatedContent = "" />
</cfoutput>
</cfcase>
</cfswitch>
In addition to wrapping the <core:MediaQueryStyles>
tag, you may also notice that I am setting theme.enableDarkModeSupport
to true. Doing this causes the rendered email to contain two additional snippets of code in the HTML <head>
:
<!--- ... truncated content. ... --->
<cfif theme.enableDarkModeSupport>
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
</cfif>
<!--- ... truncated content. ... --->
<cfif theme.enableDarkModeSupport>
<style type="text/css">
:root {
color-scheme: light dark ;
supported-color-schemes: light dark ;
}
</style>
</cfif>
<!--- ... truncated content. ... --->
I got the meta tag tip from Litmus and I got the :root
tip from CampaignMonitor.
With this new tag in place, I added a new example to my GitHub project:
<!--- Import custom tag libraries. --->
<cfimport prefix="core" taglib="./core/" />
<cfimport prefix="ex6" taglib="./ex6/" />
<cfimport prefix="html" taglib="./core/html/" />
<!--- // ------------------------------------------------------------------------- // --->
<!--- // ------------------------------------------------------------------------- // --->
<core:Email
subject="Playing with media queries"
teaser="Seeing if dark mode works.">
<ex6:Body>
<html:h1>
Trying to formulate ideas around dark mode.
</html:h1>
<html:blockquote>
<html:p>
Our deepest fear is not that we are inadequate. Our deepest fear is
that we are powerful beyond measure. It is our light, not our darkness,
that most frightens us. Your playing small does not serve the world.
There is nothing enlightened about shrinking so that other people won't
feel insecure around you. We are all meant to shine as children do. It's
not just in some of us; it is in everyone. And as we let our own lights
shine, we unconsciously give other people permission to do the same. As
we are liberated from our own fear, our presence automatically liberates
others.
</html:p>
</html:blockquote>
<html:p>
— <html:strong>Marianne Williamson</html:strong>
</html:p>
</ex6:Body>
</core:Email>
As you may notice, there's nothing in this content that points to "dark mode". And, that's kind of the whole point of what I'm trying to accomplish with this ColdFusion custom tag DSL: abstracting away a lot of the cruft. Or, at least, moving it into places where functionality can be more cohesive.
That said, notice that the Body tag is a special tag. Instead of using <core:Body>
, I'm using an example-6-specific tag, <ex6:Body>
. This tag defines the responsive styles for the example that enable the dark mode support for the content.
The <ex6:Body>
tag, like many ColdFusion custom tags, has two modes: Start and End. The Start mode sets up the theme for the child content; and, the End mode sets up the theme for the wrapper content.
<!--- Import custom tag libraries. --->
<cfimport prefix="core" taglib="../core/" />
<cfimport prefix="html" taglib="../core/html/" />
<!--- // ------------------------------------------------------------------------- // --->
<!--- // ------------------------------------------------------------------------- // --->
<cfswitch expression="#thistag.executionMode#">
<cfcase value="start">
<cfoutput>
<cfset theme = getBaseTagData( "cf_email" ).theme />
<cfset theme.importUrls.append( "https://fonts.googleapis.com/css?family=Poppins:100,200,300,500|Roboto:100,200,300,400,500,600,700" ) />
<!--- Update the base fonts for the custom font-family import. --->
<core:HtmlEntityTheme entity="h1, h2, h3, h4, h5, th">
font-family: Poppins, BlinkMacSystemFont, helvetica, arial, sans-serif ;
font-weight: 500 ; <!--- Highest that Poppins goes. --->
</core:HtmlEntityTheme>
<core:HtmlEntityTheme entity="blockquote, img, li, p, td">
font-family: Roboto, BlinkMacSystemFont, helvetica, arial, sans-serif ;
</core:HtmlEntityTheme>
<!---
Since MSO / Outlook clients won't load remote fonts, we have to define a
solid fallback font family and weight.
--->
<core:HeaderContent>
<core:IfMso>
<style type="text/css">
h1, h2, h3, h4, h5, th {
font-family: helvetica, arial, sans-serif !important ;
font-weight: 800 !important ;
}
blockquote, body, img, li, p, td {
font-family: helvetica, arial, sans-serif !important ;
}
</style>
</core:IfMso>
</core:HeaderContent>
<!--- Setup the dark mode color overrides. --->
<core:DarkModeStyles>
.html-entity-blockquote,
.html-entity-h1,
.html-entity-h2,
.html-entity-h3,
.html-entity-h4,
.html-entity-h5,
.html-entity-img,
.html-entity-li,
.html-entity-p,
.html-entity-td,
.html-entity-th {
color: cyan ;
}
.html-entity-blockquote {
border-left-color: ##666666 ;
}
.html-entity-a {
color: #theme.dark.primary# ;
}
</core:DarkModeStyles>
</cfoutput>
</cfcase>
<cfcase value="end">
<cfoutput>
<core:HtmlEntityTheme entity="table" class="e6-body">
border: 1px solid ##ebecee ;
border-width: 0px 1px 1px 1px ;
</core:HtmlEntityTheme>
<core:HtmlEntityTheme entity="td" class="e6-body-top-border">
background-color: ##ff3366 ;
font-size: 1px ;
height: 2px ;
line-height: 2px ;
</core:HtmlEntityTheme>
<core:HtmlEntityTheme entity="td" class="e6-body-content">
padding: 45px 0px 30px 0px ;
</core:HtmlEntityTheme>
<core:HtmlEntityTheme entity="td" class="e6-body-footer">
background-color: ##f8f8fa ;
border-top: 1px solid ##ebecee ;
color: ##6c7689 ;
font-size: 12px ;
font-weight: 400 ;
line-height: 17px ;
padding: 20px 0px 20px 0px ;
</core:HtmlEntityTheme>
<core:HtmlEntityTheme entity="a" class="e6-body-footer-link">
color: ##276ee5 ;
</core:HtmlEntityTheme>
<!--- Setup responsive styles. --->
<core:MaxWidthStyles>
.e6-body {
border-width: 0px 0px 0px 0px ;
width: 100% ;
}
.e6-body-gutter {
width: 20px ;
}
.e6-body-content {
padding: 20px 0px 20px 0px ;
}
</core:MaxWidthStyles>
<!--- Setup the dark mode color overrides. --->
<core:DarkModeStyles>
.e6-body {
border-color: ##666666 ;
}
.e6-body-footer {
background-color: ##333333 ;
border-top-color: ##666666 ;
color: ##cccccc ;
}
.e6-body-footer-link {
color: ##ffffff ;
}
</core:DarkModeStyles>
<html:table width="#theme.width#" class="e6-body">
<html:tr>
<html:td colspan="3" class="e6-body-top-border">
<br />
</html:td>
</html:tr>
<html:tr>
<html:td width="60" class="e6-body-gutter">
<!--- Left margin. ---><br />
</html:td>
<html:td class="e6-body-content">
#thistag.generatedContent#
</html:td>
<html:td width="60" class="e6-body-gutter">
<!--- Right margin. ---><br />
</html:td>
</html:tr>
<html:tr>
<html:td colspan="3" align="center" class="e6-body-footer">
Questions?
<html:a href="https://www.bennadel.com" decoration="false" class="e6-body-footer-link">I'm here to help.</html:a>
</html:td>
</html:tr>
</html:table>
<!--- Reset the generated content since we're overriding the output. --->
<cfset thistag.generatedContent = "" />
</cfoutput>
</cfcase>
</cfswitch>
As you can see, in the Start mode, I'm using a <core:DarkModeStyles>
tag to change the font color to cyan
.
ASIDE: Notice that the
<core:DarkModeStyles>
tag - like the<core:MaxWidthStyles>
tag - doesn't contain any@media
syntax or any!important
flags. Both of those concepts are being handled internally to the ColdFusion custom tag(s).
Now, if we run this ColdFusion example locally and switch the computer into Dark mode, we get the following output:
Sweet action!
Of course, this is running the latest version of Chrome, which supports all the new hawtness. Email clients are not quite as powerful, or consistent. That said, there is growing support for dark mode among the most popular email clients. And, if we run this HTML email through Litmus (which offers dark mode previews for about a dozen different email clients), we get the following results:
Apple Mail 12 in Dark Mode
Gmail on iOS 13 in Dark Mode
iPhone 11 Pro in Dark Mode
Outlook 365 on MacOS in Dark Mode
Outlook 365 on Windows in Dark Mode
As you can see, they are all "dark", but only some of them have the cyan text color. That's because support for dark mode still varies quite widely among the popular email clients. Of course, it will only get better with time.
I'm still feeling out what I want my ColdFusion custom tag DSL for HTML emails to look like. All the "theming" stuff still feels a little shaky to me. But, it's starting to take shape; and, the small abstractions that I'm adding are definitely going to remove a lot of the laborious details. I think in my next post, I'll try to recreate a "real world" email from InVision.
Want to use code from this post? Check out the license.
Reader Comments