moss
magical organic stylesheets 🌿
CSS framework
npm i -D @ryanatkn/moss
introduction #
Moss is a CSS framework and design system that's built around style variables, which are CSS custom properties and design tokens with additional capabilities and conventions.
Moss can be used with any website and JS framework. It exports one main stylesheet and a theme stylesheet, and it also exports the underlying CSS data, types, and helpers for more complex usage.
About Moss:
- plain CSS
- zero dependencies
- exports one main stylesheet that doubles as a reset with basic HTML tag styles
- exports a basic theme that can be replaced with your own
- also exports the underlying CSS data, helpers, and types, which can be used in many ways, including outputting an optimized utilities class file
- uses its own concept of style variables, a specialization of CSS custom properties and
design tokens
- variables are the main source of truth
- each variable provides values to light and/or dark mode
- includes optional utility and component classes that use the variables
- themes are groups of variables
- dark mode is a first-class concept in the system, not a theme, instead each theme can support light and/or dark color-schemes
- is agnostic to JS frameworks, for example usage see
Themed
in my Svelte UI library Fuz
The stylesheets:
- 🗎
@ryanatkn/moss/style.css
- the main stylesheet and CSS reset - 🗎
@ryanatkn/moss/theme.css
- or bring your own - 🗎
$routes/moss.css
- a reference implementation using Moss' helpers that includes only the utility classes your code uses, generated by Gro'sgro_plugin_moss
(you may want to implement this in your own build system if you don't use Gro) - There are no tools yet for optimizing away unused variables, so
style.css
andtheme.css
have some bloat.
Moss is being made to support Zzz and my other projects that focus on end-users, so it'll grow relatively slowly as I encounter more usecases. It's hobby-ready but expect a lot of breaking changes. Feel free to take the code and ideas for your own purposes.
In the docs, you'll see I'm writing asides using "⚠️" with open questions and other discussion of uncertainties. Your input is appreciated in the GitHub issues, or other social media, on these and other topics, see contributing.md for more 🌿
themes #
Moss supports both the browser's color-scheme and custom themes based on variables, which use CSS custom properties .
Moss works with any JS framework, but it provides only stylesheets, not integrations. This website uses my Svelte UI library Fuz to provide the UI below to control the Moss color scheme and themes.
Color scheme #
Moss supports color-scheme with dark and light modes. To apply dark mode manually,
add the dark
class to the root html
element.
The Fuz integration detects the default with prefers-color-scheme , and users can also set it directly with a component like this one:
The builtin themes support both dark and light color schemes. Custom themes may support one or both color schemes.
Builtin themes #
A theme is a simple JSON collection of variables that can be transformed into CSS that set custom properties. Each variable can have values for light and/or dark color schemes. In other words, "dark" isn't a theme, it's a mode that any theme can implement.
These docs are a work in progress, for now see 🗎 @ryanatkn/moss/theme.ts
and 🗎 @ryanatkn/moss/themes.ts
.
variables #
Style variables, or just "variables" in Moss, are CSS custom properties that can be grouped into a theme. Each variable can have values for light and/or dark color-schemes . They're design tokens with an API.
The goal of the variables system is to provide runtime theming that's efficient and ergnomic for both developers and end-users. Variables can be composed in multiple ways:
- by CSS classes, both utility and component
- by other variables, both in calculations and to add useful semantics (e.g.
button_fill_hover
defaults tofg_2
but can be themed independently) - in JS like the Svelte components in Fuz
Variables also provide an interface that's generally secure for user-generated content, if you're into that kind of thing.
The result is a flexible system that aligns with modern CSS to deliver high-capability UX and DX with minimal overhead.
export interface Theme {
name: string;
variables: Style_Variable[];
}
export interface Style_Variable {
name: string;
light?: string;
dark?: string;
summary?: string;
}
All 323 style variables #
classes #
Optional CSS classes #
Moss has three CSS files:
<!-- +layout.svelte -->
<script>
import '@ryanatkn/moss/style.css';
import '@ryanatkn/moss/theme.css'; // or bring your own
import '$routes/moss.css'; // generated by `gro_plugin_moss`
// ...
</script>
Utility classes #
- .relative|absolute|fixed|sticky|static
- .overflow_auto|hidden|scroll|clip|visible
- .overflow_x|y_auto|hidden|scroll|clip|visible
- .overflow_wrap_anywhere|break_word
- .display_none|contents
- .inline|inline_block|inline_flex|inline_grid
- .block
- .flex
- .flex_1
- .flex_wrap|wrap_reverse|nowrap
- .flex_row|column|row_reverse|column_reverse
- .grow|shrink
- .grow|shrink_0
- .align_items_center|start|end|baseline|stretch
- .align_content_center|start|end|baseline|space_between|space_around|space_evenly|stretch
- .align_self_center|start|end|baseline|stretch
- .justify_content_center|start|end|left|right|space_between|space_around|space_evenly|stretch
- .justify_items_center|start|end|left|right|baseline|stretch
- .justify_self_center|start|end|left|right|baseline|stretch
- .grid
- .float_none|left|right|inline_start|inline_end
- .flip_x|y|xy
- .font_sans|mono
- .line_height_xs-xl
- .size_xs-xl9
- .icon_size_xs-xl3
- .text_align_start|end|left|right|center|justify|justify_all|match_parent
- .vertical_align_baseline|sub|super|text_top|text_bottom|middle|top|bottom
- .white_space_normal|nowrap|pre|pre_wrap|pre_line|break_spaces
- .white_space_collapse_collapse|preserve|preserve_breaks|preserve_spaces|break_spaces
- .text_wrap_wrap|nowrap|balance|pretty|stable
- .ellipsis
- .font_weight_100-900
- .text_color_0-10
- .darken|lighten_1-9
- .bg|fg
- .bg|fg_1-9
- .color_darken|lighten_1-9
- .color_bg|fg
- .color_bg|fg_1-9
- .hue_a-i
- .color_a-i_1 -9
- .bg_a-i_1-9
- .border_color_1-5
- .border_color_a-i
- .border_color_transparent
- .border_width_0-6
- .outline_width_0-3
- .border_none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset
- .radius_0|50|100
- .radius_xs3-xl
- .fade_0|100
- .fade_1-6
- .shadow_xs-xl
- .shadow_top|bottom_xs-xl
- .shadow_inset_xs-xl
- .shadow_inset_top|bottom_xs-xl
- .shadow_color_highlight|glow|shroud
- .shadow_color_a-i
- .shadow_alpha_1-5
- .shadow_inherit|none
- .w|h_0|100|auto|max_content|min_content|fit_content|stretch
- .w|h_xs-xl15
- .t|b|l|r_0|100|auto
- .t|b|l|r_xs-xl15
- .p|pt|pr|pb|pl|px|py_0
- .p|pt|pr|pb|pl|px|py_xs5-xl15
- .m|mt|mr|mb|ml|mx|my_auto
- .m|mt|mr|mb|ml|mx|my_0
- .m|mt|mr|mb|ml|mx|my_xs5-xl15
- .gap_xs5-xl15
- .column|row_gap_xs5-xl15
- .pixelated
- .box
- .column
- .row
- .formatted
- .width_xs-md
- .selected
- .selectable
- .clickable
- .pane
- .panel
- .icon_button
- .plain
- .menu_item
- .chevron
- .chip
colors #
Color semantics #
Moss provides a palette of color and hue variables designed to support concise authoring in light and dark modes, as well as straightforward theming by both developers and end-users at runtime. The colors have more semantics than just plain values, so they automatically adapt to dark mode and custom themes, at the cost of having different values depending on color scheme and theme.
Adapting colors to dark mode
A color's subjective appearance depends on the context in which it's viewed, especially the surrounding colors and values. Moss' semantic colors are designed to work across color schemes, so each Moss color variable has two values, one for light and one for dark mode. The exceptions are the lightest (1) and darkest (9) variants, although this may change if it yields better results.
Custom themes
Instead of "blue" and "red", colors are named with letters like "a" and "b", so you can change the primary "a" from blue to any color in a theme without breaking the name-to-color correspondence everywhere. This also flexibly handles more colors and cases than using names like "primary", and although it takes some learning, it's a simple pattern to remember. ("primary" and its ilk require learning too!)
A downside of this approach is that changing a color like the primary "a" affects the many places it's used. Sometimes you may want to change the color of a specific element or state, not all the things. In those cases, use plain CSS and optionally Moss variables. Compared to most libraries, Moss provides fewer handles for granular color customizations, but the benefits include consistency, efficiency, DRY authoring, and ease of app-wide theming.
Caveats #
For performance reasons, Moss does not currently have an extensive set of variants, like
specialized states for elements or color values like "blue". Each of the 7 hues has 9 HSL
color values (e.g. hsl(120 55% 36%)
) and 9 HSL values (e.g. 120 55% 36%
, useful to apply custom alpha), handling most cases, and the base
colors can be customized with platform APIs like the color-mix CSS function.
Variants will be expanded when Moss includes a Vite plugin or other build tooling for optimization. A downside of removing unused styles is that they won't be available to your end-users at runtime. We'll probably end up with an interpreted language like Tailwind's just-in-time compiler.
Hue variables #
Hue variables contain a single hue number. Each color variable combines a hue variable with hardcoded saturation and lightness values for light and dark modes.
Hue variables therefore provide a single source of truth that's easy to theme, but to achieve pleasing results, setting the hue alone is not always sufficient. Custom colors will often require you to set per-variable saturation and lightness values.
Hue variables are also useful to construct custom colors not covered by the color variables.
For example, Moss' base stylesheet uses hue_a
for the semi-transparent ::selection
. (try selecting some text - same hue!)
Unlike the color variables, the hue variables are the same in both light and dark modes.
- NaNprimary
- NaNsuccess/help
- NaNerror/danger
- NaNsecondary
- NaNtertiary
- NaNquaternary
- NaNquinary
- NaNsenary
- NaNseptenary
Color variables #
There are 9 variables per color, numbered 1 to 9, lightest to darkest. The 5th variable of each color is used as the base for things like buttons.
Note that these values differ between light and dark modes! See the discussion above for why.
These colors were eyeballed by a programmer, and will change :]
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
buttons #
Buttons have a .selected
state that can be used for various UI purposes, like
showing a selected item in a menu or a styling button's aria-pressed
state.
Instead of having two distinct styles of buttons with outlined and filled variants, Moss makes
filled buttons the default, and selected buttons are outlined. There's also the .deselectable
modifier class for buttons that can be clicked when selected. Themes
can customize this behavior.
<button>a button</button>
Colorful buttons #
<button class="color_a">
<button class="color_b">
<button class="color_c">
<button class="color_d">
<button class="color_e">
<button class="color_f">
<button class="color_g">
<button class="color_h">
<button class="color_i">
With disabled attribute #
<button disabled>
:|
</button>
With .selected
#
.selected
<button class="selected">...</button>
.selected
buttons with .deselectable
continue to be clickable when
selected:
<button class="selected deselectable">
...
</button>
With .plain
and .icon_button
#
.plain
and .icon_button
<button class="plain">
+
</button>
<button class="icon_button">
+
</button>
<button class="plain icon_button">
+
</button>
.selected
variants
<button class="plain selected">
+
</button>
<button class="icon_button selected">
+
</button>
<button class="plain icon_button selected">
+
</button>
.selected
and .deselectable
variants
<button class="plain selected deselectable">
+
</button>
<button class="icon_button selected deselectable">
+
</button>
<button class="plain icon_button selected deselectable">
+
</button>
elements #
Styles for plain HTML elements . See also typography and forms.
#
👇 hr #
#
a link in a p
div
a link with .selected
#
code
in a p
code
in a div
#
a pre
is
preformatted
text
code
in a pre
is a
block
#
Click this summary
to see the rest of the details
The children of the details
excluding the summary
.
<details>
<summary>
Click this <code>summary</code>
to see the rest of the <code>details</code>
</summary>
<p>The children of the <code>details</code> excluding the <code>summary</code>.</p>
<Code code={'...'} />
</details>
#
#
<header>header</header>
#
<footer>footer</footer>
#
<section>section</section>
ul
#
ul
- a
- b
- see
ul
with .unstyled
- a
- b
- see
ol
#
ol
- one
- two
- etc
ol
with .unstyled
- one
- two
- etc
menu
#
menu
menu
with .unstyled
#
<table>
<thead>
<tr>
<th>th</th>
<th>th</th>
<th>th</th>
</tr>
</thead>
<tbody>
<tr><td>td</td><td>td</td><td>td</td></tr>
<tr><td>td</td><td>td</td><td>td</td></tr>
<tr><td>td</td><td>td</td><td>td</td></tr>
</tbody>
</table>
th | th | th |
---|---|---|
td | td | td |
td | td | td |
td | td | td |
<table class="w_100">
...
</table>
th | th | th |
---|---|---|
td | td | td |
td | td | td |
td | td | td |
TODO more!
forms #
#
<form>
<fieldset>
<legend>
a <Mdn_Link path="Web/HTML/Element/legend" />
</legend>
<label>
<div class="title">
username
</div>
<input
bind:value={username}
placeholder=">"
/>
</label>
...
</fieldset>
...
</form>
form
with range input #
form
with range inputform
with checkboxes #
form
with checkboxes<label class="row">
with .disabled
as needed: <label class="row disabled">
form
with radio inputs #
form
with radio inputstypography #
h1
h2
h3
h4
h5
h6
paragraphs
paragraphs
paragraphs
p with some small text
p sub p sup p
show code
Font sizes #
Font weights #
Text colors #
Line heights #
Icon sizes #
--size_
variables, --icon_
variables are in px
not rem
, so they're insensitive to browser font sizeborders #
Border colors #
Colorful border variants #
Border widths #
Outline widths #
Border radius #
shading #
Moss is designed around two simplistic models of light, one for dark mode and one for light mode. The goal is easy authoring with simple and consistent rules for arbitrary compositions and states. Each theme can choose to implement either light mode or dark mode or both.
Light mode's starting point is plain white documents (like paper) where we can think of UI construction as assembling elements that contrast against the white background, replacing some of the white blankness with darkened values/color/shape. In other words, we start with full lightness and subtract light to make visuals. Black shadows on the white background make natural sense, and white glows against a white background are invisible.
In contrast, dark mode's starting point is a lightless void where we add light. We add elements which emanate light. I think of videogames and virtual/augmented/actual reality. Black shadows are invisible against a black background, and white glows make natural sense against a black background.
This distinction leads to complication. For example, applying a black shadow to an element has a particular visual impact on the typical light mode page, but viewed in dark mode, it would disappear completely against a black background.
Moss provides APIs that simplify or hide this complexity. For example, the lighten
and darken
variables are the same in light and dark modes, but fg
and bg
are equivalent values that swap places in dark mode. Thus bg
and fg
are called color-scheme-aware, and lighten
and darken
are color-scheme-agnostic. (maybe you can think of better terminology? I
like the word "adaptive" but idk) The colors docs elaborate more on this point.
Opacity is used to enable arbitrary stacking that visually inherits its context. Not all cases are properly handled yet, and some choices are made for performance reasons, like avoiding opacity on text. (assuming this is still a thing?)
Shades and highlights #
darken
and lighten
#
darken
and lighten
bg
and fg
#
bg
and fg
In light mode, bg
is the same as lighten
and fg
is
the same as darken
. In dark mode, they're swapped.
tip: Try between light
and dark to see how bg
and fg
change, while darken
and lighten
don't change but do appear significantly
different because of the context.
Stacking opacity #
<div class="fg_1 p_sm">
<div class="fg_1 p_sm">
<div class="fg_1 p_sm">
<div class="fg_1 p_sm">
<div class="bg p_sm">
...
</div>
</div>
</div>
</div>
Fading opacity #
shadows #
Shadow #
Shadows darken in light mode and lighten in dark mode.
shadow_alpha_
Highlight #
Hightlights lighten in light mode and darken in dark mode.
shadow_alpha_
Glow #
Glows lighten in both light and dark mode.
shadow_alpha_
Shroud #
Shrouds darken in both light and dark mode.
shadow_alpha_
Colorful shadows #
These are darker in light mode than in dark mode.
shadow_alpha_