PlumeKit Documentation

Behaviour

Plume is not trying to turn every website into an app. Its interactive layer is for small, local behaviour: disclosures, menus, filters, sliders, page transitions and progressive enhancement.

This page works through the layers in order, from plain state bindings to full client scripts. When you are embedding Plume yourself, Embedding explains when these features need the runtime.

Choosing a layer

Use the smallest layer that describes the behaviour:

  • Use HTML and CSS first.
  • Use @state and on:* for local UI state.
  • Use browser actions such as page.scrollToTop or page.measure for common page behaviour.
  • Use @script when an interaction needs several steps or shared event handling.
  • Use @script(language: "javascript") only when you need browser APIs outside Plume's client script language.

State

Sometimes markup needs a bit of local UI state: whether a disclosure is open, what a search field currently holds. Declare it with @state:

PLUME
@state expanded = false

<button on:click="{expanded.toggle()}" aria-expanded="{expanded}">
  {expanded ? "Hide" : "Show"} details
</button>

<section hidden?="{!expanded}" class:open="{expanded}">
  Details
</section>

State can be rendered into:

  • Text content.
  • Attributes.
  • Conditional classes.
  • Optional attributes.
  • Inline style properties.

State is local to the rendered page. It is not a persistence layer and it is not shared with the server.

State actions

State actions are intentionally small:

PLUME
on:click="{expanded.toggle()}"
on:click="{count.increment()}"
on:click="{count.decrement()}"
on:input="{query.set(event.value)}"

These are the supported state actions:

ActionEffect
name.toggle()flips a boolean value
name.set(value)assigns a new value
name.increment()adds one to a number
name.decrement()subtracts one from a number

Form input values

For form controls, event.value is available:

PLUME
@state query = ""

<input value="{query}" on:input="{query.set(event.value)}">
<p hidden?="{query == ''}">Searching for {query}</p>

Page actions

Some interactions are about the page itself rather than a piece of state. Use page for common browser actions:

PLUME
<button on:click="{page.scrollToTop(smooth: true)}">Top</button>

Supported page actions:

  • page.scrollToTop(smooth: true)
  • page.scrollTo(selector: "#main", smooth: true)
  • page.addClass("is-open")
  • page.removeClass("is-open")
  • page.toggleClass("nav-open")
  • page.measure(selector, into: ["x", "width"])

Class actions target the document element by default. Pass target: "body" or another selector to target a specific element.

Measuring elements

Use page.measure when an interaction needs live element geometry but CSS should still do the animation:

PLUME
@state sliderX = 0
@state sliderWidth = 0

<nav on:resize="{page.measure('.nav-link[aria-current=page]', into: ['sliderX', 'sliderWidth'])}">
  <a
    class="nav-link"
    href="/projects/"
    on:pointerenter="{page.measure(event.target, into: ['sliderX', 'sliderWidth'], round: true)}"
  >
    Projects
  </a>

  <span
    class="nav-slider"
    style:--slider-x="{sliderX}px"
    style:--slider-width="{sliderWidth}px"
  ></span>
</nav>

By default, two into values receive the measured element's x and width. Pass properties: ['y', 'height'] when you need different measurements.

Available properties include x, y, width, height, top, left, right, bottom, viewportX, viewportY, centerX and centerY.

Viewport events

on:visible fires when an element enters the viewport:

PLUME
@state introSeen = false

<section on:visible="{introSeen.set(true)}" class:seen="{introSeen}">
  ...
</section>

on:resize and on:scroll run on animation frames and can update state from page geometry.

Scripts

Use @script when an interaction needs more than a single action:

PLUME
@script {
  let menu = page.query("#menu")

  on ".menu-toggle".click {
    event.preventDefault()
    menu.toggleClass("is-open")
  }

  on page.scroll {
    page.toggleClass("is-scrolled", when: page.scrollY > 24)
  }
}

Client scripts support let, var, if, else, for item in items, on target.event blocks, query helpers, class helpers, text and attribute helpers, and scroll helpers. See Client scripts for the full language reference. Use @script(language: "javascript") when you need raw browser APIs.

Keep scripts close to the markup they enhance. If the script belongs to one component instance, use @script(scoped). If it coordinates the whole page, put it in a page or layout template.

Use @navigation when same-origin links should fetch and swap page content instead of doing a full browser reload:

PLUME
@navigation(root: "main", viewTransitions: true, scroll: "top") {
  on:beforeSwap {
    page.addClass("is-leaving")
  }

  on:afterSwap {
    page.removeClass("is-leaving")
  }
}

Put it in a layout template when the whole site should use it.

Options

OptionDefaultPurpose
root"body"the selector for the element swapped on navigation
viewTransitionstrueanimate swaps with the View Transitions API when the browser supports it
scroll"top""top", "preserve" or "none"
minimumDuration0a minimum visit duration in milliseconds
progressBartrueshow a slim progress bar while a visit or intercepted form submission is in flight
progressBarDelay500how long a request must run, in milliseconds, before the bar appears

minimumDuration is useful to let a leave animation finish. progressBarDelay exists so fast navigations never flash the bar. Set progressBar: false to disable the bar entirely.

The progress bar

The progress bar needs no app CSS: the runtime injects its own namespaced style (.plume-progress-bar, 3px tall, fixed to the top of the viewport). To match your brand, override its colour from plain CSS with a custom property:

CSS
:root {
  --plume-progress-color: #16a34a;
}

Hooks

Available hooks:

  • on:start
  • on:beforeSwap
  • on:afterSwap
  • on:complete
  • on:error

Opting out

Use data-plume-navigation="false" on a link to opt out.