Inspiration
Personal climate action fails for a boring reason: there is no feedback loop.
Skip a 4 km drive and you avoid about 0.8 kg of CO2e. Nothing in the world tells you. Footprint calculators answer once and go quiet, habit trackers know nothing about carbon, and most climate content is a countdown that makes people close the tab.
The missing numbers also waste the effort people do make. Thorough recycling saves about 0.2 t CO2e a year and living car-free about 2.4 t (Wynes and Nicholas, 2017). The IPCC finds that demand-side change could cut emissions in end-use sectors by 40 to 70% by 2050 against baseline scenarios, with most of that also needing better infrastructure and policy. The size of the prize is real, and almost nobody can see which of their own habits are part of it.
So we asked: what if every real action got an immediate, honest answer, and that answer was a living place you could watch grow instead of a chart?
What Touch Grass Does
Touch Grass is a living 3D world that your real-world habits grow. Plant a tree in about a minute with no account. Log what you did in a few seconds, by tapping one of 51 actions or typing "I cycled to work and skipped meat today". You immediately get three things:
- An honest estimate: the CO2e avoided, what it was compared with, a likely range and the published source.
- A reason to come back: XP, daily rings, quests and streaks that survive a missed day, because it rains instead of punishing you.
- A visible response: your tree grows in a real-time WebGL scene that follows your local clock and belongs only to you.
Everything runs in the browser and stays there. There is no sign-up, no tracking and no database. The demo opens a fully grown day-200 island in seconds, and every number in it is computed.
Real-World Impact
It points effort at what actually moves the number
These are the app's own estimates at world-average settings, each with a source and range in the app:
| One action | Avoids about | Same as recycling this many plastic bottles |
|---|---|---|
| Train instead of a short flight | 54 kg CO2e | 1,900+ |
| Keep your phone one more year | 15 kg | 520+ |
| Buy second-hand jeans | 13 kg | 460 |
| Plant-based meal instead of beef | 6.3 kg | 220+ |
| Plant-based meal instead of a typical meal with meat | 1.5 kg | 54 |
| Recycle one plastic bottle | 0.028 kg | 1 |
One train trip equals about 15 years of turning off the tap while brushing your teeth. A person cannot feel that from a carbon total, but they can see it the moment the receipt prints. Ranking actions by weight, and showing the ranking, may be the app's most useful function.
Scoring reinforces this without rewarding pollution. Kilograms never convert into XP or growth, so a person with a large footprint cannot win by having more to cut.
It tells the truth about where you live
The same action has a very different effect in different places, and the engine computes that from 45 grid regions (Ember, 2025) plus regional transport factors:
- Line-drying one load of laundry: about 0.04 kg in Norway, 0.33 kg in the UK, 1.02 kg in India and 1.06 kg in South Africa. That is a 25-fold spread for the same act.
- Taking a bus for 5 km instead of driving: about 0.41 kg at world-average factors, 0.62 kg in India and 0.80 kg in the US.
Most climate apps give everyone the same global-average number. Touch Grass tells someone on a coal-heavy grid that saving electricity pays off unusually well, and tells someone on a hydro grid to look at food and travel instead.
What a year adds up to
We ran two ordinary habit patterns through the real engine for 365 days:
| Simulated person | Pattern | Actions logged | Avoided in a year (range) | Roughly the CO2 from driving |
|---|---|---|---|---|
| Occasional | 3 days a week, one small act each | 157 | 106 kg (68 to 179) | 500 km |
| Typical | 6 days a week, a plant-based meal and a 5 km bus trip, plus a refused bottle 3 times a week | 783 | 616 kg (393 to 1,080) | 2,900 km |
For scale, 616 kg is about 13% of the world's average CO2 per person (4.69 t, World Bank 2024) and 28% of India's (2.17 t). The two figures are not on an identical basis: ours is CO2e, theirs is CO2 excluding land use.
The breakdown teaches the same lesson as the table. For the typical person, meals are 40% of the logs and 77% of the kilograms, and refused bottles are 20% of the logs and 2% of the kilograms.
What it could do at scale
We have no production users yet, so this is a model and not a result. Take 10,000 people who plant a tree, and assume 10% keep the typical pattern for a year, 30% keep the occasional one and 60% drop off:
| Low | Central | High | |
|---|---|---|---|
| Reported as avoided | 597 t CO2e | 935 t CO2e | 1,617 t CO2e |
| If only a quarter of that would not have happened without the app | 149 t | 234 t | 404 t |
The central 234 t is roughly the CO2 from 1.1 million km of driving. The retention rates and the one-quarter share are our assumptions, and they are the first things a pilot must measure. Every extra user adds no storage cost, because the core product has no database, so the impact can grow without a matching rise in infrastructure.
Impact beyond carbon
- Action without guilt: a missed day costs nothing permanent. Rain covers it, the tree rests after a week away, and nothing earned is ever taken away. The design is aimed at people who have stopped engaging because climate content makes them feel bad.
- Low barrier to entry: free, no account, no install and no personal data. It was designed for integrated graphics rather than gaming hardware.
- Privacy by architecture: a habit log reveals what you eat, how you travel and when you are home. Touch Grass keeps that on your device.
- Climate literacy: ten short lessons, ten myth cards and 30 daily facts, each with a source. Live NASA, NOAA and World Bank readings sit beside your personal totals.
What we refuse to claim
Figures are self-reported estimates against a stated alternative. They are not measurements, offsets or carbon accounting, and the app never calls them that. Ten actions, such as litter picking and volunteering, have no credible figure, so they earn XP and print nothing. Totals are never converted into "trees planted". A sceptic can check every number on one public methodology page.
How It Works, End to End
1. Procedural Living World
One WebGL scene, generated entirely in code with no external 3D assets, is the product's main interface.
- Seeded, continuously growing tree
- A skeleton of branches, foliage clumps and accents is generated once per seed and species. A generator version is mixed into every random stream, so a saved tree never changes shape.
- A pure pose function reveals that skeleton for any growth value from 0 to 1 into preallocated typed arrays. It is monotonic by construction: wood only extends and thickens.
- Oak, cherry and pine each have their own growth routine and proportion sheet, and all produce the same skeleton format.
- Wind and mood inside the stock lit material
- Crown puffs, leaf cards, blossoms and grass are instanced meshes.
- One sway function of world position is patched into three.js's Lambert vertex shader, so trunk, leaves and grass lean together while staying lit, shadowed and tone-mapped by the engine's own code.
- The same patch grades greens by vitality: straw-yellow when thirsty, grey-blue when dormant.
- Light from the user's clock: sky and key light are a pure function of the local hour, blended between keyframes in OKLab.
- A typed pulse contract: eight pulse kinds (grow, ring, water, level-up, badge, streak, plant, celebrate) run scheduler scripts that write named channels. The scene applies those channels to transforms and shader uniforms each frame, alongside a pooled, instanced particle system.
- Interaction: orbit with inertia, one ray per pointer move against invisible hit proxies, props that say what earned them, and a full-screen Explore mode.
The tree a user sees is a deterministic function of seed, species, growth, health and clock.
2. A Frame Budget Built for Integrated Graphics
Many people open a web app on a laptop with integrated graphics, so that is the machine we designed for.
- One WebGL context, moved through the DOM: the canvas node is re-parented into whichever stage is active, so native compositor scrolling moves it. Layout is read only when the stage changes, and nothing is drawn while the stage is off screen or the tab is hidden.
- Tier by GPU class: the unmasked renderer string is classified as software, integrated, mobile, discrete or Apple silicon. Only the last two start on the high tier.
- Tiers as pixel budgets: 1.1, 1.5 and 2.6 megapixels. The medium tier redraws its 1024 px shadow map on change and otherwise every third frame. Ambient occlusion, bloom and vignette run only on the high tier.
- A frame-pacing governor: it learns the display's interval, judges windows of 30 frames and treats a frame as late at 1.4 intervals. After two slow windows it lowers resolution before it lowers the tier, and it gives a step back after 40 clean windows, at most twice per session.
Measured on an integrated Radeon laptop in a production build: 58 to 60 fps idle, dragging and scrolling, a 16.7 ms median frame, zero pixels of canvas drift, about 40 draw calls and 56,000 triangles. The high tier has not yet been measured on a discrete card.
3. Deterministic Game Engine
All rules live in a pure TypeScript module with no interface code in it.
- One transaction function:
transact(state, now, operation)settles the calendar, runs the operation on a draft, reconciles badges, quests and progression, and returns the next state plus an ordered list of events. The clock is an argument, so the same inputs always give the same result. - One mutation path: the store funnels every change through a single function, so each is atomic and reported as events.
- Three currencies that never convert: XP for effort (level thresholds follow 60 x (L - 1)^1.8), growth points for the tree (a piecewise curve with an asymptotic tail that never completes), and kilograms for impact.
- Anti-farming
- Scoring counts acts, not log entries: acts 1 to 6 of a day pay full XP, 7 to 12 pay half, and later ones pay nothing.
- Logging is capped at 150 XP and 30 growth points a day.
- Nine overlap groups share a cap, so a vegetarian day and its individual meals cannot both be counted.
- Kind streaks: a rain bank covers missed days and refills weekly. The tree turns thirsty, then dormant after seven days, and nothing earned is taken away.
- Content: 32 daily, 24 weekly and 12 epic quests, 33 badges and 16 island props.
- Persistence: saved state passes a runtime schema check that reports the first bad field by path. Unreadable saves are kept in a recovery slot, exports carry a SHA-256 checksum, and imports are validated before they replace anything.
Because the engine is deterministic, tests play three simulated users (lazy, typical and power) through a whole year, compare them with published balance tables, and assert that growth never shrinks on any day. The power user also proves the caps hold against someone logging ten acts a day.
4. The Impact Model
Every figure is an estimate of emissions avoided against a stated alternative, and the interface says so.
- A sourced factor table: 51 actions in seven categories, drawn from 55 sources. The 41 quantified actions each have a central, low and high value, a confidence level (4 high, 26 medium, 11 low) and a written comparison.
- Regional maths: kilograms per unit = non-grid part + kWh per unit x the grid intensity of one of 45 regions, plus regional car, bus and rail factors. Heat actions follow how the home is heated.
- Display rules: an "≈" mark on every figure opens the formula, range and source. Figures use two significant figures, never exceed the published high value and are never negative. Each log stores the factor version it was made with.
- A starting line: a six-question quiz gives a rough yearly footprint (about ±40%), and a pace metric projects the last four weeks against it.
- Comparisons that cannot overclaim: eight equivalences are phrased "roughly the CO2 from", and each carries its own formula and source.
The factor modules are generated by a build script from the evidence base, so the numbers have one source of truth and cannot be hand-tuned in the interface.
5. Live Planet Telemetry Through Our Own Server
The Impact page sets a user's totals beside the state of the planet without the browser ever calling a third party.
- Five readings: global temperature (NASA GISTEMP), atmospheric CO2, methane and nitrous oxide (NOAA), and CO2 per person (World Bank).
- A fallback chain of up to four steps per reading: the publisher's own file, a mirror, the last good answer the server holds, and a snapshot bundled with the app. Methane and nitrous oxide currently start at the mirror.
- Validation before trust: a reading of the wrong shape, outside a physically sensible range or too old is treated as a source that is down.
- Caching: upstream answers are cached for a day, failures are retried after five minutes, and simultaneous requests share one round of upstream calls.
- Truthful freshness: a reading is marked "Live" only when its fetch time, taken from the upstream response's own date header, is under 36 hours old. Otherwise it reads "Snapshot from" and the date.
The page is never empty and never claims to be live when it is not.
6. AI Coach With a Deterministic Fallback
Moss, the coach, works with a model and without one.
- Server-side relay: a route handler speaks the OpenAI-compatible chat protocol to a registry of twelve providers plus any custom endpoint. It detects the provider from the key prefix and fails over down an ordered chain while no token has reached the browser. Keys exist only on the server.
- Streaming: the upstream stream is re-emitted as a small, stable server-sent-event protocol with keep-alive comments, so the client never depends on one vendor's format.
- Guard rails: the system prompt is built on the server, context is sanitised and the user's name is never sent (the model writes a placeholder the browser fills in). The stats block can be switched off. A token bucket allows each visitor 30 requests an hour, with eight upstream calls at once per server instance.
- Action chips: the model can end a message with tokens naming a catalogue action and quantity. A parser validates them, and the user's own tap logs through the same store operation as the Log page. A model can suggest a log but can never write one.
- Built-in coach: with no key, a deterministic intent matcher with weighted patterns answers from the same context block and labels its answers "Built-in".
- Say it in your own words: a sentence is split into parts and matched by the model when one is configured, otherwise by built-in keyword rules and the catalogue's word index. Nothing is logged until the user confirms.
An outage at every AI provider degrades the coach to built-in answers and leaves the product working. The live path has been exercised against simulated providers and not yet against a real provider key.
7. A Demo World That Is Computed, Not Faked
Nobody can log for 200 days during a demo, so the app does it for them.
- Replay: 200 days of an ordinary, imperfect habit are played through the public game API by a seeded generator, in 10 ms slices so the page never freezes.
- Sandbox: the store redirects every read and write to a namespaced view in session storage and stops following other tabs, so the visitor's real save is never read or changed.
- Tour as a state machine: four steps advance on real signals: a pointer on the island, an action-logged event, arriving on Impact and a question sent to the coach.
Every number in the demo world, from its level to its kilograms avoided, was computed by the same rules a real user gets.
8. Local-First by Construction
Nothing in the core loop needs a server.
- No account and no database: state is one validated document in localStorage. There are no cookies, analytics or trackers, and the browser makes no third-party requests.
- Your data: export as JSON or CSV, import, or delete everything.
- Offline: a page that is open keeps working without a connection. There is no service worker yet, so opening the app still needs one.
- No external assets: geometry is generated in code, sounds are synthesised with Web Audio, and fonts are self-hosted.
The only data that can leave the device is a message you choose to send to the coach.
How We Built It
- Framework: Next.js 16.3 (App Router), React 19.2 and TypeScript 5.8 in strict mode. Every feature page, the 3D scene, the charts, the coach panel and the command palette load as separate lazy chunks.
- 3D: three.js 0.186 with React Three Fiber 9 and drei 10, plus postprocessing 6 on the high tier.
- State and rules: Zustand 5 with its persist middleware over a pure rules engine, with memoised selectors that return the same object until a slice they read changes.
- Interface: Tailwind CSS 4 with design tokens, Radix primitives, Framer Motion 12, Recharts 3, cmdk and sonner.
- Server: four route handlers (chat, estimate, status, climate) whose logic sits in platform-free modules, so it can be tested without a server.
- Quality: Vitest 3 with Testing Library (2,168 tests in 117 files), Playwright for end-to-end tests, ESLint and Prettier. About 94,000 lines of application TypeScript and 25,700 lines of tests in
src, across 470 commits. MIT licensed.
How one logged action travels through the system:
tap a sticker / confirm a coach chip / confirm a "say it" proposal
|
v
gameActions.logAction(input) interface (React client components)
|
v
store.run(operation) the single mutation path (Zustand)
|
v
transact(state, now, operation) pure rules engine
1. settle the calendar: days, rain, streak, quest periods
2. run the operation: estimate kg, score the day's acts, apply caps
3. reconcile: badges, quest progress, level and stage changes
|
+--> next state --> persist middleware --> localStorage
| memoised selectors --> React re-renders what changed
| world snapshot { growth, vitality, props, hour }
|
+--> events [action-logged, ring, quest-progress, badge-unlocked, level-up]
|
+--> feedback layer: receipt toast with Undo, sound, queued celebrations
|
+--> world bridge: events --> pulses --> emitPulse({ kind: "grow" })
|
v
pulse scheduler --> channels --> transforms, shader uniforms, particles
Challenges We Ran Into
- 863 ms of style recalculation per second. The first build of the world ran at about 30 fps. Browser performance metrics showed the cause was not WebGL: a 60-second CSS transition on registered custom properties on the root element restyled the whole document every frame while the sky colour moved. It only bit while the colour was changing, which is why the noon test scene looked fine. Removing it took style work from 863 ms to 13 ms per second.
- A canvas that trailed the page by 60 px. A fixed canvas positioned from script each frame cannot keep up with compositor scrolling. We moved the canvas node into the stage itself. Drift went to 0 px and per-frame layout reads disappeared.
- The wrong tier, and grass that paid for shadows per fragment. Choosing quality by core count gave an integrated GPU the high tier at 23 ms of GPU time per frame, and 5,400 grass blades each did a five-tap shadow lookup. Classifying the GPU and using one per-vertex crown-shade term fixed both.
- A scroll hitch on the landing page. The time-lapse previewed a new hour per scroll step, and each step restyled the document for 50 to 60 ms. Writing the previewed sky on the stage element instead of the root cut long frames while scrolling from 34 to 2.
- Clocks that lie. A local-first app trusts the device clock, and that clock can jump. The engine brings a day lived under a fast clock back to the real day and tells the user in one sentence what the correction did.
- Numbers we refused to print. Ten actions have no defensible factor, and several popular statistics were cut from the lessons because we could not confirm them from a primary source. Saying "we don't know" was harder than printing a number, and it was the right call.
Accomplishments We Are Proud Of
- A world that reacts the moment you log, at about 60 fps on integrated graphics.
- 51 actions, 55 sources and 45 grid regions, all public on one methodology page.
- Regional estimates that show a 25-fold difference for the same act, instead of one global average.
- 2,168 automated tests, including year-long simulations of the game economy.
- A responsive audit of every route at ten widths from 320 to 1,920 px, in four saved states, at zero issues.
- three.js and the chart library are in no route's first load.
What We Learned
- Measure before optimising: our worst frame cost was a style rule, not a shader.
- The tier most people land on is the product, so it has to be the designed one.
- A deterministic engine turns game balance into something a test can assert.
- Honesty is a design constraint: saying "estimate" on every figure forced a clearer interface.
- Impact depends on ranking as much as on counting. Showing that a train trip outweighs 1,900 recycled bottles can change more than any total.
- Building for the device first makes the server optional, and that simplifies almost everything.
What's Next
- Measure real impact: run a pilot with a school or workplace, using opt-in, privacy-preserving analytics, to replace our assumed retention and behaviour-change figures with observed ones.
- Accounts with backup and sync: optional, so a tree survives a cleared browser and follows you between devices.
- A service worker, so the app also opens with no connection.
- Household and classroom modes, where the same engine can support group goals.
- Your own factors: your vehicle, your energy bills and hourly grid intensity, which narrow the ranges honestly.
- Kind reminders and a real community: challenges that track both sides, and groups ranked by effort instead of tonnes, with moderation in place first.
- Wider verification: the live coach on real provider keys, physical phones, Safari, Firefox, screen readers, and more languages.
Built With
Next.js, React, TypeScript, Tailwind CSS, three.js, React Three Fiber, drei, postprocessing, WebGL, GLSL, Zustand, Recharts, Radix UI, Framer Motion, cmdk, sonner, Lucide, react-markdown, canvas-confetti, Web Audio API, Web Share API, Server-Sent Events, localStorage, Vitest, Testing Library, Playwright, ESLint, Prettier, Node.js, NASA GISTEMP, NOAA Global Monitoring Laboratory, World Bank Indicators API, Ember electricity data, OpenAI-compatible chat completions API, Groq API (optional)
Built With
- drei
- framer-motion
- glsl
- groq
- localstorage
- nasa-gistemp
- next.js
- noaa-global-monitoring-laboratory
- node.js
- openai-compatible-chat-completions-api
- postprocessing
- radix-ui
- react-three-fiber
- server-sent-events
- sonner
- tailwind-css
- testing-library
- three.js
- typescript
- vitest
- web-audio-api
- web-share-api
- webgl
- world-bank-indicators-api
- zustand

Log in or sign up for Devpost to join the conversation.