moss
magical organic stylesheets 🌿
CSS framework and design system
npm i -D @ryanatkn/moss
introduction #
Moss is a CSS framework and design system that's built around style variables, which are design tokens and CSS custom properties with particular capabilities and conventions.
Moss can be used with any website and JS framework. It exports 🗎 one main stylesheet and a replaceable 🗎 theme stylesheet, and it also exports the underlying CSS data, types, and helpers for more complex usage.
Details:
- 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
Themedin 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
- 🗎 src/routes/moss.css - a generated reference implementation using Moss's helpers that includes only the utility classes your code uses, generated by Gro with 🗎 src/routes/moss.gen.css.ts using the Moss helpers in 🗎 @ryanatkn/moss/gen_moss_css.js. I can add a Vite plugin if there's demand.
- There are not yet tools for optimizing away unused variables, so
style.cssandtheme.csshave 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 and discussions, or find me on Bluesky. 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_hoverdefaults tofg_2but 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 low overhead.
export interface Theme {
name: string;
variables: Style_Variable[];
}
export interface Style_Variable {
name: string;
light?: string;
dark?: string;
summary?: string;
}All 335 style variables #
classes #
Optional CSS classes #
Moss has three CSS files, two of which are required:
<!-- +layout.svelte -->
<script>
import '@ryanatkn/moss/style.css'; // required
import '@ryanatkn/moss/theme.css'; // required, can bring your own
import '$routes/moss.css'; // optional, generated by `gen_moss_css`
// ...
</script> The moss.css file is created on demand with the utility classes that your code
uses, if any. For now it requires Gro to
generate it, but it isn't hard to make your own integration using the helpers 🗎 gen_moss_css.ts. I can add a Vite plugin if
there's demand.
Utility classes #
Moss supports utility classes that cost nothing until you opt-in. The main stylesheet and theme are required and build around CSS custom properties - utility classes offer an orthogonal API that some developers prefer some of the time. They leverage Moss's base frameworky parts, and are well-integrated with the other APIs and tools.
Moss exports 🗎 helpers to generate styles
on demand based on class usage in your source files, so you can ship the minimal code needed. Some
values are interpreted to efficiently support large value ranges, e.g. opacity_0 through opacity_100 and font_weight_1 to font_weight_1000.
$globals include inherit|initial|revert|revert_layer|unset.
Position and display
- .position_static|relative|absolute|fixed|sticky|$globals
- .display_none|contents|block|flow_root|inline|inline_block|run_in|list_item|inline_list_item|flex|inline_flex|grid|inline_grid|ruby|block_ruby|table|inline_table|$globals
- .visibility_visible|hidden|collapse|$globals
- .float_left|right|none|inline_start|inline_end|$globals
- .opacity_0-100
- .overflow_auto|hidden|scroll|clip|visible
- .overflow_x|y_auto|hidden|scroll|clip|visible
- .overflow_wrap_normal|anywhere|break_word|$globals
- .scrollbar_width_auto|thin|none|$globals
- .scrollbar_gutter_auto|stable|stable_both_edges|$globals
Flexbox and grid
- .flex_1
- .flex_wrap_wrap|wrap_reverse|nowrap|$globals
- .flex_direction_row|column|row_reverse|column_reverse|$globals
- .flex_grow|shrink_1|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
Sizing and spacing
- .width|height_0|100|1px-3px|auto|max_content|min_content|fit_content|stretch
- .width|height_xs5-xl15
- .top|bottom|left|right_0|100|1px-3px|auto
- .top|bottom|left|right_xs5-xl15
- .inset_0|1px-3px|xs5-xl15
- .p|pt|pr|pb|pl|px|py_xs5-xl15
- .p|pt|pr|pb|pl|px|py_0|1px-3px
- .pt|pr|pb|pl_100
- .m|mt|mr|mb|ml|mx|my_xs5-xl15
- .m|mt|mr|mb|ml|mx|my_0|1px-3px|auto
- .mt|mr|mb|ml_100
- .gap_xs5-xl15
- .column|row_gap_xs5-xl15
- .width_upto_xs-xl
- .width_atleast_xs-xl
- .height_upto_xs-xl
- .height_atleast_xs-xl
Typography
- .font_family_sans|serif|mono
- .line_height_xs-xl|0|1
- .font_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
- .word_break_normal|break_all|keep_all|$globals
- .white_space_normal|nowrap|pre|pre_wrap|pre_line|break_spaces
- .white_space_collapse_collapse|preserve|preserve_breaks|preserve_spaces|break_spaces|$globals
- .text_wrap_wrap|nowrap|balance|pretty|stable
- .user_select_none|auto|text|all|$globals
- .font_weight_100-900
- .ellipsis
Colors
- .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-j
- .color_a-j_1-9
- .bg_a-j_1-9
Borders and outlines
- .border_color_1-5
- .border_color_a-j
- .border_color_transparent
- .border_width_0-9
- .outline_width_0|focused|active
- .border_style_none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset|$globals
- .border_radius_xs3-xl
- .border_radius_0-100
- .border_top|bottom_left|right_radius_xs3-xl
- .border_top|bottom_left|right_radius_0-100
Shadows
- .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-j
- .shadow_alpha_1-5
- .shadow_inherit|none
Transforms and visual effects
- .flip_x|y|xy
- .pixelated
Composite classes
- .box
- .column
- .row
- .formatted
- .selected
- .selectable
- .clickable
- .pane
- .panel
- .icon_button
- .plain
- .menu_item
- .chevron
- .chip
Builtin classes #
Moss's 🗎 main stylesheet provides styles for the base HTML elements using the framework's variables, acting as a modern CSS reset with sensible defaults and integrated theming. It includes Moss-specific CSS classes - not utility classes in the strict sense - that provide common generic functionality.
.unstyled
<ul>
<li>1</li>
<li>2</li>
</ul> - a
- b
<ul class="unstyled">
<li>a</li>
<li>b</li>
</ul> - 1
- 2
The .unstyled class lets Moss provide solid default element styles with a simple and
generic opt-out:
:where(:is(ul, ol, menu):not(.unstyled)) {
padding-left: var(--space_xl4);
} Respecting .unstyled isn't a straightforward choice in all cases. Help is
appreciated to refine the internals. For example, should input respect it? Maybe?
All styles or a subset?
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's 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 component values (e.g. 120 55% 36%, useful to efficiently 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's 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
- NaNerror/danger
- NaNsecondary/accent
- NaNtertiary/highlight
- NaNquaternary/muted
- NaNquinary/decorative
- NaNsenary/caution
- NaNseptenary/info
- NaNoctonary/flourish
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()
- 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
outlined buttons the default, and selected buttons are filled. There's also the .deselectable modifier class for buttons that remain clickable 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"> <button class="color_j"> 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
diva 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>#
aside looks like this<aside>
<aside>
<aside>nested asides</aside>
</aside>
</aside> #
<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 #
menumenu 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="width_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 families #
Font sizes #
Font weights #
Font weight values can be any integer from 1 to 1000.
There are no variables for font-weight but there are utility classes.
Text colors #
Line heights #
Icon sizes #
--font_size_ variables, --icon_ variables are in px not rem, so they're insensitive to browser font size18px32px48px80px128px192px256pxborders #
Border shades #
Border colors #
Border widths #
Outlines #
Each border utility class has a corresponding outline variant using the same border variables
(like outline_color_b, outline_width_4, and outline_style_solid), and there are also two special outline variables:
Border radius #
Border radius percentages #
Interpreted utility classes, 0 to 100 (%).
Border radius corners #
Border radius corner percentages #
Interpreted utility classes, 0 to 100 (%).
shading #
Moss is designed around two simplistic models of light, one for dark mode and one for light mode, mapping to the web platform's color-scheme. 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
and the shadows docs implement more of the idea.
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 lightenbg and fg #
bg and fgIn 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 transparency #
Many styles are designed to stack, so things can appear in different contexts while retaining relative color value distinctiveness ("color value" as in darkness-lightness). Internally this uses simple transparency instead of complex selectors or other structure.
<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> these shades use opacity, but notice how contrast changes with depth, creating limitations
This adds some complexity and performance costs, and it's currently incomplete, but so far it feels like an elegant solution with many unfinished details, and I plan to continue integrating the idea in more places while considering alternative designs. However alpha transparency has multiple costs, so I'm trying to be mindful to not use alpha for text and other cases that are more performance-sensitive, but we may need to change this behavior for the base cases, or include performance themes.
Opacity #
Interpreted utility classes, 0 to 100 (%).
shadows #
Moss's shadows build on the light model discussed in the shading docs.
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_ layout #
Space variables #
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
Space variants are used in classes like .p_md for padding, margin, other forms of spacing like gap, positioning, dimensions, etc.
Width variables #
- =
- =
- =
- =
- =
Width variants have classes like .width_sm and .min_width_md.