CSS configures a DOM tree.
The analogy with Nix
Section titled “The analogy with Nix”Most developers know the basics of how CSS selectors works: you give elements classes, you write rules that target those classes, and the browser applies styling.
Nest applies the same model to Nix Configurations.
| Web (CSS) | Nest |
|---|---|
| HTML element | Configurable Node (host, user, service, …) |
| CSS class | Trait (gaming, devenv, lb, web) |
class="web server" | Tag a node with traits. is = [ web server ] |
Stylesheet rule .web { … } | Configure NixOS { is = web; nixos = …; } |
| DOM hierarchy | Infrastructure attrset hierarchy |
| Inherited CSS properties | Node attribute inheritance |
:has(.admin) | select (has admin) |
| Element sibling traversal | select.siblings web |
DOM as attrset
Section titled “DOM as attrset”Your attrset structure is the DOM. Everything outside nest.{trait,rules} is part of your DOM.
nest.prod.system = "x86_64-linux";nest.prod.env = "prod";
nest.prod.lb = { is = [ nest.host nest.lb ]; addr = "10.0.0.1"; };nest.prod.web = { is = [ nest.host nest.web ]; addr = "10.0.0.2"; };lb and web both inherit system = "x86_64-linux" and env = "prod" from the prod namespace — no repetition. Moving a node to a different namespace changes what it inherits.
Traits as Classification
Section titled “Traits as Classification”A trait is used to group nodes. A node can have many traits:
is = [ nest.host nest.web ] # two traits, like class="host web"Traits form dependency DAGs. Declaring server.needs = [nginx ssh firewall] means any node with nest.server automatically gains those three traits — and any rules targeting nginx, ssh, or firewall fire on it:
nest.trait.server.needs = [ nest.nginx nest.ssh nest.firewall ];nest.trait.web.needs = [ nest.server ];
# A node with is = [nest.host nest.web] gets:# web → server → nginx, ssh, firewall (all automatically)neededBy is the inverse: the trait declares what it attaches to, not the node:
nest.trait.monitoring.neededBy = nest.server;# Every server node automatically gains monitoring — no node needs to opt inRules for Configuration
Section titled “Rules for Configuration”Rules target nodes by using CSS selectors and contribute Dendritic Nix config fragments:
nest.rules = [ # Target all nodes with the nginx trait { is = nest.nginx; nixos.services.nginx.enable = true; }
# Target lb nodes, access sibling web nodes via select { is = nest.lb; nixos = { select, ... }: let webs = select.siblings nest.web; in { services.haproxy.backends = map (w: w.addr) webs; }; }];Multiple rules can target the same node — their nixos contributions are collected as a list and passed together to nixosSystem, just like multiple CSS rules applying to one element. The NixOS module system merges them with full option semantics.
Sibling scoping
Section titled “Sibling scoping”In CSS, .prod .web selects web elements inside prod. In Nest, siblings respect namespace boundaries:
select.siblings nest.web # returns only prod webs — not staging websThe namespace boundary scopes the sibling query
Prod and staging are separate namespace containers. Sibling queries from prod nodes never cross into staging. This is intentional — your topology structure is semantic.
Selectors mirror CSS
Section titled “Selectors mirror CSS”Every CSS selector concept maps to a Nest selector:
"*" # star: any node"#lb-prod" # id: node named lb-prod".lb" # trait: all nodes having trait"[env=prod]" # attr: attribute equality"[system]" # attrExists: attribute presence".web, .lb" # or: either trait":not(.staging)" # not: negation":has(.admin)" # has: child with trait":within(.prod)" # within: ancestor with trait"prod > web" # child combinator"prod web" # descendant combinator (CSS space)Programmatic selectors are also available as constructor functions:
nest.has nest.adminnest.within nest.prodnest.not nest.stagingnest.attrs { env = "prod"; }nest.when ({ host, ... }: host.port > 1024)[ nest.host nest.web ] # list = AND compound