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. Pseudo-elements and animations

Pseudo-elements and animations#

A component sheet can style what used to need a hand-written selector — ::before, ::placeholder, @keyframes, transition — with the same typed vocabulary as the rest of the sheet.

Pseudo-elements#

ts
import {
  bg,
  blockSize,
  craftStyles,
  cssString,
  defineStateAxis,
  display,
  inlineSize,
  palette,
  pseudo,
  radii,
  radius,
  unit,
  when,
} from '@craft-ts/style';

export const status = defineStateAxis('status', ['failed']);

export const indicator = craftStyles('indicator', {
  root: [
    display.inlineFlex,
    pseudo.before([
      // Without `content`, ::before is never generated — a type error here.
      pseudo.content.empty,
      display.block,
      inlineSize(unit.rem(0.5)),
      blockSize(unit.rem(0.5)),
      radius(radii.full),
      bg(palette.accent.info),
      when(status.failed, [
        pseudo.content.text(cssString('⚠')),
        bg(palette.accent.danger),
      ]),
    ]),
  ],
});

pseudo.before([...]) is an item of a class, and nests like when(...). The emitter always puts the pseudo-element last in the selector, where CSS requires it, whatever the nesting order: when(status.failed, [pseudo.before(...)]) and pseudo.before([when(status.failed, ...)]) both give .x[data-status='failed']::before.

Available: pseudo.before, pseudo.after, pseudo.placeholder, pseudo.marker, pseudo.selection, pseudo.backdrop.

content is required, and typed#

::before and ::after are not generated without a content, and nothing they declare applies. Rather than a decoration that silently disappears, it is a type error:

ts
pseudo.before([display.block]);
// ~~~~~~~~~ ERROR_a_generated_pseudo_element_needs_content

The content itself comes from pseudo.content:

helperCSS
pseudo.content.emptycontent: ""
pseudo.content.nonecontent: none
pseudo.content.text(cssString('→'))content: "→"
pseudo.content.counter(ident('step'))content: counter(step)

The check reads the top level of the block: a content placed only under a when(...) leaves the base scenario without one, which is the same bug.

Pseudo-classes are axes#

There is no pseudo.hover. :hover, :focus-visible or :disabled are states, and a state is an axis (interaction.hover, defineStateAxis). That is what puts it in the variant contract, the visual matrix and the contrast proof — a hand-written :hover would be invisible to all three.

Keyframes and animations#

ts
import { animate, easing, keyframes, rotate } from '@craft-ts/style';

export const spin = keyframes('spin', {
  from: [rotate(unit.deg(0))],
  to: [rotate(unit.deg(360))],
});

export const spinner = craftStyles('spinner', {
  root: [
    ...animate(spin, {
      duration: unit.ms(800),
      easing: easing.linear,
      iterations: 'infinite',
    }),
  ],
});

keyframes(name, steps) returns a token; animate(token, options) plays it. An animation cannot point at keyframes that do not exist. The steps are from, to or percentages, and each step holds declarations from the generated table.

animate writes longhands (animation-name, animation-duration, …), so a variant can change the duration alone. It is not called animation because that name is the generated shorthand helper.

Transitions#

ts
import { prop, transitions } from '@craft-ts/style';

export const button = craftStyles('button', {
  root: [
    ...transitions([prop.backgroundColor, prop.color], {
      duration: unit.ms(150),
      easing: easing.easeOut,
    }),
  ],
});

The properties are named from the table (prop.backgroundColor). There is no all: transition: all animates whatever a later variant happens to change, layout included, and nobody decided that.

easing holds the keywords (linear, ease, easeIn, easeOut, easeInOut, stepStart, stepEnd) and two constructors, easing.cubicBezier(x1, y1, x2, y2) and easing.steps(count, position).

Reduced motion#

You write none of it. Under prefers-reduced-motion: reduce, the global foundation collapses every animation and transition of the document to an instant.

←Global foundation and fontsAxes and the matrix→

On this page

  • Pseudo-elements
  • content is required, and typed
  • Pseudo-classes are axes
  • Keyframes and animations
  • Transitions
  • Reduced motion

@craft-ts · MIT licensed

GitHubnpm

Search