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.

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 with { title: 'Hello', children: 'world', link: '/'} will generate the following HTML structure:

<div class="Teaser">
  <h1 class="Teaser-H1">
    Hello
  </h1>
  <p>
    world
  </p>
  <a href="/">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:

a {
  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.

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

a {
  display: block;
  @media (min-width: 600px) {
    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.

If a .tl file does not export an element but just some CSS properties or nested content, it will be treated as a mixin:

// util/clearfix.tl 
{
  :after {
    content: "";
    display: table;
    clear: both;
  }
}

To use a mixin, just reference it as you would do with any other element.

Note: In future Teal might map mixins without content to utility classes instead of mixing them in.

If you need more flexibility and want to parameterize the properties, you can use a macro element instead.

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.

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.

Teal supports the following built-in functions:

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.

You can display different output depending on the truthiness of a value:

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:

a {
  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.

In Teal @keyframe declarations don't have a name. Instead their name is derived from the name of the file in which they are declared:

// /ani/fade-in.tl 
@keyframes {
  from { color: transparent }
  100% { color: #000 }
}

You can access the name using the import() function:

div {
  animation: import(ani/fade-in) 2s;
}

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.

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.

Eventually yes, as Teal's design makes it really easy to write new language adapters. The next official adapter will be a flattening compiler that can be used as base to target existing template languages like mustache, handlebars or Twig. JSP and ERB will probably come next.

In its first iteration Teal supported a wide range of language constructs like callables, member access, object literals and unary/binary/ternary expressions that were mapped to the target language. We then decided to give up on these features and settled with a logic-less approach in the spirit mustache.

We decided to focus on the possibility to switch back and forth between Teal and your runtime language instead. Just put a .js or .php file right next to your components and reference it from Teal as you would do with a regular Teal component. Then in your host language invoke the Teal API to render fragments and pass the results to other components.

CSS was designed with a different use-case in mind. And while the original document-centric idea is still great if you need to style markup that you don't control or if you want to style the same document in many diffent ways, today's modern web applications often struggle with the global nature of CSS.

Thinking in components is so fundamentally different from how we’ve been working before, that we should take second, unbiased look. Just because we've been using two different technologies, this doesn't mean that defining how a component is rendered can't be one single concern.

There are quite a lot of projects in the React.js universe that do something similar like Teal. This is great if your whole team knows how React works and React apps are everything you'll ever build. With Teal on the other hand you can let one part of your team build static Teal components regardless of the final runtime environment, while others can use that very same code in a React/Flux/Node/PHP/whatever application without having to care about the stylistic aspects.

Usage

To use Teal, choose a runtime below and head over to the respective project page. Here's what using Teal inside an express app looks like:

var express = require('express')
var teal = require('teal-express')
 
var app = express()
var tl = teal(app)
 
app.get('/', function(req, res) {
  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.)