CSS Styling
Blinc includes a powerful CSS parser that allows you to define styles using familiar CSS syntax. This enables separation of concerns between layout code and visual styling.
Overview
The CSS system supports:
- ID-based selectors (
#element-id) - State modifiers (
:hover,:active,:focus,:disabled) - CSS custom properties (
:rootandvar()) - Keyframe animations (
@keyframes) - Automatic animation application via the
animation:property - Theme integration (
theme()function) - Length units (
px,sp,%) - Gradients (
linear-gradient,radial-gradient,conic-gradient)
Basic Usage
Parsing CSS
#![allow(unused)]
fn main() {
use blinc_layout::prelude::*;
let css = r#"
#card {
background: #3498db;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
"#;
let result = Stylesheet::parse_with_errors(css);
// Check for errors
if result.has_errors() {
result.print_colored_diagnostics();
}
let stylesheet = result.stylesheet;
}
Applying Styles to Elements
Attach the stylesheet to the RenderTree:
#![allow(unused)]
fn main() {
use std::sync::Arc;
// In your render tree setup
render_tree.set_stylesheet(Some(Arc::new(stylesheet)));
// Then use IDs on elements
div()
.id("card")
.child(text("Styled with CSS!"))
}
Supported Properties
Background
#element {
background: #ff5733; /* Hex color */
background: rgb(255, 87, 51); /* RGB */
background: rgba(255, 87, 51, 0.8); /* RGBA */
background: theme(primary); /* Theme token */
}
Gradients
CSS gradients are fully supported for the background property:
Linear Gradients
#element {
/* Angle-based (0deg = up, 90deg = right, 180deg = down) */
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
/* Direction keywords */
background: linear-gradient(to right, red, blue);
background: linear-gradient(to bottom right, #fff, #000);
/* Multiple color stops */
background: linear-gradient(90deg, red 0%, yellow 50%, green 100%);
/* Implied positions (evenly distributed) */
background: linear-gradient(to bottom, red, yellow, green);
/* Different angle units */
background: linear-gradient(0.25turn, red, blue); /* 90deg */
background: linear-gradient(1.5708rad, red, blue); /* ~90deg */
}
Radial Gradients
#element {
/* Simple circle from center */
background: radial-gradient(circle, red, blue);
/* With position */
background: radial-gradient(circle at center, red, blue);
background: radial-gradient(circle at 25% 75%, red, blue);
/* Ellipse shape */
background: radial-gradient(ellipse at center, red, blue);
/* Multiple color stops */
background: radial-gradient(circle, red 0%, yellow 50%, green 100%);
}
Conic Gradients
#element {
/* Simple color wheel */
background: conic-gradient(red, yellow, green, blue, red);
/* With starting angle */
background: conic-gradient(from 45deg, red, blue);
/* With position */
background: conic-gradient(at 25% 75%, red, blue);
/* Combined angle and position */
background: conic-gradient(from 90deg at center, red, blue);
}
Gradient Color Stops
Color stops can use any supported color format:
#element {
/* Hex colors with positions */
background: linear-gradient(to right, #667eea 0%, #764ba2 100%);
/* RGBA colors */
background: linear-gradient(45deg, rgba(255, 0, 0, 0.5), rgba(0, 0, 255, 0.8));
/* Named colors */
background: linear-gradient(to right, red, orange, yellow, green, blue);
/* Mixed formats */
background: linear-gradient(135deg, #ff0000, rgba(0, 255, 0, 0.5) 50%, blue);
}
Border Radius
#element {
border-radius: 8px; /* Uniform */
border-radius: theme(radius-lg); /* Theme token */
}
Box Shadow
#element {
box-shadow: 2px 4px 12px rgba(0, 0, 0, 0.3); /* x y blur color */
box-shadow: theme(shadow-md); /* Theme token */
box-shadow: none; /* Remove shadow */
}
Transform
#element {
transform: scale(1.02); /* Uniform scale */
transform: scale(1.5, 0.8); /* Non-uniform */
transform: translate(10px, 20px); /* Translation */
transform: translateX(10px); /* X only */
transform: translateY(20px); /* Y only */
transform: rotate(45deg); /* Rotation */
}
Opacity
#element {
opacity: 0.8;
}
Render Layer
#element {
render-layer: foreground; /* On top */
render-layer: background; /* Behind */
render-layer: glass; /* Glass layer */
}
Length Units
Blinc CSS supports three types of length units:
Pixels (px)
Raw pixel values. These are the default when no unit is specified.
#element {
border-radius: 8px;
box-shadow: 2px 4px 12px rgba(0, 0, 0, 0.3);
transform: translate(10px, 20px);
}
Spacing Units (sp)
Spacing units follow a 4px grid system, where 1sp = 4px. This helps maintain consistent spacing throughout your application.
#card {
border-radius: 2sp; /* 2 * 4 = 8px */
box-shadow: 1sp 2sp 4sp rgba(0,0,0,0.2); /* 4px 8px 16px */
transform: translate(4sp, 2sp); /* 16px, 8px */
}
Common sp values:
1sp= 4px2sp= 8px4sp= 16px6sp= 24px8sp= 32px
Percentages (%)
Percentages are supported in gradient color stops and position values.
#element {
/* Gradient color stops use percentages */
background: linear-gradient(to right, red 0%, blue 100%);
/* Radial/conic gradient positions */
background: radial-gradient(circle at 25% 75%, red, blue);
}
State Modifiers
Define different styles for interactive states:
#button {
background: theme(primary);
transform: scale(1.0);
}
#button:hover {
background: theme(primary-hover);
transform: scale(1.02);
}
#button:active {
transform: scale(0.98);
}
#button:focus {
box-shadow: 0 0 0 3px theme(primary);
}
#button:disabled {
opacity: 0.5;
}
Querying State Styles
#![allow(unused)]
fn main() {
// Get base style
let base = stylesheet.get("button");
// Get state-specific style
let hover = stylesheet.get_with_state("button", CssElementState::Hover);
let active = stylesheet.get_with_state("button", CssElementState::Active);
// Get all states at once
let (base, states) = stylesheet.get_all_states("button");
for (state, style) in states {
println!(":{} => {:?}", state, style.opacity);
}
}
CSS Variables
Define reusable values with custom properties:
:root {
--brand-color: #3498db;
--hover-opacity: 0.85;
--card-radius: 12px;
--spacing-md: 16px;
}
#card {
background: var(--brand-color);
border-radius: var(--card-radius);
opacity: 1.0;
}
#card:hover {
opacity: var(--hover-opacity);
}
Fallback Values
#element {
background: var(--undefined-color, #333); /* Uses fallback */
}
Accessing Variables Programmatically
#![allow(unused)]
fn main() {
// Get a variable value
if let Some(value) = stylesheet.get_variable("brand-color") {
println!("Brand color: {}", value);
}
// Iterate all variables
for name in stylesheet.variable_names() {
let value = stylesheet.get_variable(name).unwrap();
println!("--{}: {}", name, value);
}
}
Theme Integration
Use the theme() function to reference theme tokens:
#card {
background: theme(surface);
border-radius: theme(radius-lg);
box-shadow: theme(shadow-md);
}
#button {
background: theme(primary);
}
#button:hover {
background: theme(primary-hover);
}
Available Theme Tokens
Colors:
primary,primary-hover,primary-activesecondary,secondary-hover,secondary-activesuccess,success-bgwarning,warning-bgerror,error-bginfo,info-bgforeground,foreground-mutedbackground,surface,surface-hoverborder,border-muted
Radii:
radius-sm,radius-default,radius-mdradius-lg,radius-xl,radius-2xl
Shadows:
shadow-sm,shadow-default,shadow-mdshadow-lg,shadow-xl
Keyframe Animations
Define complex animations with @keyframes:
@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.8;
transform: scale(1.05);
}
}
Percentage Positions
@keyframes complex-animation {
0% { opacity: 0; }
25% { opacity: 0.5; transform: scale(1.1); }
50% { opacity: 1; }
75% { opacity: 0.5; transform: scale(0.9); }
100% { opacity: 1; transform: scale(1); }
}
Accessing Keyframes
#![allow(unused)]
fn main() {
// Get keyframes by name
if let Some(keyframes) = stylesheet.get_keyframes("fade-in") {
println!("Animation has {} stops", keyframes.keyframes.len());
for kf in &keyframes.keyframes {
println!(" {}%: opacity={:?}",
(kf.position * 100.0) as i32,
kf.style.opacity
);
}
}
}
Converting to Motion Animation
#![allow(unused)]
fn main() {
// Convert to MotionAnimation (for simple from/to animations)
let motion = keyframes.to_motion_animation(300, 200); // enter_ms, exit_ms
// Convert to MultiKeyframeAnimation (for complex multi-step animations)
let animation = keyframes.to_multi_keyframe_animation(1000, Easing::EaseInOut);
}
Animation Property
Apply animations to elements with the animation: property:
@keyframes slide-in {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
#modal {
animation: slide-in 300ms ease-out;
}
Animation Shorthand
#element {
/* animation: name duration timing-function delay iteration-count direction fill-mode */
animation: pulse 2s ease-in-out 100ms infinite alternate forwards;
}
Individual Properties
#element {
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 */
}
Automatic Application
When a stylesheet is attached to the RenderTree, elements with IDs automatically receive animations:
#![allow(unused)]
fn main() {
let css = r#"
@keyframes card-enter {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
#card {
animation: card-enter 300ms ease-out;
}
"#;
let stylesheet = Stylesheet::parse_with_errors(css).stylesheet;
render_tree.set_stylesheet(Some(Arc::new(stylesheet)));
// This element will automatically animate on render!
div()
.id("card")
.child(content())
}
Error Handling
The CSS parser collects errors without failing:
#![allow(unused)]
fn main() {
let css = r#"
#card {
background: red;
opacity: invalid; /* Error: invalid value */
unknown-prop: foo; /* Warning: unknown property */
}
"#;
let result = Stylesheet::parse_with_errors(css);
// Check for issues
if result.has_errors() {
println!("Has {} error(s)", result.errors_only().count());
}
if result.has_warnings() {
println!("Has {} warning(s)", result.warnings_only().count());
}
// Print colored diagnostics to console
result.print_colored_diagnostics();
result.print_summary();
// The valid properties are still parsed!
let style = result.stylesheet.get("card").unwrap();
assert!(style.background.is_some()); // "red" was parsed
}
Error Information
#![allow(unused)]
fn main() {
for error in &result.errors {
println!("Line {}, Column {}: {}",
error.line,
error.column,
error.message
);
if let Some(ref prop) = error.property {
println!(" Property: {}", prop);
}
if let Some(ref val) = error.value {
println!(" Value: {}", val);
}
}
}
Motion Container Integration
Use CSS keyframes with the Motion container:
#![allow(unused)]
fn main() {
let css = r#"
@keyframes modal-enter {
from { opacity: 0; transform: scale(0.9) translateY(20px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
"#;
let stylesheet = Stylesheet::parse_with_errors(css).stylesheet;
// Method 1: Using from_stylesheet
motion()
.from_stylesheet(&stylesheet, "modal-enter", 300, 200)
.child(modal_content())
// Method 2: Using keyframes_from_stylesheet for multi-step animations
motion()
.keyframes_from_stylesheet(&stylesheet, "pulse", 1000, Easing::EaseInOut)
.child(pulsing_element())
}
Complete Example
#![allow(unused)]
fn main() {
use blinc_layout::prelude::*;
use std::sync::Arc;
fn styled_app() -> impl ElementBuilder {
// Define styles
let css = r#"
:root {
--card-bg: theme(surface);
--card-radius: theme(radius-lg);
--brand-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
@keyframes fade-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
#app-container {
background: theme(background);
}
#card {
background: var(--card-bg);
border-radius: var(--card-radius);
box-shadow: theme(shadow-md);
animation: fade-in 300ms ease-out;
}
#card:hover {
box-shadow: theme(shadow-lg);
transform: translateY(-2px);
}
#gradient-card {
background: var(--brand-gradient);
border-radius: theme(radius-lg);
box-shadow: theme(shadow-md);
}
#gradient-card:hover {
background: linear-gradient(135deg, #7c8ff0 0%, #8b5cb8 100%);
transform: translateY(-2px);
}
#primary-button {
background: theme(primary);
border-radius: theme(radius-default);
}
#primary-button:hover {
background: theme(primary-hover);
transform: scale(1.02);
}
#primary-button:active {
transform: scale(0.98);
}
"#;
let result = Stylesheet::parse_with_errors(css);
if result.has_errors() {
result.print_colored_diagnostics();
}
// In real usage, attach to render_tree
// render_tree.set_stylesheet(Some(Arc::new(result.stylesheet)));
div()
.id("app-container")
.flex_col()
.p(24.0)
.gap(16.0)
.child(
div()
.id("card")
.p(16.0)
.child(text("Styled with CSS!"))
)
.child(
div()
.id("gradient-card")
.p(16.0)
.child(text("Gradient background!"))
)
.child(
button("Click me")
.id("primary-button")
)
}
}
Best Practices
- Use CSS variables for values you want to reuse or override
- Use theme tokens for colors that should respect the app’s theme
- Check for errors after parsing to catch typos and invalid values
- Keep animations short for UI transitions (150-400ms)
- Use state modifiers for hover/active effects instead of manual callbacks
- Prefer ID selectors (
#id) for precise targeting
Comparison with Builder API
| CSS | Builder API |
|---|---|
background: #3498db; | .bg(Color::hex("#3498db")) |
border-radius: 8px; | .rounded(8.0) |
transform: scale(1.02); | .scale(1.02) |
opacity: 0.8; | .opacity(0.8) |
box-shadow: theme(shadow-md); | .shadow_md() |
Both approaches can be combined - use CSS for base styles and the builder API for dynamic values.