Skip to main content
@craft-ts
LearnLearn with EffectAI agentsGuideReference
@craft-ts/core@craft-ts/component@craft-ts/dev-tools@craft-ts/effect@craft-ts/style@craft-ts/style-testing@craft-ts/i18n@craft-ts/i18n-effect@craft-ts/mcp
ExamplesMigrationEffect compatibility & maturityAdopting CraftTS progressivelyPress kitRoadmapBacklog
Beta
  • Learn
  • Learn with Effect
  • AI agents
  • Guide
  • Reference
  • Packages
  • Resources
  • Guide overview
  • Create a project
i.Core concepts
  • The mental model
  • Which primitive should I use?
  • Anatomy of a primitive
  • Generators and yield*
  • Insertions
  • Typed insertion pipes
  • Exceptions as values
ii.Managing state
  • Local state
  • State machines
  • query
  • Mutations
  • queryParams
  • asyncProcess
  • Selecting
  • Reacting to mutations
  • Collections
  • Collection utilities
  • Persistence
  • GlobalPersisterHandler
  • Pagination placeholders
  • Schema validation
iii.Recommended approaches
  • Inject at the point of use
iv.Structuring the app
  • craftService
  • Service scopes
  • Shaping the public API
  • Abstract services
  • App start
  • Lazy services
  • Server functions
  • craftRegisterFor
  • Target wrapper
v.Routing & type-safe DI
  • Setup
  • CLI automation
  • ESLint rules
  • Route providers
  • Route guards
  • Route exception handling
  • Non-blocking navigation
  • Global error component
  • Route load errors
  • Scaling routes
vi.Components & templates
  • Components
  • Activating the style system
  • Typed styles
  • Defining a design system
  • Tokens and variables
  • Global foundation and fonts
  • Pseudo-elements and animations
  • Axes and the matrix
  • Context obligations
  • Testing visual states
  • Text contrast
  • Visual attestation
  • Template obligations
  • Typed CSS variables
  • Fine-grained reactivity
  • Progressive rendering
  • Directives and .pipe(...)
  • settledValue & pendingNode
  • Accessibility
  • Customization
  • Content projection
  • Styling a component
  • Template migrator
vii.Forms
  • Overview
  • Validators
  • Submitting
  • Nested forms
  • Exception handling
  • Complete examples
viii.Testing
  • Testing services
  • Testing components
  • Type-level tests
  • Browser boundaries
  • Page stability
  • Architecture rules
    • Overview
    • Declarative baseline
    • Unique identities
    • HTTP endpoint ownership
    • Computed purity
    • Dependency cycles
    • Mutation reactions
    • Route DI proofs
    • Route component files
    • Path boundaries
    • Exclusive links
    • Persisted identities
    • insertSelect keys
    • craftEffect and network
    • craftEffect and imperative sync
    • Interactive element names
    • Metric thresholds
    • Server-state loaders
    • Primitive loader requirements
  • Extensible architecture graph
  • Graph insights
  • Craft graph vs Nx
ix.Reactivity utilities
  • craftComputed
  • craftEffect
  • craftMethod
  • source$
  • on$
  • fromEventToSource$
  • sourceFromEvent
  • afterRecomputation
x.Internationalisation
  • Type-safe i18n
  • The catalogue
  • Tokens
  • The runtime
  • With Effect
xi.Deployment (experimental)
  • Overview
  • Manifest reference
  • Diagnostics
  • Providers
  • Alchemy provider
xii.Going further
  • SSR and hydration
  • Effect integration
  • Program operators
  • Pattern matching
  • Observability
  • AI agents
  • Temporal runtime
  1. Components & templates
  2. Tokens and variables

Tokens and typed variables#

Level 1. Useful from the first component, and it does not require anything else in the app to change.

No value is a string#

Every value is a nominal object, not a branded string:

ts
import { bg, p, palette, space, unit } from '@craft-ts/style';

p(space(4)); // ✅
p(unit.rem(1.5)); // ✅
p('12px'); // ❌ a length is not a string
p(`${4}px`); // ❌ not even one of the right shape
bg('red'); // ❌ a colour is not a keyword
p(palette.text.strong); // ❌ a colour is not a length

The shape matters more than it looks. With string & { __length?: true } — an optional phantom on a primitive base — 'blabla' stays assignable, every test stays green, and the guarantee written on this page is false.

The scales are closed#

space(7) does not compile. When a step is missing, add it to the scale; there is no [17px] arbitrary-value syntax on purpose.

The one way out is marked:

ts
import { unsafeLength } from '@craft-ts/style';

unsafeLength('13px', 'aligns with a legacy image');

It compiles, and it propagates unproven to the dependency graph, where the debt is counted. Without this door a blocked agent bypasses the design system entirely; with it unmarked, it bypasses it in silence.

The property table is generated#

477 properties, generated from MDN data — which is what guarantees no keyword was invented. A closed keyword set is a namespace, never a string:

ts
import { display, position } from '@craft-ts/style';

display.inlineFlex; // ✅
display.inlineFlexx; // ❌ Property 'inlineFlexx' does not exist
position('sticky'); // ❌ a keyword is not something you pass in

Two consequences worth knowing:

  • overflow is not in the table. The only road to overflow-block: auto is provides(scrollPort.block) — see obligations. The wrong fix is not discouraged, it cannot be written.

  • 124 helpers are narrower than CSS. A grammar alternative the generator cannot close is dropped rather than approximated, so a helper may refuse a form CSS would accept. It can never produce CSS a browser rejects. The list is exported as NARROWED_PROPERTIES.

A few values the table cannot type on its own have constructors:

  • gradient.linear, .radial, .repeatingLinear, .repeatingConic, written with bgImage(...) — several images make several layers, and bgSize / bgPosition take one inline and one block value;

  • uaScheme.light / .dark for color-scheme, so native controls follow the theme;

  • spanAllColumns for grid-column: 1 / -1;

  • pseudo.content.attr(ident('data-x')) for a ::before / ::after whose text is an attribute the template writes.

Typed custom properties#

ts
import { color, cssVars, kind, p, palette, unit } from '@craft-ts/style';

export const v = cssVars('badge', {
  ink: kind.color(palette.text.strong),
  pad: kind.length(unit.px(16)),
});

color(v.ink); // ✅ the token carries its kind's brand
p(v.ink); // ❌ a <color> variable is not a length
v.ink.or(space(4)); // ❌ the fallback is typed against the same kind

Two rules the browser enforces and the types cannot:

A registered initial-value must be computationally independent. initial-value: 1rem makes the whole @property rule invalid and the browser drops it silently — the variable stops being registered, var(--x) resolves to nothing, and whatever reads it computes to zero. cssVars refuses a relative unit there and names the fix.

inherits: false is the right default, and wrong for a theme. A variable an element sets and reads on itself should not inherit: it bounds invalidation. A theme variable is the opposite — set once on a wrapper, read by everything below — so pass { inherits: true }. A non-inheriting theme hands every descendant the initial value, which looks exactly like dark mode not working, with no error anywhere.

←Defining a design systemGlobal foundation and fonts→

On this page

  • No value is a string
  • The scales are closed
  • The property table is generated
  • Typed custom properties

@craft-ts · MIT licensed

GitHubnpm

Search