SM2KB

Browser-native since 2015

The framework that stayed small while the web grew.

Sheriff’s 2KB framework is a compact layer over the DOM: selectors, events, readiness, styling, creation, and convention-driven component extension—without a build step or virtual DOM.

repository bytes
core primitives
build tools required
sheriff.http.jsB
<script src="https://js.msheriff.com/sheriff.http.js"></script>
<button class="signal extends classCounter" value="0">Count: 0</button>
class Counter {
    onclick() {
      this.textContent = `Count: ${++this.value}`
    }
}

Not a replacement for every application.

A reminder that the browser already ships with a remarkably capable runtime.

Own the code. Keep the payload legible.

01 / EVOLUTION

A decade measured in ideas, not major versions.

The public file history shows concentrated moments of refinement. Each step adds leverage while keeping the same browser-first center.

2015First public roots

A tiny vocabulary for the DOM

The earliest commits establish the direct style: select nodes, wait for readiness, attach behavior, create elements, and move on.

  • Zero compilation
  • Global, memorable helpers
  • Native DOM underneath
id · first · all · style · create
2017The 2KB identity

Events and readiness crystallize

The file announces itself as “Sheriff’s 2KB JS framework” and consolidates its event and startup primitives around a deliberately small API.

  • Queued ready()
  • Collection-aware on()
  • Lightweight A/B hooks
on(el, event, handler)
2018Components without ceremony

Convention-driven extension arrives

Node.extends and NodeList.extends turn ordinary elements into component instances, wiring on* methods to browser events.

  • addEventListener
  • One-shot events
  • Class-name activation
<ul class="extends classListItem">
2022Small, safer, still direct

The modern snapshot

Startup moves to DOMContentLoaded, component lookup drops dynamic evaluation, and the public API remains readable in one screen.

  • No runtime dependencies
  • No virtual DOM
  • No framework-owned state model
document + this
2026One core · one include

A standalone core and a combined HTTP distribution

/framework.v2026.js keeps the modern core independently available. Version 2026.2 resolves both window-backed constructors and bare top-level class declarations; /sheriff.http.js embeds that exact core before its labeled HTTP and cache source.

  • extends className detects bare class Name {}
  • Core remains independently usable
  • HTTP adds cached queries and mutations
Sheriff.http ·

02 / PAYLOAD

Small enough to understand before your coffee cools.

Uncompressed browser asset or repository-file sizes, pinned to the versions shown. This is a payload perspective—not a feature-equivalence benchmark.

minified distribution
package source
production UMD + ReactDOM
global production build

PUBLIC API INVENTORY

The callable surface, not just the payload.

Function names are taken from the exact source or browser distributions pinned above. Open a framework to inspect its callable public exports.

ScriptMaster 2KB functions · all.js source

on · once · ready · id · first · all · style · create · Node.extends · NodeList.extends

Sheriff 2026 core + HTTP functions · combined include

Sheriff.debounce · Sheriff.each · Sheriff.emit · Sheriff.extend · Sheriff.escapeHtml · Sheriff.formData · Sheriff.html · Sheriff.many · Sheriff.on · Sheriff.one · Sheriff.render · Sheriff.signal · Sheriff.uid · signal.get · signal.set · signal.subscribe · signal.update · Sheriff.http.createApi · endpoint.initiate · endpoint.prefetch · endpoint.select · endpoint.subscribe · api.cache.invalidateTags · api.cache.reset · api.cache.metrics

functions · minified distribution

$ · $.fn.each · $.type · $.ajax · $.fn.on · $.fn.off · $.fn.html · $.fn.addClass · $.fn.removeClass · $.fn.css · $.fn.append · $.src

exported functions · package source

app · h · memo · text

callable exports · production UMD

React.Component · React.PureComponent · React.act · React.cloneElement · React.createContext · React.createElement · React.createFactory · React.createRef · React.forwardRef · React.isValidElement · React.lazy · React.memo · React.startTransition · React.unstable_act · React.useCallback · React.useContext · React.useDebugValue · React.useDeferredValue · React.useEffect · React.useId · React.useImperativeHandle · React.useInsertionEffect · React.useLayoutEffect · React.useMemo · React.useReducer · React.useRef · React.useState · React.useSyncExternalStore · React.useTransition · ReactDOM.createPortal · ReactDOM.createRoot · ReactDOM.findDOMNode · ReactDOM.flushSync · ReactDOM.hydrate · ReactDOM.hydrateRoot · ReactDOM.render · ReactDOM.unmountComponentAtNode · ReactDOM.unstable_batchedUpdates · ReactDOM.unstable_renderSubtreeIntoContainer

callable exports · global production build

EffectScope · ReactiveEffect · Transition · VueElement · assertNumber · callWithAsyncErrorHandling · callWithErrorHandling · camelize · capitalize · cloneVNode · compile · computed · createApp · createBlock · createCommentVNode · createElementBlock · createElementVNode · createHydrationRenderer · createPropsRestProxy · createRenderer · createSSRApp · createSlots · createStaticVNode · createTextVNode · createVNode · customRef · defineAsyncComponent · defineComponent · defineCustomElement · defineEmits · defineExpose · defineModel · defineOptions · defineProps · defineSSRCustomElement · defineSlots · effect · effectScope · getCurrentInstance · getCurrentScope · getCurrentWatcher · getTransitionRawChildren · guardReactiveProps · h · handleError · hasInjectionContext · hydrate · hydrateOnIdle · hydrateOnInteraction · hydrateOnMediaQuery · hydrateOnVisible · initCustomFormatter · initDirectivesForSSR · inject · isMemoSame · isProxy · isReactive · isReadonly · isRef · isRuntimeOnly · isShallow · isVNode · markRaw · mergeDefaults · mergeModels · mergeProps · nextTick · normalizeClass · normalizeProps · normalizeStyle · onActivated · onBeforeMount · onBeforeUnmount · onBeforeUpdate · onDeactivated · onErrorCaptured · onMounted · onRenderTracked · onRenderTriggered · onScopeDispose · onServerPrefetch · onUnmounted · onUpdated · onWatcherCleanup · openBlock · patchProp · popScopeId · provide · proxyRefs · pushScopeId · queuePostFlushCb · reactive · readonly · ref · registerRuntimeCompiler · render · renderList · renderSlot · resolveComponent · resolveDirective · resolveDynamicComponent · resolveTransitionHooks · setBlockTracking · setDevtoolsHook · setTransitionHooks · shallowReactive · shallowReadonly · shallowRef · stop · toDisplayString · toHandlerKey · toHandlers · toRaw · toRef · toRefs · toValue · transformVNodeArgs · triggerRef · unref · useAttrs · useCssModule · useCssVars · useHost · useId · useModel · useSSRContext · useShadowRoot · useSlots · useTemplateRef · useTransitionState · warn · watch · watchEffect · watchPostEffect · watchSyncEffect · withAsyncContext · withCtx · withDefaults · withDirectives · withKeys · withMemo · withModifiers · withScopeId

03 / CHOICE TABS

One interaction. Six framework dialects.

Choose a framework to compare the same two-choice tab interface. Each panel shows a complete, runnable-shaped example for the exact version listed in the payload comparison.

ORIGINAL

ScriptMaster 2KB

The historical extends className convention binds onclick directly to the component root.

<script src="https://js.msheriff.com/framework.js"></script>
<div class="choice-tabs extends classChoiceTabs">
  <button data-choice="core">Core</button>
  <button data-choice="http">HTTP</button>
  <p class="choice-panel" data-panel="core">Core selected</p>
  <p class="choice-panel" data-panel="http" hidden>HTTP selected</p>
</div>
var ChoiceTabs = class {
    onclick(event) {
      const tab = event.target.closest('[data-choice]')
      if (!tab) return
      this.$$('button').forEach(button =>
        button.classList.toggle('active', button === tab))
      this.$$('.choice-panel').forEach(panel =>
        panel.hidden = panel.dataset.panel !== tab.dataset.choice)
    }
}

04 / LIVE RUNTIME

This interaction runs on Sheriff 2026.2.

The button uses Sheriff.on(), Sheriff.one(), and native element creation from the single sheriff.http.js include.

Runtime ready. No signal sent yet.

05 / COMPONENT LAB

Small primitives. Real interface patterns.

All eight examples use the single-file /sheriff.http.js distribution and bare top-level classes while keeping the original extends className convention. HTML, JavaScript, and complete copy-ready snippets are available on every card.

Standalone core sourceframework.v2026.js
Single 2026 includesheriff.http.js

The historical framework.js remains untouched as a downloadable archive. The site itself runs only the single sheriff.http.js include, which supplies the complete 2026.2 core, bare-class extends className activation, and HTTP/cache source.

01

STATE

Counter

0
// class="extends classCounterExample"
class CounterExample {
    constructor() { this.count = 0 }

    onclick(event) {
      const step = event.target.closest('[data-counter-step]')
      if (step) this.count += Number(step.dataset.counterStep)
      else if (event.target.closest('#counter-reset')) this.count = 0
      else return
      Sheriff.one('#counter-value', this).value = this.count
    }
}
02

INPUT

Slider

Intensity64%
// class="extends classSliderExample"
class SliderExample {
    oninput(event) {
      if (!event.target.matches('#range-control')) return
      const value = event.target.value
      Sheriff.one('#slider-value', this).value = `${value}%`
      Sheriff.one('#slider-orb', this)
        .style.setProperty('--level', Number(value) / 100)
    }
}
03

DISCLOSURE

Accordion

Native events and semantics stay visible, portable, and easy to debug.
// class="extends classAccordionExample"
class AccordionExample {
    onclick(event) {
      const activeButton = event.target.closest('button')
      if (!activeButton) return
      const willOpen = activeButton.ariaExpanded !== 'true'

      this.$$('.accordion-item button').forEach(button => {
        const active = button === activeButton && willOpen
        Sheriff.one(`#${button.ariaControls}`).hidden = !active
        button.ariaExpanded = String(active)
      })
    }
}
04

NAVIGATION

Tabs

DOM first

Selection, events, rendering, and cleanup stay close to the platform.

// class="extends classTabsExample"
class TabsExample {
    constructor() {
      this.activate = function (tab, focus) {
        Sheriff.many('[role="tab"]', this).forEach(candidate => {
          const active = candidate === tab
          candidate.ariaSelected = String(active)
          candidate.tabIndex = active ? 0 : -1
        })
        Sheriff.many('[role="tabpanel"]', this)
          .forEach(panel => panel.hidden = panel.id !== tab.ariaControls)
        if (focus) tab.focus()
      }
    }

    onclick(event) {
      const tab = event.target.closest('[role="tab"]')
      if (tab) this.activate(tab)
    }

    onkeydown(event) {
      const tabs = Sheriff.many('[role="tab"]', this)
      const move = event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0
      if (!move) return
      event.preventDefault()
      const index = tabs.indexOf(event.target)
      this.activate(tabs[(index + move + tabs.length) % tabs.length], true)
    }
}
05

DATA

Sortable table

Small-framework field notes
Status
Signals 2026 Ready
DOM helpers 2015 Stable
Core + HTTP 2026 Ready
// class="extends classTableExample"
class TableExample {
    constructor() { this.direction = {} }

    onclick(event) {
      const button = event.target.closest('[data-table-sort]')
      if (!button) return
      const key = button.dataset.tableSort
      const body = Sheriff.one('tbody', this)
      this.direction[key] = this.direction[key] === 1 ? -1 : 1

      Sheriff.many('tr', body)
        .sort((a, b) => a.dataset[key].localeCompare(
          b.dataset[key], undefined, { numeric: true }
        ) * this.direction[key])
        .forEach(row => body.append(row))
    }
}
06

VALIDATION

Form

Ready for input.

// class="extends classFormExample"
class FormExample {
    onsubmit(event) {
      if (!event.target.matches('#validation-form')) return
      event.preventDefault()
      const form = event.target
      const status = Sheriff.one('#validation-status', this)
      if (!form.checkValidity()) {
        status.textContent = 'Please complete the highlighted fields.'
        form.reportValidity()
        return
      }
      const values = Sheriff.formData(form)
      status.textContent = `Valid: ${values.name} · ${values.email}`
    }
}
07

MUTATION

Form submission

Local demo adapter ready.

// class="extends classSubmissionExample"
class SubmissionExample {
    constructor() {
      let sequence = 0
      this.api = Sheriff.http.createApi({
        fetchFn: (_url, request) => Promise.resolve(Response.json({
          id: `SM-2026-${String(++sequence).padStart(3, '0')}`,
          received: JSON.parse(request.body)
        }, { status: 201 })),
        endpoints: build => ({
          submit: build.mutation({
            query: body => ({ url: '/submissions', method: 'POST', body })
          })
        })
      })
    }

    async onsubmit(event) {
      if (!event.target.matches('#submission-form')) return
      event.preventDefault()
      const form = event.target
      if (!form.checkValidity()) return form.reportValidity()
      const result = await this.api.endpoints.submit.initiate(
        Sheriff.formData(form)
      )
      Sheriff.one('#submission-status', this).textContent =
        `Accepted ${result.data.id}`
      form.reset()
    }
}
08

CORE + FETCH · 2026

sheriff.http.js

Run the same query twice. The first request reaches the network; the second resolves from the TTL cache.

State
idle
Network
0
Cache hits
0
Entries
0
Waiting for a query.
// class="extends classHttpExample"
class HttpExample {
    constructor() {
      this.api = Sheriff.http.createApi({
        baseUrl: '/',
        ttl: 30_000,
        endpoints: build => ({
          status: build.query({
            query: () => 'demo-data.json',
            providesTags: ['Status']
          })
        })
      })
    }

    async onclick(event) {
      if (event.target.closest('#http-invalidate')) {
        this.api.cache.invalidateTags(['Status'])
        Sheriff.one('#http-state', this).textContent = 'invalidated'
        return
      }
      if (!event.target.closest('#http-query')) return
      const result = await this.api.endpoints.status.initiate()
      Sheriff.one('#http-state', this).textContent =
        result.meta.cached ? 'cached' : 'network'
      Sheriff.one('#http-output', this).textContent = result.data.message
    }
}

Small is not nostalgia.

Small is a design constraint that keeps paying interest.

Read the original source Compare Apt.js