A small pre-processing extension language for CSS written in Elixir. Its main purpose is to provide a native Elixir pre-processor for CSS, in the vein of Sass/Scss.
button {
background-color: blue;
color: white;
padding: 5px;
.class_1 {
color: yellow;
svg&, .child {
fill: red;
}
}
&.concatenated_class {
padding: 10px;
}
@media screen and (max-width: 756px) {
font-size: 24px;
@media and (min-width: 500px) {
font-weight: 300;
}
}
}
button { background-color: blue; color: white; padding: 5px }
button .class_1 { color: yellow }
button svg.class_1, button .class_1.child { fill: red }
button.concatenated_class { padding: 10px }
@media screen and (max-width: 756px) { button { font-size: 24px } }
@media screen and (max-width: 756px) and (min-width: 500px) { button { font-weight: 300 } }
The &
operator can only be used inside a block and either at the start or end of each selector (or if single inside a :not(&)
, or :is(&)
).
If it's at the start it will append that selector to the parent, unless it's a tag selector (p
, canvas
, your-custom-element
, etc) in which case it will prepend itself, and raise an error if you're trying to do so with a parent tag.
If it's at the end then the selector becomes the parent of it's parent.
div, section {
&.concat, .parent& {
.inner { color: blue; }
&.inner-concat { color: red; }
}
}
into
div.concat .inner,
.parent div .inner,
section.concat .inner,
.parent section .inner {
color:blue
}
div.concat.inner-concat,
.parent.inner-concat div,
section.concat.inner-concat,
.parent.inner-concat section {
color:red
}
Bare selectors inside blocks create regular selection chains.
@media
declarations can be nested as well but as of now there's no semantic checks done, which means that if you write something non-sensical it will be placed as is.
$!a_variable red;
$!another_variable 12;
div {
color: <$a_variable$>;
font-size: <$another_variable$>px;
}
div {
color: red;
font-size: 12px;
}
$*!primary red;
div { color: <$primary$>; }
:root {
--primary: red;
}
div { color: red; }
$!scope_variable_1 20px;
$!scope_variable_2 blue;
div { font-size: <$scope_variable_1$>; }
@include file_2.cssex;
#main {
font-size: <$scope_variable_1$>;
color: <$scope_variable_2$>;
}
$()scope_variable_1 16px;
$!scope_variable_2 red;
.something {
font-size: <$scope_variable_1$>;
color: <$scope_variable_2$>;
}
@include file_3.cssex;
$?scope_variable_1 12px;
$?scope_variable_2 green;
.something-2 {
font-size: <$scope_variable_1$>;
color: <$scope_variable_2$>;
}
div { font-size: 20px; }
#main {
font-size: 20px;
color: red;
}
.something {
font-size: 16px;
color: red;
}
.something-2 {
font-size: 20px;
color: red;
}
Variables are literal values that can be used throughout a spreadsheet. They're inserted in place using the interpolation markers, <$ variable_name $>
, or $::variable_name
.
The declaration form is:
$!name_of_variable literal_value;
literal_value
shouldn't be surround by quotes unless you want the quotes to be part of the value. It must always end with semi-colon followed by newline.
Using variables with @include
's directives allow one to share or create values that are overridable or specific to a stylesheet/include.
When a stylesheet declares an @include
the current variables in scope will be made available on the child.
Child files (the one's @include
d) can override the parent's variable value (after the child has been processed) if they declare them with $!
,
You can otherwise limit this by declaring them with:
$()
which scopes the variable locally so they do not become available to the parent afterwards, and you can also conditionally set them with:
$?
which only sets them for the file if it isn't set in its scope yet. This allows you to create customisable themes, in that you can have a file(s) that specifies all colors, sizes and others, and then make the theme "components", set their needed values with $?
which will populate those for the scope only if they haven't been declared previously, or override them in an included stylesheet with $()
You can also declare a variable with $*!
, that does all the same but sets as well a CSS variable on the root element, with the same name as the variable declared, prefixed with --
.
If you referr to a variable that hasn't been declared or not in scope you'll get an error.
Assigns are the equivalent of variables but for Elixir values and they have the same options and scoping as that of variables, but instead of being declared with $!
, $()
and $?
, they're declared with @!
, @()
and @?
.
Variables are available inside EEx blocks or when calling functions. Since they can hold any elixir term they're great to create basic iterable structures to generate CSS based on repetition or iteration.
@!colors %{
primary: "red",
secondary: "rgb(120, 255, 80)"
};
<%= for {color, val} <- @colors, reduce: "" do
acc ->
acc <> """
$*!#{color} #{val};
.btn-#{color} {
background-color: #{val};
}
"""
end %>
into
.root {
--primary: red;
--secondary: rgb(120, 255, 80)
}
.btn-primary { background-color: red }
.btn-secondary { background-color: rgb(120, 255, 80) }
(while also creating both the primary
and secondary
variables that are accessible through the remaining spreadsheet and @include
d children.
Inside EEx blocks they can be referred to with Elixir's @assign_name
notation. When passing them to function as arguments they can be passed in as @::assign_name
So @fn::name_of_function(@::colors)
would call that function with the argument being translated to the Elixir value of that variable.
As literal variables it will error if they're used without having been declared before or not available in scope.
The declaration form is:
@!name_of_variable {:tuple, %{elixir: "map"}};
It has to be terminated with semicolon followed by newline.
Expandables allow you to define utility classes that can be used inside any selector to add attributes & or selectors. It allows for then @apply
ing those blocks in different ways. You can force the @apply
to be exactly as it was resolved when declared and keep the selector hierarchies there defined in reference to the @expandable
selector, you can use it to be dynamically evaluated, or to apply its hierarchies in the new context while using any variable interpolation as it occurred when defining. It might sound confusing but with an example is easier to see
$!color red;
@expandable .hoverable {
color: <$color$>;
&:hover {
color: @fn::darken(<$color$>, 10);
}
container& {
background-color: black;
}
}
.class-1 {
@apply hoverable;
}
$!color blue;
.class-2 {
@apply !hoverable;
}
.class-3 {
@apply ?hoverable;
}
.class-4 {
@apply hoverable;
}
.hoverable {
color:red
}
.hoverable:hover {
color:rgba(204,0,0,1.0)
}
container .hoverable {
background-color:black
}
.class-1 {
color:red
}
.class-1:hover {
color:rgba(204,0,0,1.0)
}
container .class-1 {
background-color:black
}
.class-2 {
color:red
}
.class-2 .hoverable:hover {
color:rgba(204,0,0,1.0)
}
.class-2 container .hoverable {
background-color:black
}
.class-3 {
color:blue
}
.class-3:hover {
color:rgba(0,0,204,1.0)
}
container .class-3 {
background-color:black
}
.class-4 {
color:red
}
.class-4:hover {
color:rgba(204,0,0,1.0)
}
container .class-4 {
background-color:black
}
As you can see, we had a variable $!color
declared with the value red
.
Then we declared an @expandable
block for the selector .hoverable
, where we used interpolation for color
, and had nesting selectors, one of which calling a function again with an interpolated value.
That is the top level declarations that came on top of the stylesheet:
.hoverable {
color:red
}
.hoverable:hover {
color:rgba(204,0,0,1.0)
}
container .hoverable {
background-color:black
}
Then we declared a .class-1
selector and applied the hoverable
element we declared as @expandable
before inside it (notice we don't use the .
before its name).
What this did was apply all the contents of that expandable element inside .class-1
. You can see that the nesting &
where applied to .class-1
:
.class-1 {
color:red
}
.class-1:hover {
color:rgba(204,0,0,1.0)
}
container .class-1 {
background-color:black
}
Then we overwrote the color
variable with the value blue
.
On .class-2
we did the same as in .class-1
but this time we prepended the name with !
. This forces the expandable element to be inserted with the interpolation it had when evaluated originally and also with the nesting referring to the original .hoverable
selector:
.class-2 {
color:red
}
.class-2 .hoverable:hover {
color:rgba(204,0,0,1.0)
}
.class-2 container .hoverable {
background-color:black
}
On .class-3
we did the same, but now prepended the expandable identifier with ?
. This made it be dynamically evaluated, both interpolation and nesting. You can see the color being blue and the result of @fn::darken
being blue as well:
.class-3 {
color:blue
}
.class-3:hover {
color:rgba(0,204,0,1.0)
}
container .class-3 {
background-color:black
}
Lastly we did the same original non-prefix @apply
in .class-4
and in this case, the value of the variables used were the ones set at declaration time, red
, but the nesting was still applied in terms of the current selector:
.class-4 {
color:red
}
.class-4:hover {
color:rgba(204,0,0,1.0)
}
container .class-4 {
background-color:black
}
You can also string together several elements to expand in a single @apply
, with different evaluation scopes as well. The order of declaration is the order the attributes will be set.
@apply one-expandable ?another-expandable !a-different-one;
@expandable
declarations specify a single class selector (e.g. .class
) followed by a block { ... }
.
The @expandable
directives are placed in order of declaration at the top of the final stylesheet as individual selectors, but you can declare them from any file or part of the file as long as it's not inside a block, they must always be declared on a top level.
The only exception on their final placement is when using @media
selectors inside @expandable
. In those cases normal rules will go to the top but the media parts will be placed alongside the other media statements.
The @apply
directive takes a list of white-space separated tokens, where each token refers to a previously declared @expandable
block. You should not use the .
of the original class to refer to them. You can additionally define their expansion mode with ?
(all elements dynamically evaluated) or !
(as a static block as evaluated when declared). The default without prefix is a mix of both, variables with the values at the time of declaration and nesting dynamically evaluated inside the context of the block the @apply
is declared in.
Keep note that <$ ... $>
interpolation is the only consistent form of interpolation. If you use, for instance in @fn::...
call the form $::color
, this will always be dynamically evaluated. EEx blocks follow the same pattern, if you want them to be expanded as they resolved at declaration you need to use the !
prefix when @apply
ing.
Note that you can also declare @media
attributes inside @expandables
and nest them as well. Most times you'll want to use normal applying or ?
.