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
.ripplemodules
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
.ripplefiles - 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:
onClickonPointerMoveonPointerDownonKeyDown
For capture phase events, just add Capture to the end of the prop name:
onClickCaptureonPointerMoveCaptureonPointerDownCaptureonKeyDownCapture
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.