{"id":13682358,"url":"https://github.com/restaumatic/purescript-specular","last_synced_at":"2026-02-20T16:32:57.066Z","repository":{"id":40371917,"uuid":"112614520","full_name":"restaumatic/purescript-specular","owner":"restaumatic","description":"A Reflex-Dom inspired UI library for PureScript","archived":false,"fork":false,"pushed_at":"2025-08-21T15:25:57.000Z","size":1168,"stargazers_count":133,"open_issues_count":6,"forks_count":8,"subscribers_count":18,"default_branch":"master","last_synced_at":"2026-01-23T17:41:40.551Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PureScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/restaumatic.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2017-11-30T13:27:51.000Z","updated_at":"2025-10-24T14:08:19.000Z","dependencies_parsed_at":"2024-01-08T14:30:41.654Z","dependency_job_id":"21d93ccd-3058-4043-9685-b5f7825c35f9","html_url":"https://github.com/restaumatic/purescript-specular","commit_stats":{"total_commits":326,"total_committers":11,"mean_commits":"29.636363636363637","dds":"0.18404907975460127","last_synced_commit":"b28f97dfbaeea7b2d2da86e1391f9108a95c229f"},"previous_names":[],"tags_count":41,"template":false,"template_full_name":null,"purl":"pkg:github/restaumatic/purescript-specular","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/restaumatic%2Fpurescript-specular","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/restaumatic%2Fpurescript-specular/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/restaumatic%2Fpurescript-specular/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/restaumatic%2Fpurescript-specular/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/restaumatic","download_url":"https://codeload.github.com/restaumatic/purescript-specular/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/restaumatic%2Fpurescript-specular/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29656978,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-20T09:27:29.698Z","status":"ssl_error","status_checked_at":"2026-02-20T09:26:12.373Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-02T13:01:44.700Z","updated_at":"2026-02-20T16:32:57.034Z","avatar_url":"https://github.com/restaumatic.png","language":"PureScript","readme":"# Specular [![CI](https://github.com/restaumatic/purescript-specular/actions/workflows/ci.yml/badge.svg)](https://github.com/restaumatic/purescript-specular/actions/workflows/ci.yml)\n\nSpecular is a library for building Web-based UIs in PureScript, based on\nFunctional Reactive Programming (FRP).\n\nThe API and DOM interaction is heavily inspired by [Reflex][reflex] and [Reflex-DOM][reflex-dom].\nThe FRP implementation is based on [Incremental](https://github.com/janestreet/incremental) (although the algorithm differs in some important ways).\n\n## API\n\n### FRP types\n\nTo use Specular effectively, you need to be familliar with some basic types.\n\n#### [Dynamic](https://pursuit.purescript.org/packages/purescript-specular/docs/Specular.FRP.Base#t:Dynamic)\n\n`Dynamic a` represents a read-only reference to a changing value of type `a`.\n\n```purescript\n-- | Read the current value of a `Dynamic`.\nreadDynamic :: forall m a. MonadEffect m =\u003e Dynamic a -\u003e m a\n\n-- | Execute the given action for the current value, and each new value when it changes.\nsubscribeDyn_ :: forall m a. MonadEffect m =\u003e MonadCleanup m =\u003e (a -\u003e Effect Unit) -\u003e Dynamic a -\u003e m a\n```\n\n`Dynamic` is a `Monad`.\n\n```purescript\n-- `pure` creates a Dynamic that never changes.\npure \"foo\" :: Dynamic String\n\n-- An applicative combination of Dynamics changes whenever one of them changes.\nd1 :: Dynamic Int\nd2 :: Dynamic Int\n(+) \u003c$\u003e d1 \u003c*\u003e d2 :: Dynamic Int\n\n-- Using the power of Monad we can choose which Dynamic to observe.\nwhich :: Dynamic Bool\n(which \u003e\u003e= if _ then d1 else d2) :: Dynamic Int\n```\n\nWe can introduce new root Dynamics using `newDynamic`. Root Dynamics are read-write\nand will be replaced by [Refs](https://pursuit.purescript.org/packages/purescript-specular/docs/Specular.Ref#t:Ref) in the future, since they are almost the same.\n\n\n```purescript\n-- | Construct a new root Dynamic that can be changed from `Effect`-land.\nnewDynamic :: forall m a. MonadEffect m =\u003e a -\u003e m { dynamic :: Dynamic a, read :: Effect a, set :: a -\u003e Effect Unit, modify :: (a -\u003e a) -\u003e Effect Unit }\n```\n\n#### [Event](https://pursuit.purescript.org/packages/purescript-specular/docs/Specular.FRP.Base#t:Event)\n\n`Event a` represents a source of occurences. Each occurence carries a value of type `a`.\n\n`Event` is a `Functor`.\n\nWe can construct a trivial event `never :: forall a. Event a`, which never occurs.\n\nEvents can be combined:\n\n```purescript\n-- | An Event that occurs when any of the events occur. If some of them occur simultaneously, the occurence value is that of the leftmost one.\nleftmost :: forall a. Array (Event a) -\u003e Event a\n```\n\nEvents can be transformed:\n\n```purescript\n-- | Retain only the occurences of the event for which the given predicate function returns `true`.\nfilterEvent :: forall a. (a -\u003e Boolean) -\u003e Event a -\u003e Event a\n\n-- | Map the given function over an Event, and retain only the occurences for which it returned a Just value.\nfilterMapEvent :: forall a b. (a -\u003e Maybe b) -\u003e Event a -\u003e Event b\n\n-- | Retain only the occurences of the Event which contain a Just value.\nfilterJustEvent :: forall a. Event (Maybe a) -\u003e Event a\n```\n\nWe can observe `Event`s by being notified of their occurences.\n\n```purescript\n-- | Execute the given action for each occurence of the Event.\nsubscribeEvent_ :: forall m a. MonadEffect m =\u003e MonadCleanup m =\u003e (a -\u003e Effect Unit) -\u003e Event a -\u003e m a\n```\n\n\n#### [Ref](https://pursuit.purescript.org/packages/purescript-specular/docs/Specular.Ref#t:Ref)\n\n`Ref a` represents a read-write reference to a mutable observable variable.\n\nWe can think of a `Ref` as of `Effect.Ref`, but with additional functions:\n- the ability to notify subscribers about changes to the value,\n- the ability to focus using a lens.\n\n`Ref a` consists of:\n- `Ref.value :: Ref a -\u003e Dynamic a` to observe the value\n- `Ref.modify :: Ref a -\u003e (a -\u003e a) -\u003e Effect Unit` to modify the value using a function\n\nAs a shortcut we have `Ref.write :: Ref a -\u003e a -\u003e Effect Unit` to replace the value completely,\nand a `Ref.read :: forall a. =\u003e Ref a -\u003e Effect a` to read the current value of a `Ref`.\n\nCreating a Ref:\n\n```purescript\nRef.new :: forall a. a -\u003e Effect (Ref a)\n```\n\n`Ref` is not a `Functor`, because it's read-write. It's `Invariant`, that is, it can be mapped over using a bijection.\n\nThis API will also likely change in the future, so that our interface resembles a standard [Ref](https://pursuit.purescript.org/packages/purescript-refs/5.0.0/docs/Effect.Ref#t:Ref)\n\n### Building DOM content\n\n#### [Widget](https://pursuit.purescript.org/packages/purescript-specular/docs/Specular.Dom.Widget#t:Widget)\n\n`Widget a` is a computation which can perform `Effects`, produce DOM nodes, subscribe to Events and Dynamics and returns a value of type `a`.\n\n`Widget`s can be executed using `runMainWidgetInBody` - their contents will be inserted into the `document.body` element.\n\n#### [Prop](https://pursuit.purescript.org/packages/purescript-specular/docs/Specular.Dom.Element#t:Prop)\n\n`Prop` is a modifier attached to a DOM element. Specific ways to construct a `Prop` are presented below.\n\n#### [Attrs](https://pursuit.purescript.org/packages/purescript-specular/docs/Specular.Dom.Browser#t:Attrs)\n\n`Attrs` is a map of HTML attributes.\n\n```purescript\n-- A singleton map can be constructed using the `:=` operator.\n\"type\":=\"button\" :: Attrs\n\n-- Attrs can be combined using the Monoid instance.\n\"type\":=\"button\" \u003c\u003e \"name\":=\"btn\" :: Attrs\n```\n\n#### Static DOM\n\n```purescript\nimport Specular.Dom.Element\n\n-- | Produce a text node.\ntext :: String -\u003e Widget Unit\n\n-- | `el tag props body` - Produce a DOM Element.\n-- |\n-- | The elements produced by the `body` widget will be inserted as children of the element.\nel :: forall a. TagName -\u003e Array Prop -\u003e Widget a -\u003e Widget a\n\n-- | `el tag props body` - Produce a DOM Element with no props.\nel_ :: forall a. TagName -\u003e Widget a -\u003e Widget a\n\n-- | Attach static attributes to the element.\nattrs :: Attrs -\u003e Prop\n\n-- | Attach a static attribute to the element.\nattr :: AttrName -\u003e AttrValue -\u003e Prop\n\n-- | Attach CSS classes to the element\nclasses :: [ClassName] -\u003e Prop\n\n-- | Attach a CSS class to the element\nclass_ :: ClassName -\u003e Prop\n```\n\nFor example, to produce the following HTML:\n\n```html\n\u003cdiv class=\"alert alert-warning alert-dismissible fade show\" role=\"alert\"\u003e\n  \u003cstrong\u003eHoly guacamole!\u003c/strong\u003e You should check in on some of those fields below.\n  \u003cbutton type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\"\u003e\n    \u003cspan aria-hidden=\"true\"\u003e\u0026times;\u003c/span\u003e\n  \u003c/button\u003e\n\u003c/div\u003e\n```\n\nOne would write the following Specular code:\n\n```purescript\nel \"div\" [classes [\"alert\", \"alert-warning\", \"alert-dismissible\", \"fade\", \"show\"], attr \"role\" \"alert\"] do\n  el_ \"strong\" $ text \"Holy guacamole!\"\n  text \" You should check in on some of those fields below.\"\n  el \"button\" [class_ \"close\", attrs (\"type\":=\"button\" \u003c\u003e \"data-dismiss\":=\"alert\" \u003c\u003e \"aria-label\":=\"Close\")] do\n    el \"span\" [attr \"aria-hidden\"  \"true\"] do\n      text \"×\"\n```\n\n#### Dynamic text, attributes and classes\n\nMost of the `Prop` constructors have their dynamic counterparts. As a convention, their names end in `D`. For example:\n\n```purescript\n-- | Attach dynamic attributes to the element.\nattrsD :: Dynamic Attrs -\u003e Prop\n\n-- | Attach dynamic CSS classes to the element\nclassesD :: Dynamic [ClassName] -\u003e Prop\n```\n\n`text` also has a dynamic counterpart:\n\n```purescript\n-- | Create a text node whose text will reflect the value of the given Dynamic.\ndynText :: Dynamic String -\u003e Widget Unit\n```\n\nFor convenience, utilities for common cases are provided such as:\n\n```purescript\nattrWhenD :: Dynamic Boolean -\u003e AttrName -\u003e AttrValue -\u003e Prop\nclassWhenD :: Dynamic Boolean -\u003e ClassName -\u003e Prop\n```\n\nFor example: assume you have `name :: Dynamic String`.\nThe code:\n\n```purescript\nlet isLong nm = String.length nm \u003e= 5\nel \"div\" [class_ \"name\", classWhenD (isLong \u003c$\u003e name) \"long\"] do\n  text \"Your name is: \"\n  dynText name\n```\n\nwhen `name` has value `\"Jan\"`, would produce\n\n```html\n\u003cdiv class=\"name\"\u003eYour name is Jan\u003c/div\u003e\n```\n\nwhereas when `name` has value `\"Titelitury\"`, would produce\n\n```html\n\u003cdiv class=\"name long\"\u003eYour name is Titelitury\u003c/div\u003e\n```\n\n#### Dynamic DOM structure\n\nSometimes changing text and attributes is not enough. For that there's `withDynamic_`:\n\n```purescript\nwithDynamic_ :: forall a. Dynamic a -\u003e (a -\u003e Widget Unit) -\u003e Widget Unit\n```\n\nWhenever the Dynamic changes, it will re-render a new `Widget` based on the latest value.\n\nExample:\n\n```purescript\n-- Assume loading :: Dynamic Boolean\n\nwithDynamic_ loading $\n  if _ then\n    el \"div\" [class_ \"loading\"] $ text \"Loading...\"\n  else\n    el_ \"div\" do\n      el_ \"h1\" $ text \"Content\"\n      el_ \"p\" $ text \"Bla bla bla\"\n```\n\nWarning: Re-rendering a whole DOM block on each change has performance implications. Use with care.\n\n#### Handling events\n\n```purescript\n-- | Connect a DOM event on the node to a callback.\non :: EventType -\u003e (DOM.Event -\u003e Effect Unit) -\u003e Prop\n\n-- | Shorthand: `on \"click\"`\nonClick :: (DOM.Event -\u003e Effect Unit) -\u003e Prop\n\n-- | Like `onClick`, but takes a callback which ignores the DOM event.\nonClick_ :: Effect Unit -\u003e Prop\n```\n\nExample:\n\n```purescript\n-- Assume save :: Effect Unit\n\nel \"button\" [attr \"type\" \"button\", onClick_ save] do\n  text \"Save\"\n```\n\n\nFor inputs, we have predefined props that make `change` and `input` events handling easier (available in [Specular.Dom.Element](https://pursuit.purescript.org/packages/purescript-specular/docs/Specular.Dom.Element))\n\n```purescript\n-- * Input value\n-- | Attach dynamically-changing `value` property to an input element.\n-- | The value can still be changed by user interaction.\n-- |\n-- | Only works on `\u003cinput\u003e` and `\u003cselect\u003e` elements.\nvalueD :: Dynamic String -\u003e Prop\n\n-- | Set up a two-way binding between the `value` of an `\u003cinput\u003e` element,\n-- | and the given `Ref`.\n-- |\n-- | The `Ref` will be updated on `change` event, i.e. at the end of user interaction, not on every keystroke.\n-- |\n-- | Only works on input elements.\nbindValueOnChange :: Ref String -\u003e Prop\n\n\n-- | Attach dynamically-changing `checked` property to an input element.\n-- | The value can still be changed by user interaction.\n-- |\n-- | Only works on input `type=\"checkbox\"` and `type=\"radio\"` elements.\ncheckedD :: Dynamic Boolean -\u003e Prop\n\n-- | Set up a two-way binding between the `checked` of an `\u003cinput\u003e` element,\n-- | and the given `Ref`.\n-- |\n-- | Only works on input `type=\"checkbox\"` and `type=\"radio\"` elements.\nbindChecked :: Ref Boolean -\u003e Prop\n\n```\n\nExample:\n\n```purescript\n\nimport Prelude\nimport Specular.Ref (Ref, newRef)\nimport Specular.Dom.Browser ((:=))\n\nimport Specular.Dom.Element (el, attr, bindValueOnChange)\nimport Specular.Dom.Widget (emptyWidget)\n\nlet description :: Ref String = newRef \"\"\n\nel \"input\" [attr \"type\" \"text\", bindValueOnChange description] emptyWidget\n\n```\n\n\n#### A Counter example\n\n```purescript\nmodule Main where\n\nimport Prelude\nimport Effect (Effect)\n\n\nimport Specular.Dom.Element (attr, class_,  el,  onClick_, text, dynText)\nimport Specular.Dom.Widget (runMainWidgetInBody)\nimport Specular.Ref (Ref)\nimport Specular.Ref as Ref\n\n\nmain :: Effect Unit\nmain = do\n  -- | Will append widget to the body\n  runMainWidgetInBody do\n    counter :: Ref Int \u003c- Ref.new 0\n\n    -- | Subtract 1 from counter value\n    let subtractCb = (Ref.modify counter) (add (negate 1))\n\n    -- | Add 1 to counter value\n    let addCb =  (Ref.modify counter) (add 1)\n\n    el \"button\" [class_ \"btn\", attr \"type\" \"button\", onClick_ addCb ] do\n      text \"+\"\n\n    dynText $ show \u003c$\u003e Ref.value counter\n\n\n    el \"button\" [class_ \"btn\", attr \"type\" \"button\", onClick_ subtractCb ] do\n      text \"-\"\n```\n\n\u003cp class=\"callout warning\"\u003eWarning: examples which can be found in this repo which are using \"FixFRP\" are deprecated !\u003c/p\u003e\n\n\n## Getting started - using starter app\n\nClone this repository and start hacking: https://github.com/restaumatic/purescript-specular-starter\n\n## Getting started - manually\n\nWe will use spago in this example, because spago allows us to override package sets.\n\nInitialize a repository and install purescript\n\n- `npm init`\n- `npm install --save-dev purescript@0.14.3`\n- `npm install --save-dev spago`\n\nAdd `node_modules/.bin` to path:\n- `export PATH=\"./node_modules/.bin:$PATH\"`\n\nInitialize `spago`:\n\n- `spago init`\n\nto check if everything is working so far:\n- `spago build`\n\n\nSince `Specular` is not in an official `package-set`, you will have to add it manually,\nby appending `with specular` to your `in upstream` block in `packages.dhall` file.\n\n```dhall\n-- Something like this will exist in your packages.dhall\nlet upstream =\n      https://github.com/purescript/package-sets/releases/download/psc-0.14.3-20210716/packages.dhall sha256:1f9af624ddfd5352455b7ac6df714f950d499e7e3c6504f62ff467eebd11042c\n\nin  upstream\n  with specular =\n    { dependencies =\n      [ \"prelude\"\n      , \"aff\"\n      , \"typelevel-prelude\"\n      , \"record\"\n      , \"unsafe-reference\"\n      , \"random\"\n      , \"debug\"\n      , \"foreign-object\"\n      , \"contravariant\"\n      , \"avar\"\n      ]\n    , repo = \"https://github.com/restaumatic/purescript-specular.git\"\n    , version = \"master\"\n    }\n```\n\nInstall specular:\n- `spago install specular`\n- `spago build`\n\nReplace the content of `src/Main.purs` with the counter example, and run:\n- `spago bundle-app`\n\nCreate and open `index.html` file.\n```html\n\u003chtml\u003e\n  \u003cbody\u003e\n    \u003cscript\u003ewindow.global = {}\u003c/script\u003e\n    \u003cscript src=\"index.js\"\u003e\u003c/script\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\nThe ugly global is required for now (possibly a browserify artifact).\n\nIf everything worked correctly, there should be a Spec(ta)ular counter!  :)\n\n## Why not just use Reflex and GHCJS?\n\nIn short: code size. Specular demos are 240K unminified (with DCE - `pulp build\n-O`), or 19K minified with `uglifyjs -c -m` and gzipped. In contrast, a a GHCJS\n(`0.2.1.9007019`) program that prints `Hello World` (no DOM bindings included,\njust `base`) weighs `1.1M` unminified, or 62K minified with Closure Compiler's\n`ADVANCED_OPTIMIZATIONS` and gzipped. Supporting Haskell semantics has a cost.\n\nThere are also other reasons, of course.\n\n## Why not use other PureScript UI libraries?\n\nSee [Motivation](doc/Motivation.md).\n\n## Limitations\n\nSome of the cons of Specular:\n\n- No good way to do server-side rendering. Local state complicates this.\n\n- Performance may be sometimes bad, because it does not use any Virtual DOM -\n  the element placement instructions you write translate pretty much directly to\n  `createElement`/`appendChild`. There are no (representative) benchmarks yet.\n\n- Time travel debugging, as known from Elm, is not possible.\n\n- Currently no way to bind to React Native.\n\n- Programs written with Specular may be harder to understand for some people who\n  prefer the single state variable approach.\n\n- Compared to Reflex, it has way less FRP combinators.\n\n- Creating recursive data flows is more cumbersome than in Reflex, because\n  PureScript has eager evaluation and no `RecursiveDo`.\n\n- It's immature and not popular, and may have bugs.\n\nIf you think there are more, please open an issue. They should be listed.\n\n[reflex]: https://github.com/reflex-frp/reflex\n[reflex-dom]: https://github.com/reflex-frp/reflex-dom\n\n## Who's using it?\n\n- **Restaumatic** - used in production for a signification portion of online ordering frontend, as well as for backoffice apps and our mobile app for restaurants.\n\n## Contact\n\nIf you discover bugs, want new features, or have questions, please post an\nissue using the GitHub issue tracker, or use [GitHub Discussions](https://github.com/restaumatic/purescript-specular/discussions).\n\nYou can also use the [Gitter chat](https://gitter.im/purescript-specular/community).\n","funding_links":[],"categories":["PureScript","UI Libraries"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frestaumatic%2Fpurescript-specular","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frestaumatic%2Fpurescript-specular","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frestaumatic%2Fpurescript-specular/lists"}