🔗Overview
Bowdy compiles BASS files (.bass) to standard CSS. Plain CSS is valid BASS, so you can adopt it incrementally.
var primary = #0d6efd
var radius = 4px
.card
color: $primary
border-radius: $radius.card{color:#0d6efd;border-radius:4px}Output is minified by default. Pass --pretty for formatted output.
Two block styles, freely mixed — even inside the same file:
.card {
color: red
}
.card
color: red
&:hover
color: blackRules:
- Indentation defines nesting for the indent style. A selector with no indented body and no
{...}is a parse error (never a silent empty rule). { ... }works everywhere a block is expected.- You can nest an indent block inside braces and vice versa.
🔗Comments
// single line, stripped
/* block, stripped */
/** docblock, preserved verbatim */
/*! banner, preserved verbatim *///runs to end of line./* ... */is stripped./** ... */and/*! ... */are preserved in output in source order, including banners inside rule bodies (emitted before the rule) and between rules.#is not a comment. It starts an ID selector or hex color.
🔗Selectors
Supported: class (.foo), ID (#header), element (h1), universal (*), pseudo-class/element (:hover, ::before, :root), functional pseudo (:is(), :not(), :nth-child(2n+1)), attributes (see below), comma lists, combinators.
.foo { color: red; }
#header { width: 100px; }
h1 { color: red; }
* { margin: 0; }
.btn:hover { color: blue; }
.foo::before { display: block; }.foo{color:#ff0000}
#header{width:100px}
h1{color:#ff0000}
*{margin:0}
.btn:hover{color:#0000ff}
.foo::before{display:block}Comma-separated and combinators (spacing-insensitive > / + / ~):
h1, btn { color: red; }
a, btn:hover { padding-top: 10px; }
.offcanvas.offcanvas-start { color: red; }
.parent>.child { color: red; }
.btn-check:checked+.btn { color: red; }
ol ol, ul ul, ol ul, ul ol { margin-bottom: 0; }
.table :is(thead,tbody,tfoot)>tr>th,td { padding: .5rem; }🔗Attribute selectors
[data-bs-theme=dark] { color: red; }
[data-bs-theme="dark"] { color: red; }
.dropdown[data-bs-popper] { padding: 0; }
a[href^="http"] { color: blue; }
[data-x~=foo] { display: block; }
[data-x$="suffix"] { display: none; }
[data-x|=en] { width: 10px; }
input[type="checkbox"]:checked { color: green; }
[data-x=foo i] { display: block; }Operators: = ~= ^= $= *= |= ^=. Case flags i / s are preserved ([type="text" i]).
🔗Properties and values
.card
color: red
margin: 10px 20px
box-shadow: 0 1px 2px rgba(0,0,0,.3), inset 0 0 0 1px red
color: red !importantname: value, one per line (indent) or;-separated (braces). Trailing;is optional.!importantsuffix is supported.- Space-separated parts form one value; commas separate segments.
- Duplicate keys are allowed (vendor fallbacks):
th { text-align: inherit; text-align: -webkit-match-parent; }🔗Numbers and units
- Ints (
10,1e3->1000), floats (1.5,1.0->1), scientific (1e3px,1.5e-2px,1E1,-1e2px,5e-1s). - Leading-dot (
.125rem->0.125rem) and leading-plus (+5px->5px,+.5->0.5). - Negative numbers are single tokens (
-0.375rem -0.75remare two values). - Glued units:
10px,2em,50%,45deg,0.3s. Unknown suffixes (e.g.1x) flow through as text; typos surface as validation errors instead of vanishing.
Known suffixes:
px em rem % vh vw vmin vmax ch ex cm mm in pt pc
s ms
deg rad grad turn
dpi dpcm dppx
Hz kHz fr🔗Strings, keywords, colors
.a { font-family: "Arial"; }
.a { inherits: true; }
.a { content: null; }
.a { color: #0d6efd; }
.a { color: red; } /* -> #ff0000 */"..."/'...'strings. CSS escapes round-trip ("\201E","a\\b","a\"b").true/false/nullin value position render as text.- Hex keeps spelling (lowercased):
#0d6efd,#0e3f,#1e1e1e. - Named colors normalize to lowercase hex (
red->#ff0000), including inside gradients.transparent,currentcolorand CSS-wide keywords (inherit initial unset revert revert-layer) stay keywords.
🔗Variables
var primary = red
var radius = 4px
const gap = 8px
.card
color: $primary
border-radius: $radiusvar/const. Use sites always use$name.- Definitions are always bare (
var accent = redregisters$accent);$accentor_accentat definition is a parse error.*marks export for imports (var accent* = ...). - Optional type:
var c: color = red,var w: length = 4px. - Comma form:
var a = 10, b = 20(same-line idents only). - Arithmetic in scope:
$base + 5,$radius - 3px,4px + 3px. Same-unit rule applies to typed lengths/angles/times (mismatched units are hard errors).1px * $i/$i * 1pxwork for loop unrolling. - Reassignment:
$i = $i + 1(used withwhile). - Named colors in initializers hexify (
var primary = redstores#ff0000).
🔗Interpolation
${expr} inside values and selectors:
.a
width: ${3}px /* -> 3px */
order: ${size + 1}
for i in range(1, 3):
.p-${$i}
z-index: $i
padding: ${$i}px
margin: ${$i}px auto.p-1{z-index:1;padding:1px;margin:1px auto}.p-2{z-index:2;padding:2px;margin:2px auto}.p-3{z-index:3;padding:3px;margin:3px auto}${$i}px/${3}emfold into a unit. Loop vars resolve at parse time when unrolled; other dynamic bases error clearly in codegen.
🔗Nesting
Descendant nesting is automatic; & splices the parent (Sass-style):
.card
color: gray
&:hover
color: black
&.active
color: red
.title
font-weight: bold.card{color:gray}.card:hover{color:black}.card.active{color:red}.card .title{font-weight:bold}Combinators and comma handling:
.parent
& > .item
margin: 0
& + .item
margin: 0
& ~ .item
margin: 0.a, .b
.child
color: red
/* -> .a .child, .b .child{color:red} */
.card
&:hover, &:focus
outline: 2px
/* -> .card:hover, .card:focus{outline:2px} */
.a, .b
&:hover
color: red
/* -> .a:hover, .b:hover{color:red} */- Parent may carry properties; they emit first, then children flatten.
- Deep nesting works (
.x .y .z). &inside mixins resolves against the call-site parent.- At-rules may interleave with nesting and stay in source order.
🔗At-rules
Both styles work; at-rules nest inside selectors and selectors inside at-rules:
@media (max-width: 768px)
.foo
color: red
@supports (display: grid)
.foo { color: red; }
@layer
.foo
color: red
@layer base, theme;
@font-face {
font-family: "Custom";
src: url("custom.woff2");
}
@keyframes slide
from
opacity: 0
to
opacity: 1
@keyframes move { 0% { left: 0; } 25%, 75% { left: 50%; } 100% { left: 100%; } }
@charset "utf-8";
@import url("style.css");
@namespace url("http://www.w3.org/1999/xhtml");Nested at-rule inside a selector stays inside the emitted rule block:
.parent
@media (max-width: 768px)
.child
color: blue
/* -> .parent{@media (max-width: 768px){.child{color:blue}}} */🔗Mixins
Reusable declaration blocks spliced into rule bodies. No CSS is emitted for the definition alone.
mixin btn(color: color) =
color: $color
border-radius: 4px
.a
@btn(red).a{color:#ff0000;border-radius:4px}Signatures:
mixin reset() =
margin: 0
padding: 0
mixin box(w: length, h: length) =
width: $w
height: $h
mixin btn(color: color) {
color: $color
}- Indent body requires
=(mixin m(...) =); brace body does not (mixin m(...) { ... }). Missing=before an indent body is a parse error. - Calls:
@name(args)inside a rule body.@reset()with parens works for zero-arg mixins. - Positional + named args; named accepts both
$h = 5pxandh = 5px:
.a
@box($h = 5px, $w = 10px)- Typed params are checked; missing / too many args and unknown mixins are errors (unknown mixins warn at codegen when not statically known).
- Bodies see outer vars; args accept outer vars and calls (
@m(darken(red, 10))). - Nested selectors splice with the caller as parent;
&works in both indent and brace mixin bodies. - Property order is preserved (parent props, then spliced props, then trailing parent props).
🔗Functions
fn / func are aliases. Functions evaluate in expression position and support overloading and forward declarations.
fn dbl(n: int): int =
return $n * 2
var p = dbl(21)
.a { z-index: $p }.a{z-index:42}- Indent body requires
=(func dbl(n: int): int =); brace body does not (func dbl(n: int): int { ... }).= { ... }is a parse error, as is an indented body without=. return expr, plusecho,break,continuestatements exist.- Typed params (
n: int, used as$n) and return type (: int).
🔗Control flow
Conditions take : or {. All forms work top-level and inside rule bodies (inside rules they emit declarations in source order).
var debug = true
.a
if $debug:
outline: 1px
else:
outline: nonevar m = false
.a
if $m:
color: red
elif $m == false:
color: blue
else:
color: greenLoops:
for i in range(1, 3):
.p-${$i}
z-index: $i
/* -> .p-1{z-index:1}.p-2{z-index:2}.p-3{z-index:3} */
/* arrays of objects */
var s = [{k: 0, v: 0}, {k: 1, v: 0.25rem}]
for item in $s:
.p-${$item.k}
padding: $item.v
for s in [{k: 0, v: 0}, {k: 1, v: 1rem}]:
.m-${$s.k}
margin: $s.vrange(lo, hi)is inclusive. Constantrangeloops with${...}selectors unroll at parse time.- Inside rules,
forrepeats declarations:.a { for i ...: z-index: $i }->z-index:1;z-index:2;....
var i = 0
.a
while $i < 2
z-index: $i
$i = $i + 1
/* -> .a{z-index:0;z-index:1} */Case:
case $x:
of 1:
/* ... */
of 2:
/* ... */
else:
/* ... */case X: with optional colon, of value: branches, optional else:. Brace form case X { of ... } is also accepted.
🔗Modules and imports
// _vars.bass
var accent* = #0d6efd
var radius* = 4px
.base
color: gray
// main.bass
import "./_vars.bass"
.a
color: $accent
border-radius: $radius.base{color:#808080}.a{color:#0d6efd;border-radius:4px}- Only
*-exported vars/functions are visible to importers; rules splice unconditionally. - Relative imports resolve against the importing file. Missing files are errors.
- Package imports:
import "pkg/name"/import "pkg/name/sub"resolve under~/.tim/packages/<name>/<latest-version>/src/(main.bass,index.bass,<name>.bass, plus.timl/.cssfallbacks). - One import statement per line:
import "./_vars.bass".
🔗Custom properties and var()
Custom properties are first-class typed values, not strings:
:root { --bs-blue: #0d6efd; --bs-breakpoint-md: 768px; }
.btn { color: var(--bs-btn-hover-color); }
.a { color: var(--x, red); }--namelexes as one token;--color-gray-100is never re-split.var(--name[, fallback])parses as a real call returningcssvar. Bare keywords in fallbacks stay verbatim (var(--x, none)); dynamic fallbacks evaluate.var($n)allows a computed name (var n = "--brand").- Registry is file-global across entry + imports, order-independent, last-wins. Selector/media scoping is ignored by design.
- Declarations infer types: lengths, colors,
darken(red, 10)-> color,$caliases, chainedvar(--other)aliases. Keywords (transparent,auto) stay unchecked. - Under
--strict: mistyped uses are hard errors (color: var(--fs-medium)where--fs-medium: 1rem); undeclared names only warn (they may come from plain-CSS imports or JS); fallbacks validate too.env()is untouched.
🔗Modern CSS functions
Two worlds:
- Bowdy value functions parse as real calls and evaluate to strictly typed values.
- Everything else (
calc,url,env,min/max/clamp,attr,counter,repeat/minmax, ...) stays opaque raw text with spacing preserved.
.a { width: calc(100% - 2rem); }
.a { width: clamp(1rem, 2.5vw, 2rem); }
.a { background: url("img.png"); }
.a { padding-top: env(safe-area-inset-top); }
.a { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
.a { transform: translate(-50%, -50%) rotate(45deg); }
.a { filter: drop-shadow(0 0 5px rgba(0,0,0,.5)); }
.a { background: color-mix(in oklab, red, blue); }Typed families: transforms (translate translateX translateY translateZ translate3d scale scaleX scaleY scaleZ scale3d rotate rotateX rotateY rotateZ rotate3d skew skewX skewY matrix matrix3d perspective), filters (blur brightness contrast grayscale invert opacity saturate sepia hue-rotate drop-shadow), gradients (linear-gradient repeating-linear-gradient radial-gradient repeating-radial-gradient conic-gradient repeating-conic-gradient), shapes (circle ellipse inset polygon path xywh ray), easing (cubic-bezier steps linear), colors (rgb rgba hsl hsla hwb lab lch oklab oklch color light-dark color-mix image image-set), plus var.
Notes:
- Gradient direction keywords (
to right,circle at center) stringify so they never resolve as vars. color-mix(in <space>, a, b)leads with theinkeyword; percentages space-join onto their color.- Fully static
rgb()/hsl()-family calls keep source spelling (rgb(13 110 253),rgba(0,0,0,.3)); dynamic forms (any$var/var()/nested call) evaluate and render canonically. Slash alpha (rgb(255 0 0 / 50%)) splices into components + alpha. - A real CSS call nesting a permanently opaque fn (
calc,url,env, ...) falls back to verbatim text. - Dynamic multi-values (
margin: $a $b,border: 1px solid $c) evaluate each part at runtime; static multi-values render directly.
🔗Type system: lenient vs strict
Default bowdy c is lenient: only VM types apply (variable types, function signatures, stdlib constructors). A mistyped width: $colorVar still compiles.
--strict enables the static CSS type system: property values check against their CSS syntax, invalid colors are hard errors, and undeclared var(--x) names warn. Output is identical either way; only diagnostics change.
var primary = red
.a { width: $primary; } /* lenient: compiles; strict: error */Strict also validates literals (color: 123, width: 100 200, display: yesplease, z-index: 10px, color: #zzzzzz all error) while box-shadow, grid-template-columns, content, margin, inherits stay warn-only for exotic forms.
🔗Errors
Parse and codegen errors carry source context (snippet + caret + line:col):
.a { color: red;
^
(1:16) expected closing '}' ...Common failures: missing = after mixin signature, missing :/{ after if/for/of, unclosed paren/brace, missing mixin arg, type mismatch under --strict, missing import file.