Physics Tags
component / motion / marketing
Tags that drop into a box with gravity, land on each other and can be picked up and thrown — hand-rolled AABB physics with no dependency, chips or icon tiles, over a plain list that is the base state for the server, reduced motion and print.
- type
- component
- files
- 2
- registry deps
- skeleton
- npm deps
- none
installs
- lib/automindz/physics-world.ts
- components/automindz/physics-tags.tsx
- Hand-rolled AABB physics in lib/automindz/physics-world.ts, no dependency
- Base state is a CSS-wrapped list: server, reduced motion and print get the same picture
- Drag and throw with the pointer; the grip pushes a pile a few pixels a sub-step, never teleports
- icon on a tag, appearance="tile" for a wall of marks — none ship in the default config
- Loop stops on its own once the pile is still
Edit lib/automindz/physics-world.ts, not the component, and pass it — tags has no default here (<PhysicsTags tags={PHYSICS_TAGS} label="What ships in every plan" />). There is no stylesheet to import and no npm dependency: the world is six hundred lines of arithmetic in the lib file, and the component writes one transform per chip per frame. **The base state is the finished pile, laid out by CSS** — a flex-wrapped <ul> of <li> chips packed along the bottom of a box height tall — and it is what the server sends, what paints before hydration, what prefers-reduced-motion gets and what a print stylesheet gets; the simulation is layered on after mount and only when motion is allowed, so there is exactly one picture to design. height is a floor, not a ceiling: if the chips need more room to wrap than that, the box is as tall as the wrap, so nothing under the band moves when the drop takes over. **The chips are not interactive elements** — no button, no tabIndex, no keyboard equivalent, on purpose: the drag is a pointer-only enhancement over a list that already carries everything the pile does, and a focus stop for nudging a decorative chip is a chore added to the tab order. interactive={false} is the drop with no listeners, no grab cursor and no user-select: none. startOnView (default on) holds the chips above the box until it scrolls into view and pauses the loop while it is off screen; the loop also stops on its own the moment the pile is still, so a settled band costs nothing. Ceilings, in arithmetic rather than in a note: one frame advances the world at most 1/30s however long a background tab was away, so a pile cannot launch itself through the floor when someone looks back; speed is capped at 2400px/s and integrated in 1/120s sub-steps, so a chip can never cover more than 20px between collision checks and never passes through another; a throw carries your last movement's velocity, capped the same way. Chips never rotate — a static ±6° lean is cosmetic — so tipping is stood in for by sliding: a chip whose centre hangs past the union of what is under it slides toward the drop, a chip bridging two others stays, and a chip on a two-pixel corner falls past it. A flipped reduced-motion preference stops the loop where it is. A tag may carry an icon — an inline SVG component in currentColor — drawn before the label; appearance="tile" makes every tag *with* an icon a 56px square of the mark alone with the label as its accessible name and tooltip (the logo wall that fell over), while a tag without one stays a chip, so a half-licensed config still reads as one pile. The default config names no vendor and ships no mark: which brands you may display is between you and the mark's owner. The pointer holds a chip at PHYSICS_GRIP — it closes on the pointer a fraction per sub-step rather than being written to it — so a chip dragged through a pile pushes the pile a few pixels at a time along the axis it actually arrived on, and a held chip dragged down onto a grounded one rides over it, because nothing pushes a chip through the floor. Pass label to name the list, and pass tags while loading too — the skeleton chips take their widths from the labels. An empty tags renders nothing at all rather than an empty box. Gravity, bounce, friction, the slide and the sleep threshold are constants in the lib file with their reasoning beside them, not props.
Variations
one state per section
- 01The default: eighteen capabilities, dropped
- 02appearance="tile": the logo wall that fell over
- 03Chips with a mark before the label
- 04Forty chips in a shorter box
- 05The drop without the drag
- 06Loading, at the band's real height
- 07No tags at all
- 08Still is the base state
The default: eighteen capabilities, dropped
Scroll it into view and the chips fall in one after another, land, and pile up along the floor; the loop stops the moment the pile is still — open the performance panel and watch it go quiet. Pick one up and throw it: it carries the velocity of your last movement, capped, and everything it lands on moves out of the way. Nothing here is a button and nothing is in the tab order, on purpose — the list is the content, and the pile is a presentation of it. Turn Reduce Motion on and reload: the same eighteen <li>s, wrapped along the bottom of the same box by CSS, which is also what the server sent and what a print stylesheet gets.
- Single sign-on
- Webhooks
- Audit log
- Row-level security
- Rate limiting
- Role-based access
- Usage metering
- Multi-tenant
- SCIM provisioning
- Data residency
- SOC 2
- Export to CSV
- API keys
- Custom domains
- Two-factor auth
- Scheduled reports
- Retention policies
- Priority support
appearance="tile": the logo wall that fell over
Every tag here carries an icon, so each is a 56px square with the mark alone and the label as its accessible name (and its tooltip). Pick up Stripe and drop it on the pile. The marks are Simple Icons paths snapshotted into the docs site — the item ships none of them: which brands a client may display is between them and the mark's owner, and the default config names no vendor. A tag without an icon stays a chip in this appearance, so a config with four licensed marks and fourteen words still reads as one pile.
- Stripe
- Vercel
- Supabase
- PostgreSQL
- GitHub
- Linear
- Notion
- Figma
- Docker
- Cloudflare
- Terraform
- Next.js
- Sentry
- Datadog
- HubSpot
- Zapier
- Resend
- Clerk
- Auth0
- Okta
- Anthropic
Chips with a mark before the label
The same tags in the default appearance: the icon sits before the label at text size, and the chip is measured with it in, so the collision box is the chip you see. This is the shape for a stack section that has to say the names — a tile is for a wall the reader already recognises.
- Stripe
- Vercel
- Supabase
- PostgreSQL
- GitHub
- Linear
- Notion
- Figma
- Docker
- Cloudflare
- Terraform
- Next.js
- Sentry
- Datadog
- HubSpot
- Zapier
- Resend
- Clerk
- Auth0
- Okta
- Anthropic
Forty chips in a shorter box
height is a floor, not a ceiling: forty chips need more room to wrap than 240px, so the base state grows to fit them and the box the drop lands in is exactly as tall as that wrap — nothing under the band moves when the simulation takes over. Watch the pile spread rather than tower: a chip whose centre hangs past what is under it slides off, which is the stand-in for tipping in a world where nothing rotates, and a chip bridging two others stays put, exactly as it would on a table.
- Single sign-on
- Webhooks
- Audit log
- Row-level security
- Rate limiting
- Role-based access
- Usage metering
- Multi-tenant
- SCIM provisioning
- Data residency
- SOC 2
- Export to CSV
- API keys
- Custom domains
- Two-factor auth
- Scheduled reports
- Retention policies
- Priority support
- Single sign-on
- Webhooks
- Audit log
- Row-level security
- Rate limiting
- Role-based access
- Usage metering
- Multi-tenant
- SCIM provisioning
- Data residency
- SOC 2
- Export to CSV
- API keys
- Custom domains
- Two-factor auth
- Scheduled reports
- Retention policies
- Priority support
- Webhooks
- SSO
- SOC 2
- API keys
The drop without the drag
interactive={false} is the same fall with no pointer: no listeners are attached, the cursor stays the page's, and the text stays selectable — user-select: none is only set on chips you are allowed to pick up. Use it where the pile sits behind or beside something that has its own gesture, or on a page where a grab cursor over a marketing band would promise more than it delivers.
- Single sign-on
- Webhooks
- Audit log
- Row-level security
- Rate limiting
- Role-based access
- Usage metering
- Multi-tenant
- SCIM provisioning
- Data residency
- SOC 2
- Export to CSV
- API keys
- Custom domains
- Two-factor auth
- Scheduled reports
- Retention policies
- Priority support
Loading, at the band's real height
Skeleton chips in the same wrap, each roughly the width of its label, so the band is the height it will be and does not re-wrap when the labels land. Pass tags while loading — the config is where the widths come from, so pass it even before you have decided to show it. The simulation does not start on a loading list: there is nothing to measure until the text is there.
No tags at all
An empty config renders nothing — not an empty box, which at 320px tall reads as a failed load. The dashed frame below is the preview's, and it is empty on purpose.
Still is the base state
Turn on Reduce Motion in your OS and reload: eighteen chips in a flex-wrapped list, packed along the bottom of a 320px box by content-end, no transform on any of them. That is the server's HTML, the pre-hydration paint, the print stylesheet and the reduced-motion path in one picture, and it is where the item spends its life on every device that asked for less motion. Flip the setting on while a pile is mid-fall and the loop stops where it is — a half-landed pile is still, and still is what was asked for.
- Single sign-on
- Webhooks
- Audit log
- Row-level security
- Rate limiting
- Role-based access
- Usage metering
- Multi-tenant
- SCIM provisioning
- Data residency
- SOC 2
- Export to CSV
- API keys
- Custom domains
- Two-factor auth
- Scheduled reports
- Retention policies
- Priority support
