Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

CSS Styling

Blinc includes a full-featured CSS engine that lets you style your UI with familiar CSS syntax. Write stylesheets with selectors, animations, transitions, filters, 3D transforms, and more — then apply them with a single ctx.add_css() call.

Quick Start

use blinc_app::prelude::*;
use blinc_app::windowed::{WindowedApp, WindowedContext};

fn main() -> Result<()> {
    let mut css_loaded = false;

    WindowedApp::run(WindowConfig::default(), move |ctx| {
        if !css_loaded {
            ctx.add_css(r#"
                #card {
                    background: linear-gradient(135deg, #667eea, #764ba2);
                    border-radius: 16px;
                    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
                    padding: 24px;
                    transition: transform 0.3s ease, box-shadow 0.3s ease;
                }
                #card:hover {
                    transform: scale(1.03);
                    box-shadow: 0 12px 48px rgba(102, 126, 234, 0.5);
                }
            "#);
            css_loaded = true;
        }
        build_ui(ctx)
    })
}

fn build_ui(_ctx: &WindowedContext) -> impl ElementBuilder {
    div().id("card").child(text("Hello, CSS!").size(20.0).color(Color::WHITE))
}

Rust code defines structure. CSS defines style.


Table of Contents


Selectors

Blinc supports a wide range of CSS selectors — from simple IDs to complex combinators.

ID Selectors

The most common way to target elements. Attach an id in Rust, then style it in CSS:

#![allow(unused)]
fn main() {
div().id("card")        // Rust
}
#card { background: #3b82f6; }

Class Selectors

Assign CSS classes with .class() in Rust:

#![allow(unused)]
fn main() {
div().class("icon-wrapper")
}
.icon-wrapper {
    border-radius: 24px;
    backdrop-filter: blur(12px);
    transition: transform 0.2s ease;
}
.icon-wrapper:hover {
    transform: scale(1.12);
}

Type / Tag Selectors

Target elements by tag name (primarily used for SVG sub-elements):

svg { stroke: #ffffff; fill: none; stroke-width: 2.5; }
path { stroke: #8b5cf6; stroke-width: 5; }
circle { fill: #f3e8ff; stroke: #a78bfa; }
rect { fill: #fef3c7; stroke: #f59e0b; }

Universal Selector

* { opacity: 1.0; }

Pseudo-Classes (States)

Interactive states are matched automatically based on user input:

#button:hover   { transform: scale(1.02); }
#button:active  { transform: scale(0.98); }
#button:focus   { box-shadow: 0 0 0 3px #3b82f6; }
#button:disabled { opacity: 0.5; }
#checkbox:checked { background: #3b82f6; }

Structural Pseudo-Classes

.item:first-child     { border-radius: 12px 12px 0 0; }
.item:last-child      { border-radius: 0 0 12px 12px; }
.item:only-child      { border-radius: 12px; }
.item:nth-child(2)    { background: #f0f0f0; }
.item:nth-last-child(1) { font-weight: bold; }
.item:first-of-type   { color: red; }
.item:last-of-type    { color: blue; }
.item:nth-of-type(3)  { opacity: 0.5; }
.item:only-of-type    { border: 2px solid green; }
:empty                { display: none; }
:root                 { --primary: #3b82f6; }

Functional Pseudo-Classes

:not(.hidden) { opacity: 1; }
:is(#card, .panel) { border-radius: 12px; }
:where(.btn, .link) { cursor: pointer; }

Pseudo-Elements

#input::placeholder { color: #64748b; }
#text::selection    { background: #3b82f6; }

Combinators

Chain selectors for precise targeting:

/* Child combinator — direct children only */
#parent > .child { padding: 8px; }

/* Descendant combinator — any depth */
#list .item { margin: 4px; }

/* Adjacent sibling — next element */
.trigger:hover + .target { opacity: 1; }

/* General sibling — any following sibling */
.trigger:hover ~ .item { background: #e0e0e0; }

Complex Selectors

Combine any of the above:

#card:hover > .title { color: #ffffff; }
#list .item:last-child { border-bottom: none; }
.icon-wrapper:hover #pause { fill: rgba(0, 0, 0, 0.7); }
#progress:hover #time-left { opacity: 1; }

Visual Properties

Background

Supports solid colors, gradients, and image URLs:

/* Solid colors */
#el { background: #3b82f6; }
#el { background: rgb(59, 130, 246); }
#el { background: rgba(255, 255, 255, 0.15); }

/* Linear gradient */
#el { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
#el { background: linear-gradient(to right, red, blue); }
#el { background: linear-gradient(to bottom right, #fff, #000); }

/* Radial gradient */
#el { background: radial-gradient(circle, red, blue); }
#el { background: radial-gradient(circle at 25% 75%, red, blue); }

/* Conic gradient */
#el { background: conic-gradient(from 45deg, red, yellow, green, blue, red); }

/* Background image */
#el { background: url("path/to/image.jpg"); }

Border Radius

#card { border-radius: 12px; }
#avatar { border-radius: 50px; }          /* Circle */
#card { border-radius: theme(radius-lg); } /* Theme token */

Corner Shape

Controls the shape of rounded corners using superellipse exponents. Instead of the standard circular arc, you can create beveled, squircle, scooped, or notched corners.

/* Uniform shape for all corners */
#card { corner-shape: 2; }                /* Squircle (smoother than circular) */
#card { corner-shape: 0; }                /* Bevel (straight diagonal cut) */
#card { corner-shape: -1; }               /* Scoop (concave inward curve) */

/* Functional syntax */
#card { corner-shape: superellipse(2); }   /* Same as corner-shape: 2 */

Value reference:

ValueShapeDescription
1RoundStandard circular arc (default)
0BevelStraight diagonal cut
2SquircleSmoother than circular (iOS-style)
-1ScoopConcave inward curve
100SquareSharp corner (ignores border-radius)
-100NotchSharp 90-degree inward notch

Rust builder equivalents:

#![allow(unused)]
fn main() {
div().corner_shape(2.0)                                   // Uniform squircle
div().corner_shapes(0.0, 2.0, 2.0, 0.0)                  // Per-corner: bevel TL, squircle others
div().corner_squircle()                                    // Preset: squircle
div().corner_bevel()                                       // Preset: bevel
div().corner_scoop()                                       // Preset: scoop
}

Corner shape is animatable via transitions and keyframes:

#card {
    border-radius: 20px;
    corner-shape: 1;
    transition: corner-shape 0.3s ease;
}
#card:hover {
    corner-shape: 2;   /* Morph from round to squircle on hover */
}

Border

#el { border-width: 2px; border-color: #3b82f6; }
#el { border-width: 1.5px; border-color: rgba(255, 255, 255, 0.5); }

Box Shadow

#card { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); }
#glow { box-shadow: 0 0 40px rgba(139, 92, 246, 0.7); }
#card { box-shadow: none; }

Text Shadow

#heading { text-shadow: 3px 3px 0px rgba(255, 68, 68, 1.0); }

Outline

#el { outline: 3px solid #f59e0b; }
#el { outline-offset: 6px; }
#el { outline-width: 2px; outline-color: #ef4444; }

Opacity

#el { opacity: 0.75; }

Visibility

#el { visibility: hidden; }

Z-Index & Render Layer

#overlay { z-index: 10; }
#el { render-layer: foreground; }  /* foreground | background | glass */

Layout Properties

All standard flexbox layout properties can be set from CSS:

Sizing

#card {
    width: 380px;
    height: 200px;
    min-width: 100px;
    max-width: 600px;
}

/* Percentage values */
#full { width: 100%; }

/* Auto sizing */
#auto { width: auto; }

Spacing

#card {
    padding: 24px;
    padding: 6px 8px;          /* vertical horizontal */
    padding: 8px 12px 16px;    /* top horizontal bottom */
    padding: 8px 12px 16px 4px; /* top right bottom left */
    margin: 16px;
    gap: 20px;
}

Flexbox

#container {
    display: flex;
    flex-direction: row;        /* row | column | row-reverse | column-reverse */
    flex-wrap: wrap;            /* wrap | nowrap */
    align-items: center;        /* center | start | end | stretch | baseline */
    justify-content: space-between; /* center | start | end | space-between | space-around | space-evenly */
    gap: 16px;
}

#item {
    flex-grow: 1;
    flex-shrink: 0;
    align-self: center;
}

Positioning

#el {
    position: absolute;  /* static | relative | absolute | fixed | sticky */
    top: 10px;
    right: 0;
    bottom: 0;
    left: 10px;
    inset: 0;           /* shorthand for all four */
}

Overflow

#scroll { overflow: scroll; }
#clip   { overflow: clip; }
#el     { overflow-x: scroll; overflow-y: hidden; }

Overflow Fade

Replaces the hard clip at overflow boundaries with a smooth fade-to-transparent ramp. Each edge can have an independent fade distance in pixels.

/* Uniform: all 4 edges fade over 24px */
#scroll { overflow-fade: 24px; }

/* Vertical + horizontal: top/bottom 32px, left/right 0 */
#scroll { overflow-fade: 32px 0px; }

/* Per-edge: top, right, bottom, left */
#scroll { overflow-fade: 24px 0px 24px 0px; }

Rust builder equivalents:

#![allow(unused)]
fn main() {
div().overflow_fade(24.0)                              // Uniform
div().overflow_fade_y(32.0)                            // Vertical only (top + bottom)
div().overflow_fade_x(16.0)                            // Horizontal only (left + right)
div().overflow_fade_edges(24.0, 0.0, 24.0, 0.0)       // Per-edge: top, right, bottom, left
}

Overflow fade is animatable — combine with transitions for hover-triggered soft edges:

#container {
    overflow: clip;
    overflow-fade: 0px;
    transition: overflow-fade 0.3s ease;
}
#container:hover {
    overflow-fade: 32px;
}

Display

#hidden { display: none; }
#flex   { display: flex; }
#block  { display: block; }

Text & Typography

#text {
    color: #ffffff;
    font-size: 20px;
    font-weight: 700;               /* 100-900 or thin/light/normal/bold/black */
    line-height: 1.5;
    letter-spacing: 0.5px;
    text-align: center;              /* left | center | right */
    text-decoration: underline;      /* none | underline | line-through */
    text-decoration-color: #ff0000;
    text-decoration-thickness: 2px;
    text-overflow: ellipsis;         /* clip | ellipsis */
    white-space: nowrap;             /* normal | nowrap | pre | pre-wrap */
}

Transforms

2D Transforms

#el { transform: rotate(15deg); }
#el { transform: scale(1.15); }
#el { transform: scale(1.5, 0.8); }        /* non-uniform */
#el { transform: translate(10px, 20px); }
#el { transform: translateX(10px); }
#el { transform: translateY(20px); }
#el { transform: skewX(-8deg); }
#el { transform: skewY(12deg); }
#el { transform: skew(10deg, 5deg); }

Compound Transforms

Chain multiple transforms in a single property:

#el { transform: rotate(15deg) scale(1.15); }
#el { transform: scale(0.9) translateY(20px); }

Transform Origin

#el { transform-origin: center; }
#el { transform-origin: left top; }
#el { transform-origin: right bottom; }
#el { transform-origin: 50% 50%; }

Transitions

Smoothly animate property changes on state transitions (e.g., hover):

#card {
    background: #1e293b;
    transform: scale(1.0);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
    transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease;
}
#card:hover {
    transform: scale(1.05);
    box-shadow: 0 8px 32px rgba(59, 130, 246, 0.5);
    background: #334155;
}

Transition Syntax

/* Shorthand */
transition: property duration timing-function delay;

/* Multiple properties */
transition: transform 0.3s ease, opacity 0.3s ease, box-shadow 0.25s ease;

/* All properties */
transition: all 0.3s ease;

/* Individual properties */
transition-property: transform;
transition-duration: 300ms;
transition-timing-function: ease-in-out;
transition-delay: 100ms;

Animatable Properties

Almost every visual and layout property can be transitioned:

  • Visual: opacity, background, border-color, border-width, border-radius, corner-shape, box-shadow, text-shadow, outline-color, outline-width
  • Transform: transform (rotate, scale, translate, skew)
  • Layout: width, height, padding, margin, gap, min-width, max-width, min-height, max-height, top, left, flex-grow
  • Filters: filter, backdrop-filter
  • Clip & Overflow: clip-path, overflow-fade
  • SVG: fill, stroke, stroke-width, stroke-dashoffset
  • Mask: mask-image
  • 3D: rotate-x, rotate-y, perspective, translate-z

Timing Functions

FunctionDescription
easeSlow start and end (default)
linearConstant speed
ease-inSlow start
ease-outSlow end
ease-in-outSlow start and end

Layout Transitions

Layout properties animate with automatic layout recalculation:

#panel {
    width: 120px;
    height: 60px;
    padding: 8px;
    transition: width 0.4s ease, height 0.4s ease, padding 0.3s ease;
}
#panel:hover {
    width: 280px;
    height: 120px;
    padding: 24px;
}

Animations

@keyframes

Define multi-step animations:

@keyframes fade-in {
    from { opacity: 0; transform: translateY(20px); }
    to   { opacity: 1; transform: translateY(0); }
}

@keyframes pulse {
    0%, 100% { opacity: 1; transform: scale(1); }
    50%      { opacity: 0.7; transform: scale(1.05); }
}

@keyframes gradient-cycle {
    0%   { background: linear-gradient(90deg, #d0d0d0, #e0e0e0, #ffffff); }
    33%  { background: linear-gradient(90deg, #d0d0d0, #ffffff, #d0d0d0); }
    66%  { background: linear-gradient(90deg, #ffffff, #e0e0e0, #d0d0d0); }
    100% { background: linear-gradient(90deg, #d0d0d0, #e0e0e0, #ffffff); }
}

Animation Property

/* Shorthand */
#el { animation: pulse 2s ease-in-out infinite; }

/* Full shorthand */
#el { animation: slide-in 300ms ease-out 100ms 1 normal forwards; }
/*               name     duration timing  delay count direction fill-mode */

/* Individual properties */
#el {
    animation-name: pulse;
    animation-duration: 2s;
    animation-timing-function: ease-in-out;
    animation-delay: 100ms;
    animation-iteration-count: infinite;    /* or a number */
    animation-direction: alternate;         /* normal | reverse | alternate | alternate-reverse */
    animation-fill-mode: forwards;          /* none | forwards | backwards | both */
}

Animatable Keyframe Properties

All these properties can be used inside @keyframes:

  • opacity, transform, background, border-color, border-width, border-radius
  • box-shadow, text-shadow, outline, color, font-size
  • width, height, padding, margin, gap, min-width, max-width
  • filter (blur, brightness, contrast, etc.)
  • backdrop-filter
  • clip-path
  • fill, stroke, stroke-width, stroke-dasharray, stroke-dashoffset
  • d (SVG path morphing)
  • rotate-x, rotate-y, perspective, translate-z
  • light-direction, light-intensity, ambient, specular

Automatic Animation

Elements with an animation property in the stylesheet start animating automatically:

@keyframes card-enter {
    from { opacity: 0; transform: scale(0.95); }
    to   { opacity: 1; transform: scale(1); }
}

#card { animation: card-enter 300ms ease-out; }
#![allow(unused)]
fn main() {
div().id("card").child(content())  // Animates on first render!
}

Filters

Apply visual effects to elements:

#el { filter: grayscale(100%); }
#el { filter: sepia(100%); }
#el { filter: invert(100%); }
#el { filter: brightness(150%); }
#el { filter: contrast(200%); }
#el { filter: saturate(300%); }
#el { filter: hue-rotate(90deg); }
#el { filter: blur(4px); }

/* Combined filters */
#el { filter: grayscale(50%) brightness(120%) contrast(110%); }

/* Filter transitions */
#el {
    filter: blur(0px);
    transition: filter 0.4s ease;
}
#el:hover {
    filter: blur(8px);
}

Filter Animation

@keyframes blur-pulse {
    0%   { filter: blur(0px); }
    50%  { filter: blur(6px); }
    100% { filter: blur(0px); }
}
#el { animation: blur-pulse 3s ease-in-out infinite; }

Backdrop Filters & Glass Effects

Apply effects to the area behind an element — essential for glassmorphism:

/* Simple blur */
#panel { backdrop-filter: blur(12px); }

/* Combined */
#panel { backdrop-filter: blur(12px) saturate(180%) brightness(80%); }

/* Named materials */
#glass { backdrop-filter: glass; }
#metal { backdrop-filter: metallic; }
#chrome { backdrop-filter: chrome; }
#gold  { backdrop-filter: gold; }
#wood  { backdrop-filter: wood; }

/* Liquid glass (refracted bevel borders) */
#card {
    backdrop-filter: liquid-glass(
        blur(18px)
        saturate(180%)
        brightness(120%)
        border(4.0)
        tint(rgba(255, 255, 255, 1.0))
    );
}

Backdrop Filter Transitions

#panel {
    backdrop-filter: blur(4px);
    transition: backdrop-filter 0.4s ease;
}
#panel:hover {
    backdrop-filter: blur(20px) saturate(180%);
}

Clip Path

Clip elements to geometric shapes:

/* Circle */
#el { clip-path: circle(50% at 50% 50%); }
#el { clip-path: circle(40px at center); }

/* Ellipse */
#el { clip-path: ellipse(50% 35% at 50% 50%); }

/* Inset rectangle */
#el { clip-path: inset(10% 10% 10% 10% round 12px); }

/* Rect / XYWH */
#el { clip-path: rect(10px 90px 90px 10px round 8px); }
#el { clip-path: xywh(10px 10px 80px 80px round 8px); }

/* Polygon */
#hexagon {
    clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
}
#star {
    clip-path: polygon(
        50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%,
        50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%
    );
}

/* SVG Path */
#el { clip-path: path("M 10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80"); }

Clip Path Animation

@keyframes clip-reveal {
    from { clip-path: inset(0% 50% 100% 50%); }
    to   { clip-path: inset(0% 0% 0% 0%); }
}
#el { animation: clip-reveal 400ms ease-out; }

Mask Image

Apply gradient masks to fade or reveal parts of an element:

/* Linear gradient masks */
#el { mask-image: linear-gradient(to bottom, black, transparent); }
#el { mask-image: linear-gradient(to right, black, transparent); }
#el { mask-image: linear-gradient(135deg, black 0%, transparent 100%); }

/* Radial gradient masks */
#el { mask-image: radial-gradient(circle, black, transparent); }

/* URL-based masks (image texture) */
#el { mask-image: url("mask.png"); }

/* Mask mode */
#el { mask-mode: alpha; }      /* default */
#el { mask-mode: luminance; }

Mask Transitions

#reveal {
    mask-image: linear-gradient(to bottom, black, transparent);
    transition: mask-image 0.6s ease;
}
#reveal:hover {
    mask-image: linear-gradient(to bottom, black, black);
}

#radial {
    mask-image: radial-gradient(circle, black, transparent);
    transition: mask-image 0.5s ease;
}
#radial:hover {
    mask-image: radial-gradient(circle, black, black);
}

SVG Styling

Style SVG elements using CSS — including fills, strokes, and path animations.

SVG Properties

svg { stroke: #ffffff; fill: none; stroke-width: 2.5; }

#icon {
    fill: #6366f1;
    stroke: #ffffff;
    stroke-width: 2;
    stroke-dasharray: 251;
    stroke-dashoffset: 0;
}

SVG Tag-Name Selectors

Target specific SVG sub-element types within a parent:

#my-svg path   { stroke: #8b5cf6; stroke-width: 5; }
#my-svg circle { fill: #f3e8ff; stroke: #a78bfa; }
#my-svg rect   { fill: #fef3c7; stroke: #f59e0b; }

Supported tags: path, circle, rect, ellipse, line, polygon, polyline, g.

SVG Fill & Stroke Animation

@keyframes color-cycle {
    0%   { fill: #ef4444; stroke: #dc2626; }
    33%  { fill: #3b82f6; stroke: #2563eb; }
    66%  { fill: #10b981; stroke: #059669; }
    100% { fill: #ef4444; stroke: #dc2626; }
}
#icon { animation: color-cycle 4s ease-in-out infinite; }

@keyframes glow-stroke {
    0%   { stroke: #fbbf24; stroke-width: 2; }
    50%  { stroke: #f43f5e; stroke-width: 5; }
    100% { stroke: #fbbf24; stroke-width: 2; }
}
#icon { animation: glow-stroke 2s ease-in-out infinite; }

SVG Hover Transitions

#icon {
    fill: #6366f1;
    transition: fill 0.3s ease;
}
#icon:hover { fill: #f43f5e; }

#icon2 {
    stroke: #64748b;
    stroke-width: 2;
    transition: stroke 0.3s ease, stroke-width 0.3s ease;
}
#icon2:hover { stroke: #f59e0b; stroke-width: 5; }

Line Drawing Effect

Animate stroke-dashoffset to create a “drawing” effect:

#draw-svg {
    stroke-dasharray: 251;
    animation: draw 3s ease-in-out infinite alternate;
}
@keyframes draw {
    from { stroke-dashoffset: 251; }
    to   { stroke-dashoffset: 0; }
}

SVG Path Morphing

Animate the d property to morph between shapes. Both shapes must have the same number of path segments:

@keyframes morph {
    0%   { d: path("M20,20 L80,20 L80,80 L50,80 L20,80 Z"); }
    50%  { d: path("M50,10 L90,40 L75,85 L25,85 L10,40 Z"); }
    100% { d: path("M20,20 L80,20 L80,80 L50,80 L20,80 Z"); }
}
#morph-svg { animation: morph 3s ease-in-out infinite; }

This enables complex effects like hamburger-to-X menu icon animations:

@keyframes hamburger-to-x {
    0%   { d: path("M20,30 L80,30 M20,50 L80,50 M20,70 L80,70"); }
    100% { d: path("M26,26 L74,74 M50,50 L50,50 M26,74 L74,26"); }
}
#menu-icon { animation: hamburger-to-x 1.5s ease-in-out infinite alternate; }

3D Shapes & Lighting

Blinc can render 3D SDF shapes directly via CSS — no mesh files needed.

3D Shape Properties

#sphere {
    shape-3d: sphere;       /* box | sphere | cylinder | torus | capsule */
    depth: 120px;
    perspective: 800px;
    rotate-x: 30deg;
    rotate-y: 45deg;
    background: linear-gradient(45deg, #4488ff, #ff4488);  /* UV-mapped onto surface */
}

3D Lighting

#lit-shape {
    shape-3d: box;
    depth: 80px;
    perspective: 800px;
    light-direction: (0.0, -1.0, 0.5);  /* x, y, z */
    light-intensity: 1.5;
    ambient: 0.3;
    specular: 64.0;
    translate-z: 20px;
}

3D Boolean Operations (Group Composition)

Combine multiple 3D shapes with boolean operations:

/* Parent must be a group */
#compound { shape-3d: group; perspective: 800px; depth: 80px; }

/* Children contribute shapes */
#base-shape {
    shape-3d: box;
    depth: 80px;
    3d-op: union;
}
#hole {
    shape-3d: cylinder;
    depth: 120px;
    3d-op: subtract;
    3d-blend: 30px;     /* Smooth blend radius */
}

Available operations: union, subtract, intersect, smooth-union, smooth-subtract, smooth-intersect.

3D Animation

@keyframes spin-y {
    from { rotate-y: 0deg; }
    to   { rotate-y: 360deg; }
}
#rotating-shape {
    shape-3d: sphere;
    depth: 120px;
    perspective: 800px;
    animation: spin-y 4s linear infinite;
}

CSS Variables

Define reusable values with custom properties:

:root {
    --brand-color: #3b82f6;
    --card-radius: 12px;
    --hover-opacity: 0.85;
}

#card {
    background: var(--brand-color);
    border-radius: var(--card-radius);
}

#card:hover {
    opacity: var(--hover-opacity);
}

Fallback Values

#el { background: var(--undefined-color, #333); }

Accessing Variables in Rust

#![allow(unused)]
fn main() {
if let Some(value) = stylesheet.get_variable("brand-color") {
    println!("Brand color: {}", value);
}
}

Theme Integration

The theme() function references design tokens that adapt to the current app theme:

#card {
    background: theme(surface);
    border-radius: theme(radius-lg);
    box-shadow: theme(shadow-md);
    color: theme(text-primary);
    border-color: theme(border);
}

#button {
    background: theme(primary);
}
#button:hover {
    background: theme(primary-hover);
}

Available Theme Tokens

Colors:

TokenDescription
primary, primary-hover, primary-activePrimary brand colors
secondary, secondary-hover, secondary-activeSecondary colors
success, success-bgSuccess states
warning, warning-bgWarning states
error, error-bgError states
info, info-bgInfo states
background, surface, surface-elevated, surface-overlayBackground surfaces
text-primary, text-secondary, text-tertiary, text-inverse, text-linkText colors
border, border-secondary, border-hover, border-focus, border-errorBorders

Radii: radius-none, radius-sm, radius-default, radius-md, radius-lg, radius-xl, radius-2xl, radius-3xl, radius-full

Shadows: shadow-none, shadow-sm, shadow-default, shadow-md, shadow-lg, shadow-xl


Form Styling

Inputs, checkboxes, radio buttons, and textareas are all styleable via CSS:

Text Input

#my-input {
    border-color: #3b82f6;
    border-width: 2px;
    border-radius: 8px;
    color: #ffffff;
    caret-color: #60a5fa;
}
#my-input::placeholder {
    color: #64748b;
}
#my-input:hover {
    border-color: #60a5fa;
}
#my-input:focus {
    border-color: #93c5fd;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}

Checkbox & Radio

#my-checkbox {
    accent-color: #3b82f6;        /* Checkmark / dot color */
    border-color: #475569;
    border-radius: 4px;
}
#my-checkbox:hover {
    border-color: #3b82f6;
}
#my-checkbox:checked {
    background: #3b82f6;
    border-color: #3b82f6;
}

Scrollbar

#scrollable {
    scrollbar-color: #888 #333;   /* thumb-color track-color */
    scrollbar-width: thin;         /* auto | thin | none */
}

Object Fit & Position

Control how images fill their container:

#image-container img {
    object-fit: cover;             /* cover | contain | fill | scale-down | none */
    object-position: 60% 40%;     /* x% y% */
}

Interaction Properties

#overlay { pointer-events: none; }  /* auto | none */
#link    { cursor: pointer; }       /* default | pointer | text | move | not-allowed | grab | ... */
#blend   { mix-blend-mode: overlay; } /* normal | multiply | screen | overlay | ... */

Length Units

UnitDescriptionExample
pxPixels (default)12px
%Percentage of parent50%
spSpacing units (1sp = 4px)4sp = 16px
degDegrees (angles)45deg
turnFull turns (angles)0.25turn = 90deg
radRadians (angles)1.5708rad ≈ 90deg
msMilliseconds (time)300ms
sSeconds (time)0.3s

Calc Expressions & Math Functions

Blinc supports CSS calc() with full arithmetic, standard CSS math functions, and shader-inspired extensions. Any property that accepts a numeric value can use calc().

Basic Arithmetic

Standard +, -, *, / with mixed units:

#panel {
    width: calc(100% - 40px);
    padding: calc(2 * 8px);
    margin: calc(100% / 3);
    height: calc(50vh - 20px);
}

Units in Calc

All length units from the table above work inside calc():

width: calc(100vw - 300px);
height: calc(50vh - 2em);
rotate: calc(45deg + 0.25turn);
transition-duration: calc(200ms + 0.1s);

Multiply unitless values by a unit literal to produce a dimension:

/* Unitless env var → px */
border-radius: calc(mix(4, 48, 0.5) * 1px);

/* Unitless → degrees */
rotate: calc(env(pointer-x) * 25deg);

CSS Standard Functions

FunctionSyntaxDescription
minmin(a, b)Smaller of two values
maxmax(a, b)Larger of two values
clampclamp(min, val, max)Constrain value to range
#card {
    width: clamp(200px, 50%, 600px);
    font-size: min(2em, 24px);
    padding: max(8px, 2%);
}

Blinc Extension Functions

These shader-inspired functions are available inside calc() for expressive, animation-friendly math:

mix(a, b, t) — Linear Interpolation

Returns a + (b - a) * t. When t = 0 returns a, when t = 1 returns b.

/* Opacity: 30% at t=0, 100% at t=1 */
opacity: calc(mix(0.3, 1.0, env(pointer-inside)));

/* Border-radius between 4px and 48px */
border-radius: calc(mix(4, 48, env(pointer-inside)) * 1px);

smoothstep(edge0, edge1, x) — Hermite Interpolation

Returns a smooth 0→1 S-curve. Result is 0 when x <= edge0, 1 when x >= edge1, and smoothly interpolated between. Uses the Hermite formula: t*t*(3 - 2*t).

When edge0 > edge1, the curve inverts — useful for proximity effects (1 when close, 0 when far):

/* Fade in as pointer approaches (distance 1.8→0 maps to opacity 0→1) */
opacity: calc(smoothstep(1.8, 0.0, env(pointer-distance)));

/* Sharp threshold at 0.5 (smoother than step) */
opacity: calc(smoothstep(0.4, 0.6, env(pointer-inside)));

step(edge, x) — Hard Threshold

Returns 0 if x < edge, 1 otherwise. Binary on/off switch:

/* Fully visible or fully hidden */
opacity: calc(step(0.5, env(pointer-inside)));

remap(val, in_lo, in_hi, out_lo, out_hi) — Range Mapping

Linearly maps val from one range to another:

/* Map pointer-x from [-1, 1] to [10, 50] for border-radius */
border-radius: calc(remap(env(pointer-x), -1, 1, 10, 50) * 1px);

Environment Variables

env() references resolve to per-frame dynamic values. Currently used by the pointer query system:

#card {
    pointer-space: self;
    rotate-y: calc(env(pointer-x) * 25deg);
    opacity: calc(mix(0.3, 1.0, env(pointer-inside)));
}

See the Pointer Query chapter for the full list of env(pointer-*) variables.

Percentage Values

% in calc resolves against the parent dimension (width for horizontal properties, height for vertical):

width: calc(50% - 20px);
margin-left: calc(100% / 6);

Error Handling

The CSS parser is resilient — it collects errors without stopping:

#![allow(unused)]
fn main() {
let result = Stylesheet::parse_with_errors(css);

if result.has_errors() {
    result.print_colored_diagnostics();  // Pretty-printed terminal output
    result.print_summary();
}

// Valid properties are still parsed!
let style = result.stylesheet.get("card").unwrap();
}

Individual errors include line/column information:

#![allow(unused)]
fn main() {
for error in &result.errors {
    println!("Line {}, Col {}: {} (property: {:?})",
        error.line, error.column, error.message, error.property);
}
}

Scoped Style Macros

Blinc provides two compile-time macros — css! and style! — for building ElementStyle values directly in Rust. These are ideal for programmatic, scoped styling where you need dynamic values, conditional logic, or simply don’t want a global stylesheet.

Both macros produce the same ElementStyle type and support all the same properties. The difference is syntax:

css!style!
NamingCSS hyphens (border-radius)Rust underscores (rounded)
SeparatorSemicolonsCommas
Enum valuesLiteral keywords (position: absolute;)Rust expressions (position: StylePosition::Absolute)
Best forDevelopers from CSS/webRust-native code

Quick Example

#![allow(unused)]
fn main() {
use blinc_layout::prelude::*;
use blinc_core::Color;

// CSS-style syntax
let card = css! {
    background: Color::WHITE;
    border-radius: 12.0;
    box-shadow: lg;
    opacity: 0.95;
    padding: 24.0;
};

// Equivalent Rust-style syntax
let card = style! {
    bg: Color::WHITE,
    rounded: 12.0,
    shadow_lg,
    opacity: 0.95,
    p: 24.0,
};
}

Apply a macro style to a Div with .style():

#![allow(unused)]
fn main() {
div().style(css! {
    background: Color::rgb(0.1, 0.1, 0.15);
    border-radius: 16.0;
    padding: 20.0;
})
}

Visual Properties

#![allow(unused)]
fn main() {
// css! macro
let s = css! {
    background: Color::BLUE;
    border-radius: 8.0;
    box-shadow: md;                    // Presets: sm, md, lg, xl, none
    box-shadow: my_shadow;             // Or a Shadow value
    opacity: 0.8;
    clip-path: my_clip_path;           // ClipPath value
    filter: my_filter;                 // CssFilter value
    mask-image: my_mask;               // MaskImage value
    mask-mode: blinc_core::MaskMode::Alpha;
    mix-blend-mode: blinc_core::BlendMode::Overlay;
};

// style! macro
let s = style! {
    bg: Color::BLUE,
    rounded: 8.0,
    shadow_md,                         // Presets as bare keywords
    opacity: 0.8,
    clip_path: my_clip_path,
    filter: my_filter,
    mask_image: my_mask,
    mask_gradient: my_gradient,        // Gradient mask shorthand
    mask_mode: blinc_core::MaskMode::Alpha,
    mix_blend_mode: blinc_core::BlendMode::Overlay,
};
}

Corner Radius Presets (style! only)

#![allow(unused)]
fn main() {
let s = style! {
    rounded_sm,     // 2.0
    rounded_md,     // 6.0
    rounded_lg,     // 8.0
    rounded_xl,     // 12.0
    rounded_2xl,    // 16.0
    rounded_full,   // 9999.0 (pill shape)
    rounded_none,   // 0.0
};

// Per-corner control
let s = style! {
    rounded_corners: (12.0, 12.0, 0.0, 0.0),  // top-left, top-right, bottom-right, bottom-left
};
}

Shadow Presets (style! only)

#![allow(unused)]
fn main() {
let s = style! { shadow_sm };   // Small, subtle shadow
let s = style! { shadow_md };   // Medium (default card shadow)
let s = style! { shadow_lg };   // Large, elevated
let s = style! { shadow_xl };   // Extra large, floating
let s = style! { shadow_none }; // Remove shadow
}

Opacity Presets (style! only)

#![allow(unused)]
fn main() {
let s = style! { opaque };      // 1.0
let s = style! { translucent }; // 0.5
let s = style! { transparent }; // 0.0
}

Text Properties

#![allow(unused)]
fn main() {
// css! macro
let s = css! {
    color: Color::WHITE;
    font-size: 16.0;
    font-weight: FontWeight::Bold;
    text-decoration: TextDecoration::Underline;
    text-decoration-color: Color::RED;
    text-decoration-thickness: 2.0;
    line-height: 1.5;
    text-align: center;                // Keywords: left, center, right
    letter-spacing: 0.5;
    text-shadow: my_shadow;
    text-overflow: ellipsis;           // Keywords: clip, ellipsis
    white-space: nowrap;               // Keywords: normal, nowrap, pre
};

// style! macro
let s = style! {
    text_color: Color::WHITE,
    font_size: 16.0,
    font_weight: FontWeight::Bold,
    text_decoration: TextDecoration::Underline,
    text_decoration_color: Color::RED,
    text_decoration_thickness: 2.0,
    line_height: 1.5,
    text_align: TextAlign::Center,
    letter_spacing: 0.5,
    text_shadow: my_shadow,
    text_overflow: TextOverflow::Ellipsis,
    white_space: WhiteSpace::Nowrap,
};
}

Transforms

#![allow(unused)]
fn main() {
// css! macro — function syntax
let s = css! {
    transform: scale(1.05);
    transform: scale(1.5, 0.8);       // Non-uniform
    transform: translate(10.0, 20.0);
    transform: rotate(45.0);
    transform: skewX(15.0);
    transform: skewY(10.0);
    transform-origin: (50.0, 50.0);   // Percentages
};

// css! macro — expression syntax
let s = css! {
    transform: my_transform;          // A Transform value
};

// style! macro — dedicated properties
let s = style! {
    scale: 1.05,
    scale_xy: (1.5, 0.8),
    translate: (10.0, 20.0),
    rotate_deg: 45.0,
    skew_x: 15.0,
    skew_y: 10.0,
    transform_origin: (50.0, 50.0),
};
}

3D Properties

#![allow(unused)]
fn main() {
// css! macro
let s = css! {
    rotate-x: 30.0;
    rotate-y: 45.0;
    perspective: 800.0;
    translate-z: 20.0;
    shape-3d: "sphere";
    depth: 120.0;
    light-direction: (0.0, -1.0, 0.5);
    light-intensity: 1.5;
    ambient: 0.3;
    specular: 64.0;
    3d-op: "subtract";
    3d-blend: 30.0;
};

// style! macro
let s = style! {
    rotate_x: 30.0,
    rotate_y: 45.0,
    perspective: 800.0,
    translate_z: 20.0,
    shape_3d: "sphere",
    depth: 120.0,
    light_direction: (0.0, -1.0, 0.5),
    light_intensity: 1.5,
    ambient: 0.3,
    specular: 64.0,
    op_3d: "subtract",
    blend_3d: 30.0,
};
}

Layout Properties

#![allow(unused)]
fn main() {
// css! macro
let s = css! {
    width: 300.0;
    height: 200.0;
    min-width: 100.0;
    max-width: 600.0;
    padding: 24.0;
    margin: 16.0;
    gap: 12.0;
    display: flex;                     // flex | block | none
    flex-direction: column;            // row | column | row-reverse | column-reverse
    flex-wrap: wrap;
    flex-grow: 1.0;
    flex-shrink: 0.0;
    align-items: center;              // center | start | end | stretch | baseline
    justify-content: space-between;   // center | start | end | space-between | space-around | space-evenly
    align-self: end;                  // center | start | end | stretch | baseline
    overflow: clip;                    // clip | hidden | visible | scroll
    overflow-x: scroll;
    overflow-y: hidden;
};

// style! macro
let s = style! {
    w: 300.0,
    h: 200.0,
    min_w: 100.0,
    max_w: 600.0,
    p: 24.0,
    p_xy: (16.0, 24.0),               // Horizontal, vertical
    m: 16.0,
    m_xy: (8.0, 16.0),
    gap: 12.0,
    flex_col,                          // Bare keyword presets
    flex_wrap,
    flex_grow,                         // Default = 1.0
    flex_grow_value: 2.0,              // Specific value
    flex_shrink_0,                     // flex-shrink: 0
    flex_shrink: 0.5,                  // Specific value
    items_center,
    justify_between,
    self_end,
    overflow_clip,
    overflow_x: StyleOverflow::Scroll,
    overflow_y: StyleOverflow::Clip,
    display_none,
    display_block,
};
}

Position & Inset

#![allow(unused)]
fn main() {
// css! macro — keyword values
let s = css! {
    position: absolute;               // static | relative | absolute | fixed | sticky
    top: 10.0;
    right: 0.0;
    bottom: 0.0;
    left: 10.0;
    inset: 0.0;                       // Sets all four sides
    z-index: 5;
    visibility: hidden;               // visible | hidden
};

// style! macro — expression values
let s = style! {
    position: StylePosition::Absolute,
    top: 10.0,
    inset: 0.0,
    z_index: 5,
    visibility: StyleVisibility::Hidden,
};
}

Border & Outline

#![allow(unused)]
fn main() {
// css! macro
let s = css! {
    border: (2.0, Color::RED);        // Shorthand (width, color)
    border-width: 2.0;
    border-color: Color::RED;
    outline: (3.0, Color::BLUE);
    outline-width: 3.0;
    outline-color: Color::BLUE;
    outline-offset: 4.0;
};

// style! macro
let s = style! {
    border: (2.0, Color::RED),
    border_width: 2.0,
    border_color: Color::RED,
    outline: (3.0, Color::BLUE),
    outline_width: 3.0,
    outline_color: Color::BLUE,
    outline_offset: 4.0,
};
}

Materials & Layers

#![allow(unused)]
fn main() {
// css! macro — keyword presets
let s = css! {
    backdrop-filter: glass;           // glass | metallic | chrome | gold | wood
    render-layer: foreground;         // foreground | background
};

// style! macro — bare keyword presets
let s = style! {
    glass,                            // Also: metallic, chrome, gold, wood
    foreground,
};

// Custom material via expression
let s = style! {
    material: my_material,
    layer: my_layer,
};
}

Animation & Transition

#![allow(unused)]
fn main() {
// css! macro
let s = css! {
    animation: my_animation;           // CssAnimation value
    animation-name: "pulse";
    animation-duration: 2000;          // milliseconds
    animation-delay: 100;
    animation-timing-function: AnimationTiming::EaseInOut;
    animation-iteration-count: 0;      // 0 = infinite
    animation-direction: AnimationDirection::Alternate;
    animation-fill-mode: AnimationFillMode::Forwards;
    transition: my_transition;         // CssTransitionSet value
};

// style! macro
let s = style! {
    animation: my_animation,
    animation_name: "pulse",
    animation_duration: 2000,
    transition: my_transition,
};
}

SVG Properties

#![allow(unused)]
fn main() {
// css! macro
let s = css! {
    fill: Color::RED;
    stroke: Color::BLUE;
    stroke-width: 2.0;
    stroke-dasharray: vec![5.0, 3.0];
    stroke-dashoffset: 10.0;
};

// style! macro
let s = style! {
    fill: Color::RED,
    stroke: Color::BLUE,
    stroke_width: 2.0,
    stroke_dasharray: vec![5.0, 3.0],
    stroke_dashoffset: 10.0,
    svg_path_data: "M10,80 L50,20 L90,80",
};
}

Form & Interaction Properties

#![allow(unused)]
fn main() {
// css! macro
let s = css! {
    caret-color: Color::rgb(0.4, 0.6, 1.0);
    selection-color: Color::BLUE;
    placeholder-color: Color::rgba(1.0, 1.0, 1.0, 0.5);
    accent-color: Color::GREEN;
    scrollbar-color: (Color::rgb(0.5, 0.5, 0.5), Color::rgb(0.2, 0.2, 0.2));
    scrollbar-width: thin;            // auto | thin | none
    pointer-events: none;             // auto | none
    cursor: CursorStyle::Pointer;
};

// style! macro
let s = style! {
    caret_color: Color::rgb(0.4, 0.6, 1.0),
    accent_color: Color::GREEN,
    scrollbar_color: (Color::rgb(0.5, 0.5, 0.5), Color::rgb(0.2, 0.2, 0.2)),
    scrollbar_width: ScrollbarWidth::Thin,
    pointer_events_none,              // Preset keyword
    cursor: CursorStyle::Pointer,
};
}

Image Properties

#![allow(unused)]
fn main() {
// css! macro (0=cover, 1=contain, 2=fill, 3=scale-down, 4=none)
let s = css! {
    object-fit: 1;
    object-position: (0.5, 0.0);     // x, y in 0.0-1.0 range
};

// style! macro
let s = style! {
    object_fit: 1,
    object_position: (0.5, 0.0),
};
}

Conditional & Dynamic Styling

The macros shine when combined with Rust control flow:

#![allow(unused)]
fn main() {
fn card_style(is_selected: bool, scale: f32) -> ElementStyle {
    let mut s = css! {
        background: Color::WHITE;
        border-radius: 12.0;
        padding: 16.0;
    };

    if is_selected {
        s = s.merge(&css! {
            border: (2.0, Color::BLUE);
            box-shadow: lg;
        });
    }

    // Dynamic transform
    s = s.scale(scale);
    s
}
}

When to Use Each Approach

ApproachBest For
Global CSS (ctx.add_css())Shared styles, hover/focus states, animations, selectors
css! / style! macrosScoped styles, dynamic values, conditional logic
Builder API (.w(), .bg())One-off overrides, inline on Div builders

The three approaches compose naturally — CSS provides base styles, macros add scoped overrides, and builder methods fine-tune individual elements.


How It Works

Understanding the CSS pipeline helps debug styling issues.

The Three Styling Approaches

Blinc offers three ways to style elements, in increasing specificity:

  1. Global stylesheetctx.add_css() + CSS selectors (recommended for most styling)
  2. Scoped macroscss! / style! macros for inline ElementStyle
  3. Builder API.w(), .h(), .bg() etc. for direct property setting

All three can be combined. CSS provides base styles; builder methods add dynamic values.

CSS Pipeline

CSS Text
  ↓  ctx.add_css() / Stylesheet::parse_with_errors()
Stylesheet (parsed selectors + ElementStyle rules)
  ↓  apply_stylesheet_base_styles()
RenderProps (GPU-ready properties on each element)
  ↓  State changes (hover, focus, checked)
  ↓  apply_stylesheet_state_styles()
  ↓  Transition/animation detection & interpolation
  ↓  apply_animated_layout_props() + compute_layout()
GPU Rendering (SDF shader, image shader, text pipeline)

Frame Loop Order

Each frame, CSS processing happens in this order:

  1. Tree build — Elements are created, RenderProps initialized
  2. Base styles — Non-state CSS rules applied (complex selectors first, then ID selectors for higher specificity)
  3. Layout overrides — CSS layout properties (width, padding, gap, etc.) modify the flexbox tree
  4. Layout computation — Flexbox layout calculated via Taffy
  5. State styles — Hover/focus/checked states matched, transitions detected
  6. Animation tick — CSS @keyframes animations advance
  7. Transition tick — CSS transitions interpolate toward target
  8. Layout animation — If animated properties affect layout, re-compute flexbox
  9. Render — Final RenderProps sent to GPU

Specificity

Rules follow CSS specificity, applied in order:

  1. Type/class/combinator selectors (lowest)
  2. ID selectors (highest)
  3. Later rules override earlier rules at the same specificity level
  4. State styles (:hover, etc.) layer on top of base styles

Property Comparison

The same property expressed across all three approaches:

Global CSScss! macrostyle! macroBuilder API
background: #3498db;background: Color::hex(0x3498db);bg: Color::hex(0x3498db),.bg(Color::hex(0x3498db))
border-radius: 8px;border-radius: 8.0;rounded: 8.0,.rounded(8.0)
transform: scale(1.02);transform: scale(1.02);scale: 1.02,.scale(1.02)
opacity: 0.8;opacity: 0.8;opacity: 0.8,.opacity(0.8)
width: 200px;width: 200.0;w: 200.0,.w(200.0)
padding: 16px;padding: 16.0;p: 16.0,.p(16.0)
gap: 12px;gap: 12.0;gap: 12.0,.gap(12.0)
flex-direction: column;flex-direction: column;flex_col,.flex_col()
color: #fff;color: Color::WHITE;text_color: Color::WHITE,.text_color(Color::WHITE)
font-size: 16px;font-size: 16.0;font_size: 16.0,.font_size(16.0)
position: absolute;position: absolute;position: StylePosition::Absolute,.position(StylePosition::Absolute)
fill: red;fill: Color::RED;fill: Color::RED,.fill(Color::RED)
pointer-events: none;pointer-events: none;pointer_events_none,.pointer_events_none()

All three approaches can be combined — CSS provides base styles, macros add scoped overrides, and builder methods fine-tune individual elements.