Principles
The layered model under Calaca UI. Tokens, core, skin, modifiers and instance utilities each own one kind of property, and tests keep the borders closed.
Five layers, one question each
Calaca UI is five layers stacked on purpose. Each layer answers exactly one question:
- Tokens (
--ca-*): what is the raw material? Color, radius, type, weight, elevation, motion and density. Tokens hold values, never rules. - Core (
src/core): how is the component built? Display, position, the focus ring, the disabled state, the touch target. The core also neutralizes browser defaults, because prebuilt CSS ships without a reset. It carries no visual opinion: the bare skeleton is usable and ugly on purpose. - Skin (
skins/*.ts): how does it look? Radius, border, shadow, height, padding, type, color and motion. A skin is data, so a skin that extends another one lists only what differs. - Modifiers (
ca-button--primary,ca-card--md): which state or shape? A modifier sets variables (pigments, gaps, sizes); it never paints. - Instance utilities (
mt-4,h-12,rounded-none): what is different right here, once? Tailwind utilities on the element itself.
Who wins
Later layers beat earlier ones. An instance utility beats a modifier, a
modifier beats the skin, the skin beats the core, and the core beats the
browser. ca-button ca-button--md h-12 renders at h-12.
The guarantee does not rest on source order, specificity tricks or
!important. Each layer sets properties no other layer sets: the core owns
display, position, outline and touch-action; the skin owns
border-radius, background and height; a modifier owns its variables.
When two layers can never collide, you never have to ask which one wins.
If you find yourself fighting the cascade, something is on the wrong layer.
State is data, not classes
Interactive state lives on attributes, not on extra classes: data-status,
data-skin, data-appearance, open, aria-expanded, :focus-visible.
A stepper item reads data-status="complete" and paints the check; a region
reskins under [data-skin].
The split is honest on both sides: classes describe how an element looks, attributes describe what it is. Styling and behavior read the same attribute, so they can never disagree about whether a dialog is open.
Composition over components
Layout belongs to the page, not to the component. ca-container,
ca-stack, ca-cluster, ca-grid and ca-section read their spacing from
--ca-*-gap and --ca-*-max variables, and they wrap by intrinsic sizing:
a cluster falls onto the next line whenever it runs out of room, so layouts
need no media queries and fit any container they land in.
Components never reach outside their own box for positioning. The space between components is always the layout’s job.
Native first
Where the browser already knows the behavior, Calaca uses it. A dialog is a
<dialog> with commandfor invokers, a dropdown is a [popover] with
popovertarget, a select menu is ::picker(select). Small scripts fill the
gaps browsers have not shipped yet, and each one names the day you can delete
it: ca-commands.js exists only until invoker commands are universal.
Enforced, not agreed
None of this depends on the contributor remembering a style guide:
naming.test.tsrejects a class outsideca-block__part--modifier.split.test.tsfails when two layers set the same property.logical.test.tsrejects physical directions (ml-*,left-*) so every layout works right-to-left.- The skin build fails on a class no contract defines, so a typo never ships.
logicalproperties, pigments and minimum target sizes are contract, not preference.
The model survives contributors who never read this page. That is the point.