Elm is a language, but it's sort of automatically a framework as well. There's a function in the standard library called Html.program that takes four functions as arguments and returns a React+Redux style application from them. The Elm runtime exists solely in the browser and expects your main function to return some HTML, which it will display using a virtual DOM. They call this pattern 'the Elm Architecture' (TEA) and recommend that everyone use it. So it's like having a really lightweight framework baked into the standard library and indistinguishable from it.
Those four functions are view (take a model and return some HTML, like a React top-level component's render method), update (take a model and a msg/action and return a new model, like a Redux reducer), init (an initial state) and subscriptions (a list of things to watch for changes and automatically create msgs/actions from, like mouse movement or websockets). When you run a program, it feeds the initial model into the view, and renders it. HTML elements can create messages (dispatch actions) based on DOM events, and subscribed things (like websockets) can also create messages. Every time a message is created, it gets fed into the update function with the existing state, and you branch on the message type to figure out what the new state should be. Then that new state is fed into the view function again and the result is diffed against the virtual DOM and the optimal update made.
The biggest differences between React and Elm are that you don't mess with lifecycle hooks and that you don't repeat this pattern for every sub-component. Instead, you just write lots of small functions that take a model and return some HTML, and you can call those from your top-level view function -- a bit like building everything out of React stateless components.
Writing any Elm code feels a lot like writing React, but with much less boilerplate, reduced complexity, excellent error-catching, and static typing.