diff --git a/source/development/frontend.rst b/source/development/frontend.rst
index a75dfd80..3d4b7a2b 100644
--- a/source/development/frontend.rst
+++ b/source/development/frontend.rst
@@ -14,3 +14,4 @@ The OPNsense frontend is implemented with `PHP/Phalcon `__ to create the dashboard grid.
+
+Widgets are placed in the :code:`src/opnsense/www/js/widgets/` directory.
+
+-------
+Example
+-------
+
+Before going into any details, it is often most useful to present an example that includes most of the core logic:
+the `interfaces overview `__ widget.
+
+---
+ACL
+---
+
+Every widget must expose the endpoints it's using to the framework, so the controller can determine whether
+this widget is accessible for the current logged in user. To do this, any :code:`.js` file must start
+with the following line(s):
+
+.. code-block:: javascript
+
+ // endpoint:/api/endpoint/used/by/widget
+
+For example:
+
+.. code-block:: javascript
+
+ // endpoint:/api/interfaces/overview/*
+
+Multiple lines can be used if the widget uses multiple endpoints. If any of these endpoints are inaccessible,
+the widget will not be loaded. Note that the same rules as for any other
+`ACL <../../development/examples/helloworld.html#plugin-to-access-control-acl>`__ applies here.
+
+---------
+Functions
+---------
+
+The `BaseWidget `__ shows the skeleton
+of the widget Javascript module. Widgets extend this class to provide defaults to the framework. To make life a little
+easier for common patterns, other base widgets may also be exposed. Currently these are:
+
+- `BaseTableWidget `__:
+ Exposes a dynamic table that can be configured in multiple orientations and only needs a data feed.
+
+- `BaseGaugeWidget `__:
+ Exposes a Gauge widget that allows presenting simple current/total values with multiple hooks to customize the widget.
+
+The following functions are available to be overridden by the widget when extended from the BaseWidget:
+
+*Constructor*
+=====================================================================================================================
+
+.. code-block:: javascript
+
+ constructor(config) {}
+
+To provide sensible defaults to the framework, a derived javascript class should always call :code:`super()` first in the constructor.
+Afterwards, the defaults can be overridden. The properties are:
+
+- :code:`this.title`. Sets the title of the widget in the header.
+- :code:`this.tickTimeout`. Sets the interval (in ms) in which the :code:`onWidgetTick()` function is called. The default is 5000
+
+If the widget has been persisted (the user pressed 'save'), the loaded widget configuration is passed in the constructor. Any
+custom data necessary for the widget to properly reload itself can be found in the :code:`this.config` property, if any.
+
+*getGridOptions*
+=====================================================================================================================
+
+.. code-block:: javascript
+
+ getGridOptions() {}
+
+To provide flexibility, the widget can optionally override this function and return an object that will be merged and loaded
+into the GridStack API. This function is called before the widget is rendered to the DOM. For example, the following code:
+
+.. code-block:: javascript
+
+ getGridOptions() {
+ return {
+ // trigger overflow-y:scroll after 650px height
+ sizeToContent: 650
+ }
+ }
+
+will insert the :code:`sizeToContent: 650` key-value pair into the GridStack options, making sure that the height of the widget
+does not exceed a maximum of 650 pixels before a scrollbar is inserted. The GridStack API reference can be found
+`here `__.
+
+This object is also persisted once the dashboard has been saved, meaning these properties are also passed in the constructor
+on a widget reload.
+
+The properties do not have to correspond to the GridStack API, any custom data can be pushed here.
+
+*getMarkup*
+=====================================================================================================================
+
+.. code-block:: javascript
+
+ getMarkup() {}
+
+This function must return a jQuery object that contains the static markup that's necessary to build the layout
+of the widget. This function will usually just return the container (with styling attached) where dynamic content
+will be loaded using `onMarkupRendered()`
+
+*onMarkupRendered*
+=====================================================================================================================
+
+.. code-block:: javascript
+
+ async onMarkupRendered() {}
+
+As soon as the dashboard has loaded, and all widget markup has been rendered to the DOM, dynamic content can be
+provided to fill the widget by defining this function. Since this is an :code:`async` function, any API call
+within this function must be awaited. For example:
+
+.. code-block:: javascript
+
+ async onMarkupRendered() {
+ await ajaxGet('/api/interfaces/overview/interfacesInfo', {}, (data, status) => {
+ // do something with the data
+ });
+ }
+
+This will make sure that all other widgets remain responsive, and a spinner appears while the data is being loaded.
+Use jQuery to update the markup as prepared by :code:`getMarkup()`.
+
+*onWidgetResize*
+=====================================================================================================================
+
+.. code-block:: javascript
+
+ onWidgetResize(elem, width, height) {}
+
+If a widget is resized by the user, or is resized due to layout constraints / browser resize, this function will be called
+with the updated width and height. The widget element is passed into the function as well.
+
+Use this function to keep the widget responsive and the layout coherent for different sizes. For example:
+
+.. code-block:: javascript
+
+ onWidgetResize(elem, width, height) {
+ if (width > 500) {
+ $('.interface-info-detail').parent().show();
+ $('.interface-info').css('justify-content', 'initial');
+ $('.interface-info').css('text-align', 'left');
+ } else {
+ $('.interface-info-detail').parent().hide();
+ $('.interface-info').css('justify-content', 'center');
+ $('.interface-info').css('text-align', 'center');
+ }
+ }
+
+The above code will make sure that if the width of the widget is less than 500px wide, less critical
+information is removed. Adjust the styling as necessary.
+
+.. warning::
+
+ While this function is debounced (throttled to prevent excessive calls), it is still executed often during a resize.
+ If this function is doing a lot of heavy lifting, make sure you implement a notion of state to prevent
+ the same logic from executing more than necessary. An example of this can be found in the
+ `BaseTableWidget `__.
+
+If you return true from this function, the grid will be forcefully updated to adjust to a new layout.
+
+*onWidgetTick*
+=====================================================================================================================
+
+.. code-block:: javascript
+
+ onWidgetTick() {}
+
+This function is called every :code:`this.tickTimeout` milliseconds. While the dashboard is open, this function
+is used to update the data presented on the dashboard.
+
+*onWidgetClose*
+=====================================================================================================================
+
+.. code-block:: javascript
+
+ onWidgetClose() {}
+
+Executed when a widget is removed from the grid. Make sure to clean up any resources in use by this widget. It is
+not always necessary to override this function, but it's possible you're using a third party library that requires
+action to be taken when the widget is removed. An example is the cleanup of a rendered chart.
+
+.. attention::
+
+ If you're using the BaseWidget EventSource mechanism, make sure to call :code:`super.onWidgetClose()` to cleanup
+ the persistent connection to the server.
+
+*onVisibilityChanged*
+=====================================================================================================================
+
+.. code-block:: javascript
+
+ onVisibilityChanged(visible) {}
+
+Executed when the visibility of the page has changed (tab or instance switch). You're very likely not going to need
+this function, but if you do, make sure to call :code:`super.onVisibilityChanged(visible)`.
+
+*openEventSource*
+=====================================================================================================================
+
+.. code-block:: javascript
+
+ openEventSource(url, onMesage);
+
+When your widget requires a persistent connection to stream data, use the :code:`super.openEventSource()` function
+with the API endpoint and a callback function. The :code:`onMessage` callback function takes in a single :code:`event`
+parameter, of which the :code:`data` property contains the event data.
+
+*closeEventSource*
+=====================================================================================================================
+
+.. code-block:: javascript
+
+ closeEventSource();
+
+Closes the current active :code:`EventSource`. This will be called automatically if the widget closes and you don't
+have the :code:`onWidgetClose` function overridden. If you do, make sure to call :code:`super.onWidgetClose()`.
+
+
+---------------
+BaseTableWidget
+---------------
+
+The BaseTableWidget exposes a set of functions to easily create a responsive table that is capable of some basic
+CRUD functionality. To make use of this, simply extend from the BaseTableWidget, which automatically exposes the
+BaseWidget functions as well. E.g.:
+
+.. code-block:: javascript
+
+ import BaseTableWidget from "./BaseTableWidget.js";
+
+ export default class YourWidget extends BaseTableWidget {}
+
+*createTable*
+=====================================================================================================================
+
+.. code-block:: javascript
+
+ super.createTable(id, options);
+
+Creates and returns a jQuery object with the id attribute set to the id parameter of this function. The :code:`options`
+parameters is an object with the following structure:
+
+.. code-block:: javascript
+
+ let options = {
+ headerPosition: 'top'|'left'|'none',
+ }
+
+If the :code:`headerPosition` is :code:`top`, some extra options are defined:
+
+.. code-block:: javascript
+
+ let options = {
+ headerPosition: 'top',
+ rotation: ,
+ headers: [],
+ sortIndex: ,
+ sortOrder: 'asc'|'desc'
+ }
+
+- :code:`rotation` will limit the amount of table entries to this value, and 'scroll' new data into view.
+- :code:`headers` defines a static array of strings that contain the table headers. The position in the array also implicitly
+ defines the index of the column.
+- :code:`sortIndex` specifies the index of the headers array to sort on
+- :code:`sortOrder` if the sortIndex is specified, the sort order will be either ascending or descending.
+
+:code:`headerPosition` :code:`left` is a key-value structure while :code:`headerPosition` :code:`none` allows for
+arbitrary rows of data without state.
+
+*updateTable*
+=====================================================================================================================
+
+.. code-block:: javascript
+
+ super.updateTable(id, data = [], rowIdentifier = null);
+
+Inserts one or more rows into the table with id parameter :code:`id`. If a rowIdentifier is specified, only a single
+row of the table is upserted.
+
+The data layout is as follows for :code:`headerPosition` :code:`top` and :code:`none`:
+
+.. code-block:: javascript
+
+
+ [
+ ['x', 'y', 'z'],
+ ['x', 'y', 'z']
+ ]
+
+The data layout for :code:`headerPosition` :code:`left` also allows nested columns:
+
+.. code-block:: javascript
+
+
+ [
+ ['x', 'x1'],
+ ['y', 'y1'],
+ ['z', ['z1', 'z2']]
+ ]
+
+---------------
+BaseGaugeWidget
+---------------
+
+:code:`BaseGuageWidget` defines a simple responsive gauge chart. An example implementation can be found in the
+`Memory Usage Widget `__
+
+*createGaugeChart*
+=====================================================================================================================
+
+.. code-block:: javascript
+
+ super.createGaugeChart(options);
+
+
+
+*updateChart*
+=====================================================================================================================
+
+.. code-block:: javascript
+
+ super.updateChart(data);
+
+-------
+Styling
+-------
+
+Any styling can be added to the `Dashboard CSS file `__
+or a themed version of this file.
+
+Since a lot of the charts have programmatic approaches to colors, the special
+
+.. code-block:: css
+
+ :root {
+ --chart-js-background-color: #f7e2d6;
+ --chart-js-border-color: #d94f00;
+ --chart-js-font-color: #d94f00;
+ }
+
+CSS selector is defined so you can override these colors for custom themes.
\ No newline at end of file