Shared resources / Motion system v1

Fast in.
Faster out.

One motion language for Tattoo Company, tattoo.dev, Permanent and Tattoo.agency. It decides where things move from, how fast they arrive and leave, and how frames stack.

01 / First principles

Motion explains cause.

  1. 01

    Every move has an anchor.

    The eye needs to know where something came from. An element grows or rises from a defined point, chosen by its job, never from an arbitrary center.

  2. 02

    Arrive fast, settle slow.

    Entrances use the strike curve. Most of the distance is covered in the first third of the duration, so content is readable almost immediately and the tail settles it.

  3. 03

    Leave faster than you arrived.

    Exits run at 0.66× the entrance on the lift curve. Departing content should never hold attention.

  4. 04

    Only transform and opacity.

    No animating width, height, top, margins or filters on large surfaces. Motion stays on the compositor and never shifts layout.

  5. 05

    The mark holds still.

    The chrome T moves by opacity only. Motion belongs to frames, cues and controls around it.

  6. 06

    Respect the viewer.

    Under reduced motion, travel, stagger and loops stop. Short color and opacity changes remain, and the ASCII seal renders one still frame.

02 / Anchor points

Where things move from.

Set transform-origin from the anchor token for the element type. SVG children also need transform-box: fill-box.

ElementAnchorTokenWhy
Links, cues, underlinesLeft center--motion-anchor-link-cue
0% 50%
Text reads left to right; the cue grows in the reading direction and never jumps ahead of the words.
Underline exitRight center--motion-anchor-underline-exit
100% 50%
The line leaves through the far end, so it always travels forward instead of rewinding.
Buttons, chipsCenter--motion-anchor-button
50% 50%
A press is a pinch toward the finger. Scale 0.98, never below.
Cards, panels, sectionsBottom center--motion-anchor-frame
50% 100%
Frames are set down on a surface. They rise 12px from their bottom edge; they never zoom.
Menus, selects, popoversEdge touching the trigger--motion-anchor-menu
50% 0%
Content unfolds from the control that opened it, so the cause stays visible.
Status, toastsTop left of the control--motion-anchor-status
0% 0%
Feedback starts where the action happened and nudges 4px into place.
Dots, active markersCenter--motion-anchor-indicator
50% 50%
Indicators scale from nothing at their own center.
Logos and the chrome markCenter, opacity only--motion-anchor-mark
50% 50%
Never scale above 1, rotate, skew, bounce or glow. The ASCII seal is the one turning expression of the mark.

03 / Accelerated transitions

Two curves do most of the work.

Pick the curve by direction of travel, then the duration by the size of the thing moving. Bigger surfaces travel farther, so they get more time.

strikecubic-bezier(0.16, 1, 0.3, 1)

Accelerated entrance (expo-out). Fast first, then settles, like a needle meeting skin. Default for anything arriving.

--motion-ease-strike
liftcubic-bezier(0.7, 0, 0.84, 0)

Accelerated exit (quart-in). Leaves quickly and gets out of the way.

--motion-ease-lift
swapcubic-bezier(0.65, 0, 0.35, 1)

In-place state change (cubic in-out): toggles, segmented controls.

--motion-ease-swap
presscubic-bezier(0.2, 0, 0, 1)

Press and release of buttons.

--motion-ease-press
loopcubic-bezier(0, 0, 1, 1)

Linear. Continuous loops only (ASCII seal turn).

--motion-ease-loop

Duration scale

--motion-duration-instant0ms

State flips that need no travel (aria-pressed swaps under reduced motion).

--motion-duration-micro90ms

Hover color and press feedback.

--motion-duration-fast140ms

Link cues, nav underlines, indicator dots.

--motion-duration-base220ms

Buttons, segmented controls, status messages, card hover lift.

--motion-duration-frame420ms

Cards and sections entering the viewport.

--motion-duration-stage640ms

Hero surfaces and full-bleed frames.

--motion-duration-assemble900ms

ASCII seal assemble sweep.

Exit ratio

Exit = entrance × 0.66. A 420ms frame entrance leaves in about 280ms; a 140ms cue retracts in about 90ms.

Stagger

40ms between siblings, capped at 6 steps. A full row never waits more than 240ms for its last card.

04 / Frame motion

How frames enter, leave and stack.

Frame 01
Frame 02
Frame 03
Frame 04
Rise 12px from the bottom anchor · strike easing · 420ms · 40ms stagger

Enter

Opacity from 0 to 1 and a 12px rise from the bottom anchor, on strike, over 420ms (640ms for hero frames). Frames enter once, the first time they reach the viewport.

Exit

Opacity leads, travel follows. Lift curve at 0.66× the entrance. Scrolled-past content does not exit; it stays put.

Stack

Overlapping frames separate by z-order and a 1px line, never by scaling the back layer. Header 20, sticky navigation 10, content 1.

Hover

Cards lift 2px from their bottom edge on base timing. Imagery inside a card never scales or zooms.

Cues

Go cues extend 1.75× from the left anchor on fast timing. Download cues drop 2px onto their baseline. Neither one rotates.

Loops

Only the ASCII seal loops. It is linear, pauses offscreen and in background tabs, and stops for reduced motion.

05 / Reduced motion

Still means still.

When prefers-reduced-motion: reduce is set, every duration above micro collapses to 0ms. Rise, nudge, hover lift, cue extension, press scale and stagger all go neutral, and frames appear in place. The ASCII seal draws one assembled frame and does not respond to the pointer.

06 / Tokens

Use the variables, not the numbers.

The same tokens ship in every brand kit as tokens/motion.json (DTCG) and tokens/motion.css.

/* Tattoo Company motion system — generated from tokens/motion.json. */
:root {
  --motion-duration-instant: 0ms;
  --motion-duration-micro: 90ms;
  --motion-duration-fast: 140ms;
  --motion-duration-base: 220ms;
  --motion-duration-frame: 420ms;
  --motion-duration-stage: 640ms;
  --motion-duration-assemble: 900ms;
  --motion-ease-strike: cubic-bezier(0.16, 1, 0.3, 1);
  --motion-ease-lift: cubic-bezier(0.7, 0, 0.84, 0);
  --motion-ease-swap: cubic-bezier(0.65, 0, 0.35, 1);
  --motion-ease-press: cubic-bezier(0.2, 0, 0, 1);
  --motion-ease-loop: cubic-bezier(0, 0, 1, 1);
  --motion-exit-ratio: 0.66;
  --motion-stagger-step: 40ms;
  --motion-stagger-max: 6;
  --motion-rise: 12px;
  --motion-nudge: 4px;
  --motion-hover-lift: 2px;
  --motion-cue-extend: 1.75;
  --motion-press-scale: 0.98;
  --motion-anchor-link-cue: 0% 50%;
  --motion-anchor-underline-exit: 100% 50%;
  --motion-anchor-button: 50% 50%;
  --motion-anchor-frame: 50% 100%;
  --motion-anchor-menu: 50% 0%;
  --motion-anchor-status: 0% 0%;
  --motion-anchor-indicator: 50% 50%;
  --motion-anchor-mark: 50% 50%;
}

/* Reduced motion: no travel, no stagger; only short color/opacity changes remain. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --motion-duration-fast: 0ms;
    --motion-duration-base: 0ms;
    --motion-duration-frame: 0ms;
    --motion-duration-stage: 0ms;
    --motion-duration-assemble: 0ms;
    --motion-rise: 0px;
    --motion-nudge: 0px;
    --motion-hover-lift: 0px;
    --motion-cue-extend: 1;
    --motion-press-scale: 1;
    --motion-stagger-step: 0ms;
  }
}