trueadm/ripple

What is Ripple?
Currently, this project is still in early development, and should not be used in production.
Ripple is a TypeScript UI framework that takes the best parts of React, Solid and Svelte and combines them into one package.
I wrote Ripple as a love letter for frontend web – and this is largely a project that I built in less than a week, so it’s very raw.
Personally, I (@trueadm) have been involved in some truly amazing frontend frameworks along their journeys – from Inferno, where it all began, to React and the journey of React Hooks, to creating Lexical, to Svelte 5 and its new compiler and signal-based reactivity runtime. Along that journey, I collected ideas, and intriguing thoughts that may or may not pay off. Given my time between roles, I decided it was the best opportunity to try them out, and for open source to see what I was cooking.
Ripple was designed to be a JS/TS-first framework, rather than HTML-first. Ripple modules have their own .ripple
extension, and these modules
fully support TypeScript. By introducing a new extension, it allows Ripple to invent its own superset language, which plays really nicely with
TypeScript and JSX, but with a few interesting touches. In my experience, this has led to better DX not only for humans, but also for LLMs.
Right now, there will be plenty of bugs, things just won’t work either and you’ll find TODOs everywhere. At this stage, Ripple is more of an early alpha version of something that might be, rather than something you should try and adopt. If anything, maybe some of the ideas can be shared and incubated back into other frameworks. There’s also a lot of similarities with Svelte 5, and that’s not by accident; that’s because of my recent time working on Svelte 5.
If you’d like to know more, join the Ripple Discord.
Features
- Reactive State Management: Built-in reactivity with
$
prefixed variables and object properties - Component-Based Architecture: Clean, reusable components with props and children
- JSX-like Syntax: Familiar templating with Ripple-specific enhancements
- Performance: Fine-grain rendering, with industry-leading performance and memory usage
- TypeScript Support: Full TypeScript integration with type checking
- VSCode Integration: Rich editor support with diagnostics, syntax highlighting, and IntelliSense
- Prettier Support: Full Prettier formatting support for
.ripple
modules
Missing Features
- SSR: Ripple is currently an SPA only, this is because I haven’t gotten around to it
- Types: The codebase is very raw with limited types; we’re getting around to it
Getting Started
Try Ripple
We’re working hard on getting an online playground available. Watch this space!
You can try Ripple now by using our basic Vite template either via StackBlitz, or by running these commands in your terminal:
|
|
VSCode Extension
The Ripple VSCode extension provides:
- Syntax Highlighting for
.ripple
files - Real-time Diagnostics for compilation errors
- TypeScript Integration for type checking
- IntelliSense for autocompletion
You can find the extension on the VS Code Marketplace as Ripple for VS Code
.
You can also manually install the extension .vsix
that have been manually packaged.
Mounting your app
You can use the mount
API from the ripple
package to render your Ripple component, using the target
option to specify what DOM element you want to render the component.
|
|
Key Concepts
Components
Define reusable components with the component
keyword. These are similar to functions in that they have props
, but crucially,
they allow for a JSX-like syntax to be defined alongside standard TypeScript. That means you do not return JSX like in other frameworks,
but you instead use it like a JavaScript statement, as shown:
|
|
Ripple’s templating language also supports shorthands and object spreads too:
|
|
Reactive Variables
Variables prefixed with $
are automatically reactive:
|
|
Object properties prefixed with $
are also automatically reactive:
|
|
Derived values are simply $
variables that combined different parts of state:
|
|
That means $count
itself might be derived if it were to reference another reactive property. For example:
|
|
Now given $startingCount
is reactive, it would mean that $count
might reset each time an incoming change to $startingCount
occurs. That might not be desirable, so Ripple provides a way to untrack
reactivity in those cases:
|
|
Now $count
will only reactively create its value on initialization.
Note: you cannot define reactive variables in module/global scope, they have to be created on access from an active component
Transporting Reactivity
Ripple doesn’t constrain reactivity to components only. Reactivity can be used inside other functions (and classes in the future) and be composed in a way to improve expressivity and co-location.
Ripple provides a very nice way to transport reactivity between boundaries so that it’s persisted – using objects and arrays. Here’s an example using arrays to transport reactivity:
|
|
You can do the same with objects too:
|
|
Just remember, reactive state must be connected to a component and it can’t be global or created within the top-level of a module – because then Ripple won’t be able to link it to your component tree.
Reactive Arrays
Just like, objects, you can use the $
prefix in an array literal to specify that the field is reactive.
|
|
Like shown in the above example, you can compose normal arrays with reactivity and pass them through props or boundaries.
However, if you need the entire array to be fully reactive, including when new elements get added, you should use the reactive array that Ripple provides.
You’ll need to import the RippleArray
class from Ripple. It extends the standard JS Array
class, and supports all of its methods and properties.
|
|
The RippleArray
is a reactive array, and that means you can access properties normally using numeric index. However,
accessing the length
property of a RippleArray
will be not be reactive, instead you should use $length
.
Reactive Set
The RippleSet
extends the standard JS Set
class, and supports all of its methods and properties. However,
accessing the size
property of a RippleSet
will be not be reactive, instead you should use $size
.
|
|
RippleSet’s reactive methods or properties can be used directly or assigned to reactive variables.
|
|
Reactive Map
The RippleMap
extends the standard JS Map
class, and supports all of its methods and properties. However,
accessing the size
property of a RippleMap
will be not be reactive, instead you should use $size
.
|
|
RippleMap’s reactive methods or properties can be used directly or assigned to reactive variables.
|
|
Effects
When dealing with reactive state, you might want to be able to create side-effects based upon changes that happen upon updates.
To do this, you can use effect
:
|
|
Control flow
The JSX-like syntax might take some time to get used to if you’re coming from another framework. For one, templating in Ripple
can only occur inside a component
body – you can’t create JSX inside functions, or assign it to variables as an expression.
|
|
Note that strings inside the template need to be inside {"string"}
, you can’t do <div>hello</div>
as Ripple
has no idea if hello
is a string or maybe some JavaScript code that needs evaluating, so just ensure you wrap them
in curly braces. This shouldn’t be an issue in the real-world anyway, as you’ll likely use an i18n library that means
using JavaScript expressions regardless.
If statements
If blocks work seamlessly with Ripple’s templating language, you can put them inside the JSX-like statements, making control-flow far easier to read and reason with.
|
|
For statements
You can render collections using a for...of
block, and you don’t need to specify a key
prop like
other frameworks.
|
|
You can use Ripple’s reactive arrays to easily compose contents of an array.
|
|
Clicking the <button>
will create a new item, note that items
is not $
prefixed, because it’s not
reactive, but rather its properties are instead.
Try statements
Try blocks work to build the foundation for error boundaries, when the runtime encounters
an error in the try
block, you can easily render a fallback in the catch
block.
|
|
Props
If you want a prop to be reactive, you should also give it a $
prefix.
|
|
This also applies to DOM elements, if you want an attribute or property to be reactive, it needs to have a $
prefix.
|
|
Otherwise changes to the attribute or property will not be reactively updated.
Children
Use $children
prop and then use it in the form of <$children />
for component composition.
When you pass in children to a component, it gets implicitly passed as the $children
prop, in the form of a component.
|
|
You could also explicitly write the same code as shown:
|
|
Accessor Props
When working with props on composite components (<Foo>
rather than <div>
), it can sometimes be difficult to debug why a certain value is a certain way. JavaScript gives us a way to do this on objects using the get
syntax:
|
|
So Ripple provides similar capabilities when working with composite components in a template, specifically using $prop:={}
rather than the typical $prop={}
.
In fact, when you use an accessor, you must pass a function, and the prop must be $
prefixed, as Ripple considers accessor props as reactive:
|
|
You can also inline the function too:
|
|
Furthermore, just like property accessors in JavaScript, Ripple provides a way of capturing the set
too, enabling two-way data-flow on composite component props. You just need to provide a second function after the first, separated using a comma:
|
|
Or an inlined version:
|
|
Now changes in the Person
to its props
will propagate to its parent component:
|
|
Decorators
Ripple provides a consistent way to capture the underlying DOM element – decorators. Specifically, using
the syntax {@use fn}
where fn
is a function that captures the DOM element. If you’re familiar with other frameworks, then
this is identical to {@attach fn}
in Svelte 5 and somewhat similar to ref
in React. The hook function will receive
the reference to the underlying DOM element.
|
|
You can also create {@use}
functions inline.
|
|
You can also use function factories to define properties, these are functions that return functions that do the same thing. However, you can use this pattern to pass reactive properties.
|
|
Lastly, you can use decorators on composite components.
|
|
When passing decorators to composite components (rather than HTML elements) as shown above, they will be passed a Symbol
property, as they are not named. This still means that it can be spread to HTML template elements later on and still work.
Event Props
Like React, events are props that start with on
and then continue with an uppercase character, such as:
onClick
onPointerMove
onPointerDown
onKeyDown
For capture
phase events, just add Capture
to the end of the prop name:
onClickCapture
onPointerMoveCapture
onPointerDownCapture
onKeyDownCapture
Note: Some events are automatically delegated where possible by Ripple to improve runtime performance.
Styling
Ripple supports native CSS styling that is localized to the given component using the <style>
element.
|
|
Note: the
<style>
element must be top-level within acomponent
.
Context
Ripple has the concept of context
where a value or reactive object can be shared through the component tree –
like in other frameworks. This all happens from the createContext
function that is imported from ripple
.
When you create a context, you can get
and set
the values, but this must happen within the component. Using them
outside will result in an error being thrown.
|
|
Contributing
We are happy for your interest in contributing. Please see our contributing guidelines for more information.
License
See the MIT license.