| name | css-styling |
| description | Style components with the CSS engine — ClassBuilder DSL, Styleable mixin, utility classes, design tokens, and component CSS |
| user-invocable | true |
CSS Styling with the Asset Pipeline
You are a CSS specialist who styles Crystal components using the asset pipeline's utility-first CSS engine. The engine generates CSS from class names, supports responsive breakpoints, pseudo-states, ARIA states, dark mode, and WCAG 2.2 AA accessibility defaults. Colors use the OKLCH color space.
ClassBuilder DSL
Build CSS class strings with a fluent API. Classes are automatically registered with the ClassRegistry for CSS generation.
classes = Components::CSS::ClassBuilder.new
.base("flex", "items-center", "gap-4")
.hover("shadow-lg")
.focus_visible("ring-2", "ring-blue-500")
.dark("bg-gray-800", "text-gray-100")
.responsive { |r|
r.sm("flex-col")
r.lg("flex-row")
}
.build
# => "flex items-center gap-4 hover:shadow-lg focus-visible:ring-2 focus-visible:ring-blue-500 dark:bg-gray-800 dark:text-gray-100 sm:flex-col lg:flex-row"
All ClassBuilder Methods
| Method | Purpose | CSS Output |
|---|
.base(*classes) | Foundation classes | Classes as-is |
.add(classes, condition?) | Conditional add | Classes if condition true |
.when(condition, classes) | Add if true | Classes if condition true |
.unless(condition, classes) | Add if false | Classes if condition false |
.hover(classes) | Mouse hover | hover: prefix |
.focus(classes) | Any focus | focus: prefix |
.active(classes) | Active/pressed | active: prefix |
.disabled(classes) | Disabled state | disabled: prefix |
.dark(classes) | Dark color scheme | dark: prefix |
.focus_visible(classes) | Keyboard focus (WCAG 2.4.7) | focus-visible: prefix |
.focus_within(classes) | Child has focus | focus-within: prefix |
.motion_safe(classes) | No motion preference (WCAG 2.2.2) | motion-safe: prefix |
.motion_reduce(classes) | Prefers reduced motion | motion-reduce: prefix |
.aria_expanded(classes) | aria-expanded="true" | aria-expanded: prefix |
.aria_selected(classes) | aria-selected="true" | aria-selected: prefix |
.aria_checked(classes) | aria-checked="true" | aria-checked: prefix |
.aria_disabled(classes) | aria-disabled="true" | aria-disabled: prefix |
.invalid(classes) | Form invalid state | invalid: prefix |
.valid(classes) | Form valid state | valid: prefix |
.responsive { |r| ... } | Breakpoint variants | sm:, md:, lg:, xl:, xxl: |
.build | Returns deduped class string | Registers with ClassRegistry |
ResponsiveBuilder
Inside the .responsive block:
.responsive { |r|
r.sm("text-sm") # >= 640px
r.md("text-base") # >= 768px
r.lg("text-lg") # >= 1024px
r.xl("text-xl") # >= 1280px
r.xxl("text-2xl") # >= 1536px
}
Styleable Mixin
Include Components::CSS::Styleable in components for convenience methods.
css { |c| ... }
Shorthand for ClassBuilder — creates builder, yields it, calls .build:
class MyComponent < Components::StatelessComponent
include Components::CSS::Styleable
def render_content : String
wrapper_classes = css { |c|
c.base("card", "rounded-lg", "shadow-md")
c.hover("shadow-xl")
c.dark("bg-gray-800")
}
Components::Elements::Div.new(class: wrapper_classes).build { |d|
d << "Content"
}.render
end
end
class_names(**options)
Conditional class helper — includes class when value is truthy:
class_names(
"btn" => true,
"btn-primary" => variant == "primary",
"btn-disabled" => disabled?,
"btn-lg" => size == "large"
)
# => "btn btn-primary" (if variant is "primary" and not disabled/large)
Values can be String (always included), Bool, or Array(String).
variant_classes(base, variant?, size?, state?)
Generates variant class strings:
variant_classes("btn", "primary", "lg")
# => "btn btn-primary btn-lg"
variant_classes("alert", "success", nil, "dismissible")
# => "alert alert-success alert-dismissible"
merge_classes(*class_strings)
Merge and deduplicate multiple class strings:
merge_classes("flex gap-4", "flex items-center gap-8")
# => "flex gap-4 items-center gap-8"
Design Tokens
Configured via Components::CSS::Config. Default values below.
Colors (OKLCH)
Four color families, each with shades 50-950:
| Token | 50 (lightest) | 500 (base) | 950 (darkest) |
|---|
gray | oklch(0.985 0 0) | oklch(0.551 0 0) | oklch(0.13 0 0) |
red | oklch(0.971 0.013 17.38) | oklch(0.637 0.237 25.331) | oklch(0.258 0.092 26.042) |
blue | oklch(0.97 0.014 254.604) | oklch(0.623 0.214 259.815) | oklch(0.282 0.091 267.935) |
green | oklch(0.982 0.018 155.826) | oklch(0.723 0.219 149.579) | oklch(0.266 0.065 152.934) |
Full shade scale: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950
Usage in utility classes: bg-blue-500, text-gray-900, border-red-300
Spacing
| Token | Value | Token | Value |
|---|
0 | 0 | 8 | 2rem |
px | 1px | 10 | 2.5rem |
0.5 | 0.125rem | 12 | 3rem |
1 | 0.25rem | 16 | 4rem |
2 | 0.5rem | 20 | 5rem |
3 | 0.75rem | 24 | 6rem |
4 | 1rem | 32 | 8rem |
5 | 1.25rem | 48 | 12rem |
6 | 1.5rem | 64 | 16rem |
7 | 1.75rem | 96 | 24rem |
Usage: p-4 (1rem padding), mt-8 (2rem margin-top), gap-2 (0.5rem gap)
Breakpoints
| Name | Min-width | Usage |
|---|
sm | 640px | sm:flex |
md | 768px | md:grid-cols-2 |
lg | 1024px | lg:text-lg |
xl | 1280px | xl:max-w-screen-xl |
2xl | 1536px | 2xl:px-8 |
Font Sizes
| Token | Value | Token | Value |
|---|
xs | 0.75rem | xl | 1.25rem |
sm | 0.875rem | 2xl | 1.5rem |
base | 1rem | 3xl | 1.875rem |
lg | 1.125rem | | |
Border Radius
| Token | Value |
|---|
none | 0 |
sm | 0.125rem |
(default) | 0.25rem |
md | 0.375rem |
lg | 0.5rem |
xl | 0.75rem |
2xl | 1rem |
3xl | 1.5rem |
full | 9999px |
Shadows
| Token | Description |
|---|
sm | Small shadow |
(default) | Medium shadow |
md | Medium-large shadow |
lg | Large shadow |
xl | Extra-large shadow |
Utility Classes
The CSS parser (Components::CSS::Engine::Parser) handles 100+ utility patterns.
Layout & Display
block, inline-block, inline, flex, inline-flex, grid, hidden
Position
relative, absolute, fixed, sticky
Flexbox
flex-row, flex-col, flex-wrap, flex-nowrap, items-start, items-center, items-end, items-stretch, justify-start, justify-center, justify-end, justify-between, justify-around, gap-{size}
Spacing
Margin: m-{size}, mx-{size}, my-{size}, mt-{size}, mr-{size}, mb-{size}, ml-{size}
Padding: p-{size}, px-{size}, py-{size}, pt-{size}, pr-{size}, pb-{size}, pl-{size}
Sizing