AkurAI Build
Menu

Public repository / master

AkurAI-Framework / site/content/docs/styling.md

Latest change 3aae9f4e85241bcb775c31f5c03094becd3da56a - Pure-Rust UI layer: akurai-ui, akurai-crypto, Tailwind-grade akurai-css by Ólafur Búi Ólafsson

# Styling: the CSS engine

The framework includes a from-scratch atomic/utility CSS engine — Tailwind-style
in spirit, but pure Rust with zero dependencies and no Node toolchain. It emits
**only the utilities your markup actually uses**, so the shipped stylesheet stays
small by construction.

## How it works

Two steps, both pure `std`:

1. **Scan**: read your source and collect the class tokens used.
2. **Generate**: for each recognised utility class, emit its CSS rule. Unknown
   classes (your hand-written component classes, identifiers in code) are left
   alone.

There are two entry points:

```rust
// HTML/templates: tokens from class="…" attributes, first-seen order.
let css = akurai_css::build(&sources);
// e.g. ".flex{display:flex}.p-2{padding:8px}"

// Any source (Rust string literals, JS, HTML): every candidate token,
// canonical Tailwind-like order. Use this for a pure-Rust UI.
let css = akurai_css::build_any(&sources);
```

`build_any` uses `akurai_css::scan::candidates`, which splits on whitespace and
on `" ' ` , ; ( ) { } < > =`. Square brackets stay together, so
`w-[calc(100%-2rem)]` and `data-[state=open]:bg-panel` come through as one
token. A component written as
`class("inline-flex items-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-medium text-bg hover:bg-accent/90 focus-visible:ring-2 disabled:opacity-50")`
is styled with no extra configuration.

## Utility grammar

The grammar follows Tailwind's names and semantics. The spacing scale is 4px:
`2` means 8px, `px` means 1px, and `0.5`, `1.5`, `2.5` and `3.5` are allowed.
Most families also accept an arbitrary `[value]`, where `_` stands for a space
(`grid-cols-[1fr_2fr]`).

| Family | Utilities |
|---|---|
| Spacing | `p` `px` `py` `pt` `pr` `pb` `pl` `m` `mx` `my` `mt` `mr` `mb` `ml` (margins take `auto`), `gap` `gap-x` `gap-y`, `space-x` `space-y` |
| Sizing | `w` `h` `size` `min-w` `min-h` `max-w` `max-h`: the scale, fractions (`1/2` gives 50%), `full`, `screen`, `dvh`, `min` `max` `fit`, `auto`, `max-w-{xs…7xl, prose, screen-sm…screen-2xl}` |
| Layout | `block` `flex` `grid` `hidden` `contents` `sr-only` and the other display keywords, `relative` `absolute` `fixed` `sticky`, `inset` `inset-x` `top` `left` …, `z-*`, `overflow-*`, `invisible`, `isolate`, `box-border`, `aspect-square` `aspect-video`, `object-cover` |
| Flex & grid | `flex-row` `flex-col` `flex-wrap`, `flex-1` `flex-auto` `flex-none`, `grow` `shrink-0` `basis-*` `order-*`, `items-*` `justify-*` `content-*` `self-*` `place-*`, `grid-cols-{n,none,[…]}` `grid-rows-*`, `col-span-*` `col-start-*` `row-span-*`, `grid-flow-*`, `auto-cols-*` `auto-rows-*` |
| Typography | `text-{xs…9xl}` (font size only; `text-sm/6` also sets line height), `font-thin…font-black`, `font-sans` `font-mono` (read `--font-sans` / `--font-mono` when they are defined), `italic`, `tracking-*`, `leading-*`, `text-left` `text-center`, `uppercase`, `underline` `underline-offset-*`, `truncate`, `whitespace-*`, `break-words`, `tabular-nums`, `antialiased`, `line-clamp-*`, `align-*`, `list-none` `list-disc` |
| Colour | `text` `bg` `border(-x/y/t/r/b/l)` `ring` `ring-offset` `outline` `fill` `stroke` `accent` `caret` `divide` `placeholder` `decoration`, followed by a theme token (`bg` `bg-2` `panel` `panel-2` `border` `border-2` `fg` `muted` `dim` `accent` `accent-2` `ok` `warn` `info` `danger`) or `transparent` `current` `white` `black` `inherit`. Tokens become `var(--token)`. |
| Backgrounds | `bg-none`, `bg-gradient-to-{t,tr,r,br,b,bl,l,tl}` with `from-*` `via-*` `to-*` |
| Borders | `border` `border-{0,2,4,8}` `border-{x,y,t,r,b,l}[-N]`, `border-dashed` `border-none`, `divide-x` `divide-y`, `rounded-*` (per side `rounded-t-md`, per corner `rounded-tl-sm`) |
| Effects | `shadow-{sm,md,lg,xl,2xl,inner,none}`, `ring` `ring-{0,1,2,4,8}` `ring-inset` `ring-offset-*`, `opacity-{0…100}`, `blur-*` `backdrop-blur-*`, `translate-x/y-*` `scale-*` `rotate-*` |
| Interactivity | `cursor-*`, `pointer-events-none`, `select-none`, `resize-*`, `appearance-none`, `outline-none` `outline-*` `outline-offset-*`, `transition(-all/colors/opacity/shadow/transform/none)` `duration-*` `ease-*` `delay-*`, `animate-{spin,pulse,ping}`, `scroll-smooth`, `snap-*`, `touch-manipulation`, `will-change-*` |
| Tables | `border-collapse` `table-fixed` `table-auto` |

**Colour opacity.** `/N` mixes the colour with transparent:
`bg-accent/90` gives
`background-color:color-mix(in srgb, var(--accent) 90%, transparent)`.
Arbitrary colours also work: `bg-[#123456]`, `text-[var(--x)]`,
`bg-[color:var(--member)]`.

**Arbitrary properties.** `[mask-type:luminance]` emits `mask-type:luminance`.

**Modifiers.** A leading `!` adds `!important` (`!p-0`). A leading `-`
negates the value (`-mt-2`, `-translate-x-1/2`, `-z-10`).

**Flat corners.** House rule: no radius is larger than 6px, and there are no
pills. `rounded-sm` is 2px, `rounded` is 4px, `rounded-md` is 6px. `rounded-lg`,
`-xl`, `-2xl`, `-3xl` and `-full` are all clamped to 6px. Arbitrary radii are
clamped too: `rounded-[20px]` gives 6px and `rounded-[50%]` gives
`min(50%,6px)`.

**No preflight needed.** Shadows and rings combine through `--tw-shadow` and
`--tw-ring-*` custom properties, the same way Tailwind does it, so
`shadow-md ring-2` shows both. Those properties are registered with
`@property … inherits:false`, so a parent's shadow or ring never reaches its
children. Each `@property` block and each `@keyframes` block is emitted once,
at the end of the stylesheet. Border width utilities also set the border style,
so `border` draws a line on its own. The default border colour is
`currentColor`; add `border-border` for the theme colour.

### Variants

Variants can be chained: `md:dark:hover:bg-accent`.

- **State:** `hover:` `focus:` `focus-visible:` `focus-within:` `active:`
  `disabled:` `checked:` `first:` `last:` `odd:` `even:` and more.
  `placeholder:` targets `::placeholder`.
- **Group and peer:** `group-hover:` matches `.group:hover .x`, and
  `peer-checked:` matches `.peer:checked ~ .x`.
- **ARIA and data:** `aria-selected:` `aria-checked:` `aria-expanded:` match
  `[aria-…="true"]`. `aria-current:` matches any value except `false`.
  `data-[state=open]:` matches `[data-state="open"]`, and `data-[active]:`
  matches `[data-active]`.
- **Media:** breakpoints `sm` (640px), `md` (768px), `lg` (1024px), `xl`
  (1280px) and `2xl` (1536px) are min-width queries. `dark:` uses
  `prefers-color-scheme`, `motion-safe:` and `motion-reduce:` use
  `prefers-reduced-motion`, and `print:` applies when printing.

Selectors are escaped as CSS requires, for example
`.hover\:bg-accent\/90:hover{…}`.

**Order.** Base utilities come first, then state variants in Tailwind's
precedence order (`disabled:` beats `hover:`), then media blocks with
breakpoints in ascending order. Later and responsive rules therefore win. In
`build_any`, shorthands are also placed before axis and single-side
longhands, so `p-4 px-2` keeps the `px-2` padding whatever order the classes
appear in. `build` keeps base rules in first-seen order for backwards
compatibility.

Malformed classes (`p-`, `p-x`, `p--1`, `w-[1px;color:red]`) are ignored rather
than emitting broken CSS.

## Using it: `/utilities.css`

The engine is wired into `akurai serve` and `akurai dev`. On startup the server
scans every `.html`/`.js` source under your frontend dir for the utility classes
you actually use, generates the minimal stylesheet, and serves it at:

```
/utilities.css
```

Link it from your `<head>` (the starter template already does):

```html
<link rel="stylesheet" href="/utilities.css" />
```

It is **opt-in and harmless**: if your markup references no utility classes the
file is simply empty, and a project that never links it behaves exactly as
before. Hand-written component classes are left untouched — only recognised
utilities are emitted. Note the scanner reads `class="…"` attributes, so utility
classes used inside JS template literals (e.g. `` `<div class="flex">` ``) are
picked up, while classes added via `classList.add("flex")` are not.

### Production vs. dev

- **`akurai serve`** renders `utilities.css` **once at startup** and caches it in
  memory (nothing is written to disk; your source files are never modified).
- **`akurai dev`** regenerates it **fresh on every request**, so adding or
  removing a utility class in your markup shows up on the next reload — together
  with live-reload, no restart and no build step.

The generated stylesheet is also folded into the [asset pipeline](/docs/assets),
so it is additionally available under a fingerprinted, immutably cached URL —
see the manifest at `/api/_assets`.

The hand-written design tokens it builds on are documented under
[Design system → Foundations](/docs/design-system/foundations).