History & Present
Aesthetic was built around the time that CSS-in-JS solutions were rising in popularity. It was
designed to bridge the gap between current CSS-in-JS libraries and React components, and was not
meant to be yet another CSS-in-JS library. As such, Aesthetic offers the adapter pattern where
CSS-in-JS libraries are plug-and-play, while not handling the CSS itself.
Besides the above mentioned, Aesthetic aimed to solve the following problems that plagued libraries
and applications.
- CSS-in-JS libraries require different syntax for defining styles. This could be problematic when
switching libraries (for performance or other reasons), as the syntax differs, and would require
a potential massive migration (and ejection if a failure). To mitigate this, Aesthetic implements
a "unified syntax", where the same syntax works
for all adapters.
- The other issue that a unified syntax solves is third-party library adoption. If library A styles
their components with Aphrodite, and library B styles theirs with Fela, then we have 2 differing
and conflicting libraries. This would increase bundle size, reduce interoperability, and more. If
both A and B libraries were instead written in Aesthetic, then the underlying adapter (Aphrodite
or Fela) can be swapped out without breaking compatibility or increasing bundle sizes.
Outstanding issues
For the most part, Aesthetic works and serves its purpose pretty well. However, it's not perfect,
and could use a rewrite to solve the following issues.
Themes are too dynamic
There is no set structure for theme objects, and as such, they cannot be typed safely, nor can they
be trusted when interoping with third-party libraries. For example:
// Registered in an application
aesthetic.registerTheme('app-light', {
colors: {
white: '#fff',
},
unit: 8,
});
// Registered in a library
aesthetic.registerTheme('3rd-party-dark', {
color: {
black: ['#000'],
},
spacing: 4,
});
In the above example, an application and a third-party library may register a theme, completely
independent of each other, with differing structures (unit
vs spacing
, etc). When used in
parallel, components will break when themes change, as the following styles would only work under
one theme, not both.
useStyles(theme => ({
padding: theme.unit * 2,
}));
This actually breaks interoperability and the 2nd adoption problem above. This was a massive
oversight on my end when designing the theme layer.
Global styles are complicated to manage
Global styles are primarily applied to html
, body
, and a
, for global inheritance of colors,
spacing, and font sizing. This works flawlessly until one of the following occurs:
- Rendering parallel themes using the React
ThemeProvider
component. When this happens, the global
styles in the next theme to compile will overwrite the previous theme. I'm not sure there's a way
to solve this correctly.
- Dynamically change themes for the entire page, usually through a toggle switch or dropdown. When
this happens, we can easily purge/delete all existing styles in the document. However, in
practice, this is only true if the CSS-in-JS adapter that is currently configured supports it,
which most do not (will require upstream patches).
Component styles are hard to customize
Take the following style sheet for a Button
, where it has a primary background with a 1px border
(dark blue), and a base text color (white). Seems straight forward right?
useStyles(({ color }) => ({
button: {
border: `1px solid ${color.primary[4]}`,
backgroundColor: color.primary[3],
color: color.base,
},
}));
Not really. It's actually very restrictive, isolating, and hard to customize. The above works well
for the theme it was initially designed for, in this instance, a "light" theme. But what happens
when we change it to a dark theme?
- First, the indices are reversed, so
0
is darkest and 10
is lightest. Depending on the
component, this will look great, or it will look terrible, and there's no way to change it based
on theme, since the CSS/syntax is hard-coded in the component.
- The
base
color may change from white to black in the dark theme, so now we run into
accessibility concerns. Is black text viable on a colored button? Usually not. This is similar to
the issue previously mentioned, where we can't change the color
property on a theme-by-theme
basis.
- What if the dark theme wants 2px borders? Or no borders? Or rounded corners? Again, we have no
way of handling that.
- So on and so forth.
Future
I would love to resolve all the issues mentioned previously, most of which are easily solved with a
type-safe and structure-safe theme layer. However, while brainstorming the possibilities, I thought
to myself, "Why not take a step back and expand the scope of the project?". What does this mean
exactly? Well, in the current state of the web development world, companies are pushing hard and
forward with design systems, such as: Airbnb Lunar/DLS, Google Material, GitHub Primer, SalesForce
Lightning, Mozilla Photon, Shopify Polaris, IBM Carbon, so on and so forth.
Every company is building their design system from scratch, with different technologies, duplicated
across many platforms. What if there was a technological solution to this problem? This is where
Aesthetic comes in. What if Aesthetic was re-purposed to be a "design system framework", where a
design system's fundamentals are configured (fonts, spacing, borders, shadows, breakpoints,
interactions, etc), are compiled to multiple target platforms or technologies (CSS, Sass, Less,
JS/TS, iOS, Android, React Native, etc), and is ultimately robust and easy enough to be used by any
company.
How it works
I've been researching existing design systems for commonalities
(Google doc),
as a means to build a foundation for this framework. After a bit of research, and minor technical
prototyping, I believe the initial step forward would be to use YAML for configuring a design
system, while adhering to the following fundamentals.
- Modular scale will be used for all
scaling based algorithms. Can be configured per setting, with name or integer based values.
- Colors may be unique per design system (and maybe theme too), but is inaccessible to consumers.
Consumers will need to use palettes, which are pre-defined colors + states for common UI elements.
- Multiple design systems can be used in parallel (e.g., version 2019 vs version 2020). This is
possible since the "theme template" for the consumer will be identical regardless of design system
and theme parameters.
An example of that YAML file is as follows, with descriptive comments.
# Whether the design system focuses on mobile or desktop first.
# This settings will control various features, like breakpoints.
# Accepts "mobile-first" or "desktop-first".
strategy: mobile-first
# List of 3-5 breakpoints for responsive and adaptive support. If not provided,
# will default to the following 5 values.
breakpoints:
- 0
- 600
- 960
- 1280
- 1920
# Spacing related settings.
spacing:
# The algorithm used for spacing and page density calculations. Accepts the following:
# vertical-rhythm - Calculates font size + line height for spacing.
# unit - Uses an explicit pixel unit value for spacing.
type: vertical-rhythm
# Explicit spacing unit (in pixels).
unit: 8
# Text and font related settings.
typography:
# Font family for the entire system. If not provided, defaults to the OS font.
fontFamily: 'Roboto'
# Root font size (in pixels).
fontSize: 16
# Factor to increase (mobile-first) or decrease (desktop-first) root font size each breakpoint.
fontScale: 1.1
# Factor to increase font size for each text heading.
headingScale: 1.25
# Root line height.
lineHeight: 1.5
# Border related settings.
border:
# Rounded corner radius (in pixels).
radius: 3
# Factor to increase radius each size.
radiusScale: 1
# Width of the border.
width: 1
# Factor to increase width each size.
widthScale: 1
# Shadow and depth related settings.
shadow:
# Depth Y offset (in pixels).
depth: 2
# Factor to increase depth each size.
depthScale: 1
# Blur radius (in pixels).
blur: 2
# Factor to increase blur each size.
blurScale: 1.25
# Spread radius (in pixels).
spread: 0
# Factor to increase spread each size.
spreadScale: 0
# List of all color names that each theme must implement.
colors:
- red
- blue
- green
- ...
# Mapping of themes and their colors.
themes:
# A light theme with a custom name.
foo:
# Base color scheme for this theme. Accepts "light" or "dark".
# Used in `prefers-color-scheme` browser detection.
scheme: light
# Mapping of all colors in the theme, with a range of 10 hexcodes per color,
# with 400 being the base default color, and the bounds going from light to dark,
# or dark to light if scheme is "dark".
colors:
red:
00: '#FE8484' # Lightest
10: '#FE8484'
20: '#FE8484'
30: '#FE8484'
40: '#FE8484' # Base
50: '#FE8484'
60: '#FE8484'
70: '#FE8484'
80: '#FE8484'
90: '#FE8484' # Darkest
# Mapping of pre-defined palettes (UI states) to colors and their shades (from above).
# Consumers will reference these values, instead of colors directly.
palettes:
# Priority (properties in order of specificity)
# TODO - THESE ONLY APPLY TO BACKGROUNDS... WHAT ABOUT FOREGROUND/TEXT?
primary:
base: purple.40
focused: purple.50
selected: purple.50
hovered: purple.60
disabled: gray.40
failed: red.40
secondary: # ...
tertiary: # ...
neutral: # ...
# States
muted: # ...
danger: # ...
warning: # ...
success: # ...
info: # ...
# Layout
text: # ...
box: # ...
boxBorder: # ...
input: # ...
inputBorder: # ...
shadow: '#000'
# A dark theme with a custom name.
bar:
scheme: dark
colors:
red:
00: '#FE8484' # Darkest
10: '#FE8484'
20: '#FE8484'
30: '#FE8484'
40: '#FE8484' # Base
50: '#FE8484'
60: '#FE8484'
70: '#FE8484'
80: '#FE8484'
90: '#FE8484' # Lightest
Furthermore, the design system will embrace the following:
em
s and rem
s for all font sizing.
- Accessible colors and font sizes (AAA, AA Large, etc).
- Responsive and adaptive aware by default.
prefers-color-scheme
for automatic theme selection.
References