CSS and Tailwind
Style Rust UIs with typed methods, compile-checked CSS, stylesheets or Tailwind v4 classes, all flowing through one browser-style cascade.
- Rust
Whatever way you like to style, it works here. Chain typed methods, write a CSS block, drop in a <style> sheet, or reach for Tailwind classes. They all feed one cascade, the same way a browser’s does, so you can mix them without surprises. And when you make a typo, the compiler usually catches it before you run anything.
Four ways to style, one cascade
// 1. gpui / Tailwind-style methodsdiv().flex().items_center().gap_2().px_3().py_1().rounded_md().bg(rgb(0x3a3a3a)) .text_color(white()).shadow_md().hover(|s| s.bg(rgb(0x4772b3)))
// 2. a CSS block, checked at compile time (a misspelled property doesn't compile)div().style(css!("border-left: 3px solid #4772b3; padding: 4px 8px"))
// 3. CSS built at run timediv().css("grid-template-columns", format!("repeat({n}, 1fr)")) .style(format!("width: {w}px"))
// 4. stylesheets, anywhere in the treestyle().child(".card:hover > h4 { color: #e6a23c } @media (max-width: 600px) { nav { display: none } }")And the fifth, Tailwind classes: div().class("p-4 md:flex hover:bg-sky-500/50"). Each one is covered below.
Style methods
The Styled trait has about 1,300 methods, generated from Tailwind’s scales. If you know Tailwind, you know their names: replace - with _.
- Spacing and sizing on the spacing scale:
p_4,mx_auto,w_64,h_full,w_1_3,gap_x_2, or any length:size(px(20.0)),w(px(40.0)). - Display and position:
flex,grid,block,hidden,absolute,inset_0,z_index. - Flexbox:
flex_col,flex_1,items_center,justify_between,flex_wrap. - Grid:
grid_cols(3),col_span(2). - Color:
bg,text_color,border_color,opacity. - Borders and corners:
border_1,border_t_2,rounded_lg,rounded_full. - Shadows:
shadow_sm…shadow_2xl. - Text:
text_sm…text_6xl,font_bold,italic,underline,uppercase,truncate,leading_tight,whitespace_nowrap. - Overflow and cursors:
overflow_y_scroll,cursor_pointer.
Lengths come from px(10.0), rems(1.5) or relative(0.5) (a fraction of the parent).
Colors come from rgb(0xRRGGBB), rgba(0xRRGGBBAA), hsla(h, s, l, a), the named helpers (black(), white(), red(), green(), blue(), yellow(), transparent_black(), transparent_white()), or any CSS color string.
State styles: .hover(|s| ...), .active(|s| ...) and .focus(|s| ...) take a refinement with the same methods. They’re CSS :hover, :active and :focus rules for that one element.
div().px_3().py_1().bg(rgb(0x545454)) .hover(|s| s.bg(rgb(0x4772b3))) .active(|s| s.opacity(0.8))CSS blocks
css!("...")splits the block into declarations when your program compiles. Every property name is resolved then, socss!("colr: red")is a compile error, and each value is tokenized once for the whole program. Custom properties (--brand: …) and!importantwork..style("a: b; c: d")takes CSS text at run time: a&str, aString, orcss!output..css("property", value)sets one property, with the value built at run time.
Stylesheets
style().child("...") puts a stylesheet anywhere in the tree, just like a <style> tag. Its rules apply to the whole window.
const CSS: &str = r#"nav a { display: block; padding: 4px 14px; color: #c8c8c8; text-decoration: none; }nav a:hover { background: #4772b3; color: white; }.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }.card.accent { background: linear-gradient(135deg, #4772b3, #7a4fb3); color: white; }tbody tr:nth-child(odd) { background: #353535; }"#;
html().child(head().child(style().child(CSS))).child(body() /* … */)Selectors
Selectors Level 4, the same ones you’d use on the web:
- Basics: type,
*,#id,.class, and attributes:[a],=,~=,|=,^=,$=,*=, with the case-insensitiveiflag. - Combinators: descendant,
>,+,~. - State:
:hover,:active,:focus,:focus-within,:focus-visible,:checked,:disabled,:enabled,:link,:any-link,:required,:optional,:read-only,:read-write,:open,:placeholder-shown. - Structure:
:first-child,:last-child,:only-child,:first-of-type,:last-of-type,:nth-child(),:nth-last-child(),:nth-of-type(),:nth-last-of-type(),:nth-child(2 of .on),:empty,:root. - Logic:
:not(),:is(),:where()(no specificity) and:has(), including:has(> img). - Nesting:
.card { & > h4 { … } .dark & { … } }. - Generated content:
::beforeand::afterwithcontent, includingattr(),counter()andcounters().
At-rules
@mediawith Media Queries 4 ranges, evaluated against the window:@media (400px <= width <= 600px).@supports,@layer(later layers win; unlayered rules beat layered ones) and@scope (.panel) to (.stop).@containersize queries, withcontainer-typeandcontainer-name.@font-facewith local font files, and@propertyinitial values.@keyframes: see Effects and animations.
The cascade
Declarations are applied in this order, later winning:
- the built-in user-agent sheet (the browser defaults, or Blender’s inside
styled), - your stylesheets,
- inline declarations: methods,
.css(...),.style(...), - state refinements:
.hover(...),.active(...),.focus(...).
!important beats all of them. Within a level, specificity and then source order decide, as in a browser. inherit, initial, unset and revert are understood.
Values, units and colors
- Units:
px,em,rem,%,vw,vh,vmin,vmax,ch,ex,pt,pc,in,cm,mm,Q, and the rest of CSS’s length units. - Math: every CSS math function with mixed units:
calc,min,max,clamp,round,mod,rem,abs, trigonometry,pow,hypot… - Variables: custom properties (
--gap: 10px) withvar(), fallbacks (var(--missing, 20px)) and inheritance, plusenv(). - Sizing keywords:
fit-contentandstretch. - Colors: CSS Color 4 and 5:
oklch(),lab(),color(),color-mix(), relative colors andlight-dark().
div().style("width: calc(100% - 2 * var(--gap)); background: color-mix(in oklch, var(--brand) 60%, white)")Tailwind CSS v4
Class names work as Tailwind v4 utilities out of the box. There’s no build step and no config file: each class compiles to CSS the first time it’s used, like Tailwind’s JIT engine, with the v4 default theme, and utilities sort the way Tailwind sorts them.
div().class("grid grid-cols-3 gap-4 md:grid-cols-6 hover:bg-sky-500/50")What the engine understands:
- Utilities with the v4 theme, including
animate-spin,animate-ping,animate-pulse,animate-bounceand thetransition-*andease-*utilities. - Arbitrary values:
w-[calc(100%-2rem)],h-[37px],bg-[#4772b3]. - CSS variable shorthand:
bg-(--brand). - Negatives, opacity modifiers and important:
-mt-2,bg-sky-500/50,text-sm/6,p-4!. - Variants: states (
hover:,focus-visible:…), pseudo-elements,group-*andpeer-*,has-*,data-*(data-[state=open]:),aria-*,nth-*, breakpoints (md:,2xl:,max-sm:), container queries (@md:),dark:,motion-*,supports-*,*:and arbitrary variants like[&>*]:.
If your own class names collide with Tailwind’s, turn it off: blenderui::set_tailwind(false).
Checked at compile time with tw!
.class("...") accepts anything, so a typo silently does nothing. Wrap the string in tw! and the compiler checks every class:
div().class(tw!("grid grid-cols-3 gap-4 md:grid-cols-6 hover:bg-sky-500/50"))Tailwind attributes in rsx!
In rsx!, utilities can be bare attributes (UnoCSS calls this “attributify”). They become classes at compile time, and they’re checked like tw!:
rsx! { <div p-4 w-1/2 bg-[#4772b3] hover:bg-sky-500/50 md:flex -mt-2 text-sm/6 /> }HTML boolean attributes (disabled, checked, hidden, open …) stay attributes.
Every Tailwind variant also works as an attribute prefix, chained as deep as you like:
rsx! { <button class="px-3 py-1 bg-zinc-800" hover:class="bg-sky-600 text-white" // classes, prefixed active:style="transform: scale(0.97)" // any CSS, as arbitrary properties focus-visible:outline="2px solid white" // one CSS property md:dark:hover:class="bg-sky-400" // chained variants hover:bg={rgb(0x4772b3)}> // typed value via .hover(|s| ..) "Save" </button>}group-hover:, peer-checked:, data-[state=open]:, aria-expanded:, @md:, 2xl:, max-sm:, *: and every other variant work the same way, because they all go through the Tailwind engine. Typed {expr} values work only under hover, active and focus; with any other variant, use variant:class or variant:style.
“Did you mean?” errors
Anything that looks like a utility but isn’t one is a compile error with suggestions, in rsx! and in tw! alike:
error: rsx!: `bg-sky-50O` is not a Tailwind utility; did you mean `bg-sky-500` or `bg-sky-50` or `bg-sky-100`?error: rsx!: `hovr:bg-red-500` is not a Tailwind utility; did you mean `hover:bg-red-500` or `odd:bg-red-500`?error: tw!: `roundd-lg` is not a Tailwind utility; did you mean `rounded-lg` or `rounded-s-lg` or `rounded-e-lg`?The suggestions come from blenderui_tailwind::suggest, so misspelled variants, roots and values are all caught.
Coverage
These numbers come from the repository’s coverage test (cargo test --test parity -- --nocapture):
| Area | Coverage |
|---|---|
| CSS properties | 271 change layout or rendering; 137 more parse but have no effect |
| W3C property index (745 properties) | 261 rendered (35%), 129 parsed without effect (17%), 355 not recognized (48%) |
| CSS 2.1, screen properties | 90 / 90 rendered, 90 / 90 parsed |
| Flexbox | 12 / 12 |
| Grid | 23 / 23, including grid-template-areas and named lines |
| Tailwind CSS v4 | Classes compile on first use (v4 theme, variants, arbitrary values); tw! checks them at compile time |
The first row counts every name the engine knows, including aliases such as -webkit-mask; the second measures against the W3C’s list of all CSS properties. Most of the 355 unrecognized properties are drafts, speech, paged media and SVG:
| Group | Properties |
|---|---|
Draft border and corner shapes (corner-shape, border-*-clip …) |
~78 |
| SVG fill, stroke and markers | ~40 |
Gap decorations (row-rule-* …) |
38 |
| Speech and aural | 31 |
| Animation triggers and scroll-driven animation | 26 |
| Paged media, regions, rhythm and others | ~25 |
| Everything else | ~100 |
The everyday screen CSS is much further along: Flexbox 12/12, Grid 23/23 and CSS 2.1 90/90.
What renders
Every property in this list changes layout or drawing (style.rs’s handler table):
- Box model:
display,box-sizing,width,height,min-*/max-*, the logical sizes (inline-size,block-size…),aspect-ratio,marginandpaddingwith every side and logical longhand,float,clear,contain. - Positioning:
position,insetandtop/right/bottom/left, the logicalinset-*,z-index. - Flexbox, grid and alignment: every property of both modules,
gap/row-gap/column-gap(and the oldgrid-gapnames),place-*,justify-*,align-*,order. - Overflow and scrolling:
overflow,overflow-x/-y/-inline/-block,scrollbar-width,text-overflow,scroll-snap-type,scroll-snap-align. - Backgrounds:
backgroundand its longhands, includingbackground-image(gradients and PNGurl()),-size,-position,-repeat,-clip,-originand-attachment;object-fit,object-position. - Borders and outlines: every
border-*longhand (physical and logical),border-radiusand its corners,border-imageand its longhands,outlineandoutline-offset. - Effects:
opacity,visibility,box-shadow,transform,translate,rotate,scale,transform-origin,filter,backdrop-filter,mix-blend-mode,isolation,clip-path,clip,maskandmask-image/-mode/-size/-position/-repeat. - Text:
color,fontand its longhands,font-variant-caps,line-height,text-align,text-decorationand its longhands,text-underline-offset,text-shadow,text-transform,text-indent,letter-spacing,word-spacing,vertical-align,white-space,text-wrap,text-wrap-mode,white-space-collapse,word-break,overflow-wrap,tab-size,direction,unicode-bidi. - Lists, tables and generated content:
list-styleand its longhands,table-layout,border-collapse,border-spacing,caption-side,empty-cells,content,quotes,counter-reset,counter-set,counter-increment. - Containers:
container,container-type,container-name. - Interaction:
cursor,pointer-events,accent-color. - Motion:
animationand everyanimation-*longhand,transitionand everytransition-*longhand.
What parses, but has no effect yet
These 137 are accepted and ignored for now, so stylesheets written for browsers still load:
- Fonts: OpenType features and synthesis:
font-feature-settings,font-variant-numeric,font-variant-ligaturesand the otherfont-variant-*,font-kerning,font-stretch,font-variation-settings,font-synthesis*… - Multi-column:
columns,column-count,column-width,column-rule*,column-span,column-fill. - Writing modes:
writing-mode,text-orientation,text-combine-upright. - 3D:
perspective,perspective-origin,backface-visibility,transform-style,transform-box. - Scrolling details: every
scroll-margin-*andscroll-padding-*,scroll-snap-stop,scroll-behavior,overscroll-behavior*,scrollbar-color,scrollbar-gutter. - Paged media:
orphans,widows,break-*,page-break-*. - Text details:
hyphens,text-justify,line-break,line-clamp,text-emphasis*,text-align-last… - Hints and the rest:
will-change,user-select,resize,appearance,caret-color,touch-action,image-rendering,shape-outside,zoom,background-blend-mode,mask-clip,mask-composite,mask-border*…
Not done yet
Also on the list: filter: url(#svg), clip-path: path(), and the rest of the W3C index. The effects page has the animation-specific gaps.
Next steps
Last updated