teal

stylesheets without selectors

Teal – High-level templates

Teal is a high-level, component oriented template language that defines markup and CSS together in a single place. This eliminates the need for manually writing selectors and enables automatic class name management.

At the first glance a teal template might look like a Sass-file with nested type selectors, but under the hood the compiler uses this structural information together with a file's name and location to generate BEM/SUIT-like class names.

With the various compilation targets that are already available, you can use teal in Node, PHP, the browser and even in React.js projects.

Note: Teal is currently in alpha stage. We have successfully used it in production for several short-lived projects and will continue to do so while we tweak the syntax and the final feature scope.

Syntax

Here is a simple example, lets call it el/teaser.tl:

div {
  background: #888;
  padding: 1em;
  h1 {
    font-size: 2em;
    $title
  }
  p {
    $children
  }
  a { href = $link "Read more" }
}

This, when rendered, will generate the following HTML structure:

<div class="Teaser">
  <h1 class="Teaser-H1">
    {{title}}
  </h1>
  <p>
    {{children}}
  </p>
  <a href="{{link}}">Read more</a>
</div>

Also the following rules will be added to the generated stylesheet:

.Teaser {
  background: #888;
  padding: 1em;
}
.Teaser-H1 {
  font-size: 2em
}

As you can see, the Teal source code does not contain a single class name. The selectors and class attributes are automatically created by Teal.

You can think of a .tl file as kind of custom HTML element with custom attributes. If you use a tag name that contains a slash, Teal will interpret it as path, resolve it and render the specified file:

div {
  el/teaser {
    title = "Hello world"
    children = "Lorem ipsum"
  }
  ./foo {
    title = "Another component"
  }
}

Note: You can also pass other elements or document fragments as parameter:

./two-cols {
  left = el/teaser { title = "Left" }
  right = el/teaser { title = "Right" }
}

If you pass children to a component, Teal will expose them as $children. So the following two examples are equivalent:

el/teaser {
  children = {
    "Hello" b { "World!" }
  }
}
 
// this can be written as: 
 
el/teaser {
  "Hello" b { "World!" }
}

If a component does not contain a $children variable, all nested content is appended to the component's root element. All other unknown parameters are set as HTML attributes. This allows you to style HTML elements without having to list all possible attributes. See how the following example does neither contain $children nor $href:

{
  text-decoration: none;
  color: inherit;
  :hover {
    color: teal;
  }
}

Teal supports boolean, numeric and string literals:

form {
  textarea { name='answer' id="foo" cols=42 disabled=true
    ´This is a multiline string. Multiline strings are delimited by
    forward ticks (´´) which can be escaped using a double forward tick.´
  }
  "Double (\") and single (') quoted strings work as in JavaScript."
  'You can use them interchangeably.'
}

Note: Teal uses the ´ character so that you don't have to escape your markdown or ES6 template strings. In single- and double-quoted strings the backslash is used as ecape character.

A component may define different states:

button {
  background: gray;
  .primary {
    background: blue;
    font-size: 2em;
  }
  .danger {
    background: red;
  }
}

To activate a state just pass a truthy parameter with the name of the state:

./button { primary=true }

Note: If you omit the value and just specify a name true is implied. Hence the following code yields the same result:

./button { primary }

If multiple elements inside a component need to be styled when a certain state is active, just repeat the same modifier and Teal will figure out the appropriate selectors:

button {
  .primary {
    background: blue;
  }
  i {
    .primary {
      color: green
    }
  }
}

Note: Teal makes no difference between states and modifiers since from an implementation standpoint they are technically the same. You can of course use your own naming convention and add a prefix like is- or js- to to your classes as needed.

You can define global constants for colors or sizes using the @const directive:

@const {
  error: #f00;
  gutter: 12px;
  desktop: (min-width:960px);
}

With Teal you can define all media-specific styles right next to the rest of a component's style declarations:

{
  display: block;
  @media const(desktop) {
    float: left;
    width: 50%;
  }
}

When building the CSS, Teal will collect all identical media queries and group them in one single @media block at the end of the stylesheet.

You can create mixins using the @mixin directive:

// util/rounded.tl 
@mixin {
  border-radius: param(radius 50%);
}

To use a mixin, just reference it as you would do with any other element. If a mixin contains param()s you can pass values as custom CSS properties:

// el/button.tl 
button {
  util/rounded {
    radius: 3px;
  }
}

Note: In future Teal might map mixins without any parameters to utility classes instead of mixing them in. Hence @mixin could get renamed to a more generic term.

Teal provides an API that allows add-ons to register macro elements. These elements have no .tl file but are a JavaScript functions that receive an AST node which they can transform into something different. The built-in teal/stylesheet element for example is implemented as macro element.

The API also allows you to register elements with names that include wildcards. Currently Teal ships with an experimental media/* macro that uses this feature and allows you to write media/desktop { ... } as shorthand for @media const(desktop) {}

The same feature could be used to import whole component libraries, from an npm module for example.

Macros can also be implemented as functions. This is useful if you want to return just a single value rather than whole subtree. Also functions are the only option if you want to use macros inside a CSS value, since the language grammar does not allow elements in this context.

Besides the const() and param() functions that we've already seen, there are yet some additional built-ins.

Teal supports the CSS color() function via the css-color-function module.

This allows you to do things like this:

div {
  color: color(red a(10%));
  background-color: color(red lightness(50%));
  border-color: color(hsla(12550%50%.4) saturation(+ 10%) w(- 20%));
}

If you pass an expession to the CSS calc() function that can be statically evaluated, Teal will replace the call with the actual result. Of course you can use const() or param() (if inside a mixin) within the expression:

div {
  margin: calc(const(gutter) / 2);
}

If a component requires an asset, you can reference it with a relative path using the src() function:

div {
  img { src=src("./rainbow.gif") }
  background: url(src("./sky.jpg"));
}

A Teal runtime like teal-express or teal-php will expose these assets so that they become requestable.

div {
  @if $link el/a { href=$link "Details" }
  @else "Sorry, no details available."
}

You can negate the expression using the not keyword:

div {
  @if not $link "Sorry, no details available."
}

The @if construct itself is an expression, so you can pass it as argument:

{
  href = @if $link $link @else "/"
}

You can loop over arrays using the @for:

ul {
  @for $people li { $firstName $lastName }
}

The body of a @for loop is evaluated in the scope of the respective item. You can use $$ to reference the item itself. It's not possible to access the parent scope so you have to prepare your data accordingly.

Like almost anything in CSS, keyframe are globals. To properly scope your animations, Teal allows you to define them inline:

div {
  @animation 1s ease {
    from { color: transparent }
    100% { color: #000 }
  }
}

This will result in the following CSS:

@keyframes FooBar {
  from { color: transparent }
  100% { color: #000 }
}
.FooBar {
  animation: FooBar 1s ease;
}

By default Teal applies a variant of the SUIT CSS naming conventions with one special twist: State-names are used 1:1 without any special prefix. Since states must begin with a lower case character they can never conflict with a component or element class since these always start with the component name in PascalCase.

This makes it easy to toggle a component's state from JS. Just call el.classList.toggle(state) with el being the root element of your component that you can get hold of by using an add-on like teal-behavior.

Usage

var express = require('express')
var teal = require('teal-express')
 
var app = express()
var tl = teal(app)
 
app.get('/', function(reqres) {
  res.render('page', { title: 'Hello world.' })
})
  • teal-express to use Teal as view engine in express (or hapi) apps

  • teal-php to compile .tl files to PHP

  • teal-static to build static HTML pages using Teal and Markdown

  • teal-browserify to use Teal components in browser apps and/or to easily create browserify bundles from within a .tl file.

  • teal-react to compile .tl files into React components

  • teal-watch to live-reload the HTML/CSS when a file is modified

  • teal-autoprefixer to automatically add vendor prefixes

  • teal-normalize to use normalize.css as baseline

  • teal-tagify to style external markup (e.g. generated from markdown) by using scoped selectors

  • teal-behavior to initialize components on the client (add event listeners etc.)