shutterclose
v0.2.0
Published
Animated roller-shutter effect for any HTML element — zero dependencies, 1.6 KB gzip, React & Vue included
Maintainers
Readme
shutterclose
An animated roller-shutter effect for any HTML element.
┌─────────────────────────────┐
│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│ ← slat 1
│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│ ← slat 2
│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│ ← slat 3
│ ┌────────┐ │
│ │ CLOSED │ │ ← sign
│ └────────┘ │
└─────────────────────────────┘shutterclose covers any HTML element with smooth sliding slats and an optional customizable sign. Use it for modal overlays, content restrictions, maintenance screens, and creative UI moments. Ships with vanilla, React, and Vue bindings, full TypeScript types, and zero runtime dependencies.
Table of Contents
- Why shutterclose?
- Installation
- Quick Start
- Themes
- API Reference
- CDN / Browser Usage
- CSS Control
- Server-Side Rendering
- TypeScript
- Error Handling
- Contributing
- License
Why shutterclose?
- A genuinely unique effect. Animated, decelerating slats with overshoot give content a physical, mechanical roller-shutter feel that no generic fade or slide reproduces.
- Zero runtime dependencies. Pure JavaScript and CSS. Nothing else lands in your
node_modules. - Framework agnostic. First-class bindings for React and Vue, plus a vanilla API that works anywhere the DOM does.
- Tiny. 1.6 KB gzipped for the core, with separate subpath exports so you only ship the binding you use.
Installation
npm install shutterclosepnpm add shuttercloseyarn add shuttercloseNo build step? Load it straight from a CDN:
<script src="https://unpkg.com/shutterclose/dist/index.global.js"></script>
<link rel="stylesheet" href="https://unpkg.com/shutterclose/dist/shutterclose.css">Quick Start
Vanilla
import ShutterClose from 'shutterclose'
// Close the element
await ShutterClose.close('.my-element', {
sign: { title: 'CLOSED', theme: 'red' }
})
// Open it again
await ShutterClose.open('.my-element')React
import { useState } from 'react'
import { ShutterClose } from 'shutterclose/react'
export function Panel() {
const [shut, setShut] = useState(false)
return (
<ShutterClose isShut={shut} sign={{ title: 'CLOSED' }}>
<p>Content here</p>
<button onClick={() => setShut(!shut)}>Toggle</button>
</ShutterClose>
)
}Vue
<script setup>
import { ref } from 'vue'
import { ShutterClose } from 'shutterclose/vue'
const shut = ref(false)
</script>
<template>
<ShutterClose :is-shut="shut" :sign="{ title: 'CLOSED' }">
<p>Content here</p>
<button @click="shut = !shut">Toggle</button>
</ShutterClose>
</template>Themes
Six built-in sign themes ship out of the box:
🟠 default 🔵 blue 🟢 green
🟣 purple 🟡 gold 🔴 red| Theme | Palette | Suggested Use |
|-------|---------|---------------|
| default | orange / amber | General-purpose closed state |
| blue | professional blue | Informational overlays |
| green | success green | Available / online states |
| purple | modern purple | Branded or premium UI |
| gold | premium gold | Maintenance, highlights |
| red | alert red | Restricted, error, blocked |
Apply a theme through any options object:
await ShutterClose.close('.modal', {
sign: { title: 'CLOSED', theme: 'purple' }
})API Reference
Static API
The default export exposes a static surface for one-off calls, global configuration, and a fluent builder.
ShutterClose.close
ShutterClose.close(target: Target, options?: ShutterCloseOptions): Promise<void>Covers the target with the shutter animation. Resolves when the close animation completes.
await ShutterClose.close('.modal', {
slats: 12,
duration: 1.5,
sign: { title: 'CLOSED', icon: '🔒', theme: 'red' }
})ShutterClose.open
ShutterClose.open(target: Target): Promise<void>Reveals a previously closed target. Resolves when the open animation completes.
await ShutterClose.open('.modal')ShutterClose.target
ShutterClose.target(target: Target): ShutterCloseBuilderReturns a reusable, chainable builder for declarative configuration.
const shutter = ShutterClose.target('.panel')
.slats(16)
.duration(1.8)
.sign({ title: 'CLOSED', subtitle: 'Please try again later' })
.onClose(() => console.log('closed'))
.onOpen(() => console.log('opened'))
await shutter.close()
await shutter.open()| Method | Description |
|--------|-------------|
| .slats(count) | Number of slats (default: 8) |
| .duration(seconds) | Animation duration (default: 2) |
| .heightMultiplier(mult) | Slat starting-height multiplier (default: 3) |
| .deceleration(percent) | Easing deceleration, 0–100 (default: 97) |
| .easing(curve) | Custom CSS easing string |
| .theme(name) | Built-in sign theme |
| .sign(config) | Sign configuration |
| .onClose(fn) | Callback after close completes |
| .onOpen(fn) | Callback after open completes |
| .close() | Execute the close animation |
| .open() | Execute the open animation |
ShutterClose.configure
ShutterClose.configure(config: GlobalConfig): voidSets global defaults shared by every instance and static call.
ShutterClose.configure({
injectCSS: true,
defaults: {
slats: 10,
duration: 1.5,
sign: { title: 'MAINTENANCE' }
}
})Instance API
new ShutterClose(target: Target, options?: ShutterCloseOptions)Construct an instance for fine-grained, stateful control over a single target.
const instance = new ShutterClose('.overlay', {
slats: 12,
sign: { title: 'CLOSED' }
})
await instance.close()
console.log(instance.isShut) // true
await instance.open()
instance.destroy()| Member | Signature | Description |
|--------|-----------|-------------|
| isShut | boolean (read-only) | Whether the target is currently closed |
| close() | () => Promise<void> | Run the close animation |
| open() | () => Promise<void> | Run the open animation |
| destroy() | () => void | Tear down and remove the overlay |
Options
Every option is optional and merges over the configured global defaults.
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| slats | number | 8 | Number of horizontal slats |
| duration | number | 2 | Animation duration in seconds |
| heightMultiplier | number | 3 | Slat starting-height multiplier for overshoot |
| deceleration | number | 97 | Easing deceleration percentage, 0–100 |
| easing | string | — | Custom CSS easing curve |
| sign | SignConfig | — | Closed-sign configuration |
| onClose | () => void | — | Fires after the close animation completes |
| onOpen | () => void | — | Fires after the open animation completes |
SignConfig
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| title | string | Yes | Main text on the sign |
| subtitle | string | No | Secondary text below the title |
| icon | string | No | Icon or emoji, e.g. 🔒, ⚠️ |
| theme | Theme | No | One of the six built-in themes |
React
Import bindings from shutterclose/react.
useShutterClose
useShutterClose(ref: RefObject<HTMLElement>, options?: ShutterCloseOptions)import { useRef } from 'react'
import { useShutterClose } from 'shutterclose/react'
export function Panel() {
const ref = useRef(null)
const { close, open, toggle, isShut } = useShutterClose(ref, {
slats: 12,
sign: { title: 'CLOSED', theme: 'red' }
})
return (
<div ref={ref}>
<p>{isShut ? 'Closed' : 'Open'}</p>
<button onClick={toggle}>Toggle</button>
</div>
)
}| Returned | Type | Description |
|----------|------|-------------|
| close() | () => Promise<void> | Close the shutter |
| open() | () => Promise<void> | Open the shutter |
| toggle() | () => Promise<void> | Toggle the current state |
| isShut | boolean | Current closed state |
<ShutterClose />
import { ShutterClose } from 'shutterclose/react'
<ShutterClose
isShut={shut}
slats={12}
duration={1.5}
sign={{ title: 'CLOSED', icon: '🔒', theme: 'red' }}
onClosed={() => console.log('closed')}
onOpened={() => console.log('opened')}
className="container"
style={{ maxWidth: 500 }}
>
<h1>Content</h1>
</ShutterClose>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| isShut | boolean | false | Controlled closed state |
| onClosed | () => void | — | Fires when close completes |
| onOpened | () => void | — | Fires when open completes |
| className | string | — | Class for the wrapper element |
| style | CSSProperties | — | Inline styles for the wrapper |
| ...ShutterCloseOptions | — | — | All Options are accepted as props |
Vue
Import bindings from shutterclose/vue.
useShutterClose
useShutterClose(el: Ref<HTMLElement | null>, options?: ShutterCloseOptions)<script setup>
import { ref } from 'vue'
import { useShutterClose } from 'shutterclose/vue'
const el = ref(null)
const { close, open, toggle, isShut } = useShutterClose(el, {
slats: 12,
sign: { title: 'CLOSED', theme: 'red' }
})
</script>
<template>
<div ref="el">
<p>{{ isShut ? 'Closed' : 'Open' }}</p>
<button @click="toggle">Toggle</button>
</div>
</template>| Returned | Type | Description |
|----------|------|-------------|
| close() | () => Promise<void> | Close the shutter |
| open() | () => Promise<void> | Open the shutter |
| toggle() | () => Promise<void> | Toggle the current state |
| isShut | Ref<boolean> | Reactive closed state |
<ShutterClose />
<ShutterClose
:is-shut="shut"
:slats="12"
:duration="1.5"
:sign="{ title: 'CLOSED', icon: '🔒', theme: 'red' }"
class="container"
@closed="onClosed"
@opened="onOpened"
>
<h1>Content</h1>
</ShutterClose>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| isShut | boolean | false | Controlled closed state |
| class | string | — | Class for the wrapper element |
| ...ShutterCloseOptions | — | — | All Options are accepted as props |
| Event | Description |
|-------|-------------|
| closed | Emitted when the close animation completes |
| opened | Emitted when the open animation completes |
CDN / Browser Usage
The IIFE build exposes a global named ShutterClose. Drop in two tags and call it directly.
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://unpkg.com/shutterclose/dist/shutterclose.css">
</head>
<body>
<main id="app">
<h1>Welcome</h1>
</main>
<script src="https://unpkg.com/shutterclose/dist/index.global.js"></script>
<script>
ShutterClose.close('#app', {
slats: 16,
duration: 1.5,
sign: { title: 'CLOSED', theme: 'gold' }
})
</script>
</body>
</html>CSS Control
By default the core auto-injects its stylesheet into <head>, so no extra setup is needed.
import ShutterClose from 'shutterclose'
// Styles are injected automatically.For full control over loading order or bundling, use the no-css entry and import the stylesheet yourself.
import ShutterClose from 'shutterclose/no-css'
import 'shutterclose/shutterclose.css'
ShutterClose.configure({ injectCSS: false })The stylesheet exposes CSS custom properties for theming.
.sc-shutter {
--sc-duration: 2s;
--sc-easing: cubic-bezier(0.25, 0.46, 0.45, 0.94);
--sc-start-y: -300%;
}
.sc-slat {
background: linear-gradient(135deg, #333 0%, #444 100%);
}Server-Side Rendering
The CSS injector guards on typeof document === 'undefined', so importing shutterclose is safe in SSR environments such as Next.js and Nuxt.
TypeScript
shutterclose is authored in strict TypeScript and ships complete .d.ts declarations for every entry point.
import type { ShutterCloseOptions, SignConfig, Theme } from 'shutterclose'
import ShutterClose from 'shutterclose'
const options: ShutterCloseOptions = {
slats: 10,
duration: 1.5,
sign: { title: 'CLOSED', theme: 'red' }
}
await ShutterClose.close('.modal', options)Exported types: ShutterCloseOptions, SignConfig, Theme, Target, GlobalConfig, and ShutterCloseTargetError.
Error Handling
A ShutterCloseTargetError is thrown when a selector matches no element.
import ShutterClose, { ShutterCloseTargetError } from 'shutterclose'
try {
await ShutterClose.close('.does-not-exist')
} catch (err) {
if (err instanceof ShutterCloseTargetError) {
console.error('Target not found:', err.message)
}
}Contributing
Contributions are welcome. Open an issue or a pull request on GitHub. CI runs on Node 18 and 20 via GitHub Actions, and releases are managed with Changesets.
License
MIT. See LICENSE for details.
