/playhtml

interactive, collaborative html elements with a single data attribute

Primary LanguageTypeScriptMIT LicenseMIT

playhtml 🛝🌐 npm release Downloads Size

interactive, collaborative html elements with a single data attribute

playhtml is a fast, small (~300KB), and expressive library for magically creating collaborative interactive HTML elements that persist their state across sessions. For example, you can create a movable piece of HTML "furniture" by adding the can-move attribute:

<div id="couch" can-move style="font-size: 80px">🛋</div>
playhtml-demo-2.mov

If you enjoy this, please consider sponsoring the project or sending a small donation. This helps ensure that the library is maintained and improved over time and funds the hosting costs for the syncing and persistence services.

Usage

vanilla html / simple browser usage

To use this library, you can import the library and the associated styles from a CDN (in this case we will use unpkg.com). Please make sure to do this after all the HTML elements on the page and ensure that the HTML elements you are "magic-ifying" have an id set.

<body>
  <!-- ... html elements here -->
  <!-- valid example -->
  <img
    src="https://media2.giphy.com/media/lL7A3Li0YtFHq/giphy.gif?cid=ecf05e47ah89o71gzz7ke7inrgb1ai1xcbrjnqdf7o890118&ep=v1_stickers_search&rid=giphy.gif"
    can-move
  />
  id="openSign"
  <!-- INVALID EXAMPLE <img src="https://media2.giphy.com/media/lL7A3Li0YtFHq/giphy.gif?cid=ecf05e47ah89o71gzz7ke7inrgb1ai1xcbrjnqdf7o890118&ep=v1_stickers_search&rid=giphy.gif" can-move /> -->
  <!-- import the script -->
  <script type="module">
    import "https://unpkg.com/playhtml@latest";
    playhtml.init();
    // Optionally you could customize the room and host, like so:
    // playhtml.init({
    //  room: window.location.pathname,
    //  host: "mypartykit.user.partykit.dev"
    // })
  </script>
  <link
    rel="stylesheet"
    href="https://unpkg.com/playhtml@latest/dist/style.css"
  />
</body>

If you have dynamic elements that are hydrated after the initial load, you can call playhtml.setupPlayElement(element) to imbue the element with playhtml properties.

<script type="module">
  import { playhtml } from "https://unpkg.com/playhtml@latest";
  const newPlayElement = document.createElement("div");
  newPlayElement.id = "newPlayElement";
  playhtml.setupPlayElement(newPlayElement);
</script>

react / next.js / other frameworks

react @playhtml/react provides components out of the box corresponding to each of the capabilities. It manages all the state syncing for you, so you can reactively render your component based on whatever data is coming in.

Install by using your preferred package manager

npm install @playhtml/react # or
# yarn add @playhtml/react

The callback handlers are slightly different to match the reactive patterns of React. For example, instead of updateElement, which is a vanilla javascript recreation of a reactive state management system, you can simply use your own state hooks or call setData to modify the shared state.

You must import playhtml from the package and call playhtml.init somewhere before your components are rendered. This can be done anywhere in the code (unless you are using a framework with Server-Side Rendering, SSR, like with Next.JS, see below for how to handle this).

// candle
playhtml.init();
export function Candle() {
  return (
    <CanPlayElement
      defaultData={{ on: false }}
      onClick={(_e, { data, setData }) => {
        setData({ on: !data.on });
      }}
    >
      {({ data }) => (
        <img
          src={data.on ? "/candle-gif.gif" : "/candle-off.png"}
          selector-id=".candle"
          className="candle"
        />
      )}
    </CanPlayElement>
  );
}

Refer to packages/react/example.tsx for a full list of examples.

next.js Handling Next is more difficult due to the browser-first nature of playhtml and how you have to handle the same code running server-side with Next.

To initialize playhtml, you'll need to dynamically import playhtml and call init in a useEffect in the top-level component.

async function initPlayhtml() {
  const playhtml = (await import("@playhtml/react")).playhtml;

  playhtml.init();
}
// ... my component
useEffect(() => {
  void initPlayhtml();
});

Then, in your components, you'll need to turn off ssr for dynamically importing the playhtml elements.

import type { CanPlayElement as CanPlayElementType } from "@playhtml/react";
import dynamic from "next/dynamic";
const CanPlayElement = dynamic(
  () => import("@playhtml/react").then((c) => c.CanPlayElement),
  { ssr: false }
) as typeof CanPlayElementType;

To create your own custom element, refer to the can-play section.

If you're trying this out and having trouble, please message me (email, twitter) and I'm happy to help out!

Examples

Check out the full gallery of community examples for more inspiration on what you can do! And submit your own once you've made one.

Capabilities

A full list can be found in types.ts under TagType.

Custom Capabilities

can-play

can-play is the fully customizable experience of playhtml. You can create anything using simple HTML, CSS, and Javascript by simply adding on the functionality needed to the element itself. The library will handle magically syncing and persisting any data that you store.

Here's the simple example included on the playhtml website:

playhtml-candle-can-play.mov
<img can-play id="customCandle" src="/candle-gif.gif" />
<!-- IMPORTANT: this data must be set _before_ importing the playhtml library. -->
<script>
  let candle = document.getElementById("customCandle");
  candle.defaultData = true;
  candle.onClick = (_e, { data, setData }) => {
    setData(!data);
  };
  candle.updateElement = ({ data }) => {
    candle.src = data ? "/candle-gif.gif" : "/candle-off.png";
  };
  candle.resetShortcut = "shiftKey";
  customCandle.defaultData = true;
  customCandle.onClick = (_e, { data, setData }) => {
    setData(!data);
  };
  // The above statement could also be done using the `additionalSetup`
  // which can initialize several different event listeners and custom logic.
  // customCandle.additionalSetup = ({ getData, setData }) => {
  //   customCandle.addEventListener("click", (_e) => {
  //     const data = getData();
  //     setData(!data);
  //   });
  // };
</script>

<!-- Import playhtml -->
<script type="module">
  import "https://unpkg.com/playhtml@latest";
  playhtml.init();
</script>
<link
  rel="stylesheet"
  href="https://unpkg.com/playhtml@latest/dist/style.css"
/>

See all supported properties in the ElementInitializer object in types.ts.

The only required properties are defaultData, updateElement and some kind of setup to trigger those functions (in this case, onClick, but you can add custom event listeners and logic using the additionalSetup property). See more examples based on the definitions for the included capabilities in elements.ts.

If you make something fun, please show me! This is designed as an open library for anyone to add on new interactions and capabilities, so we welcome contributions for new built-in capabilities.

Advanced

IDs are recommended on all elements to uniquely identify them. If you are applying the same capability to several elements, you can also use the selector-id attribute to specify a selector to the elements that distinguishes them. The ID will be the index of the element in that selector query.

Plug-and-play Capabilities

These capabilities are common ones that have been designed and created by the community. You should expect that they are relatively well-tested, and they simply build on top of the same API and constructs that can-play uses.

can-move

playhtml-sign-with-code.mov

Creates a movable element using 2D translate on the element. Dragging the element around will move it

troubleshooting

  • This currently doesn't work on inline display elements.

can-toggle

demos-and-chill-lamp-fight.mov

from https://twitter.com/spencerc99/status/1681048824884895744

Creates an element that can be switched on and off. Clicking the element will toggle the clicked class on the element.

can-duplicate

candle-factory.mov

used to programmatically and dynamically create new playhtml elements that aren't included in the initial HTML

Creates an element that duplicates a target element (specified by the value of the can-duplicate attribute, which can be an element's ID or custom CSS selector) when clicked. Optionally can specify where the duplicate element is inserted in the DOM via the can-duplicate-to setting (default is as a sibling to the original element).

can-grow

Creates an element that can be resized using a scale transform. Clicking the element will grow it, clicking with ALT will shrink it. Currently, the max size is 2x the original size and the min size is 1/2 the original size.

can-spin

Creates a rotatable element using a rotate transform on the element. Dragging the element to the left or right will rotate it counter-clockwise and clockwise respectively.

Contributing

See CONTRIBUTING.md.

Support & Maintenance

Thank you for considering reading this little README and browing this project! I'd love to see you share the library and what you've made with it to me and with your friends. And if you enjoy using this, please consider sponsoring the project or sending a small donation. This helps ensure that the library is maintained and improved over time and funds the hosting costs for the syncing and persistence services.