JSFiddle - React, Tailwind, and code Playground
by Nicholas Berlette
HTML
<script type="module">
const CSS_TEMPLATE_ID = "INLINE_PEN_STYLESHEET_ID";
class NWindow extends HTMLElement {
static properties = {};
static template = "<slot></slot>";
static styles = "";
#connected = false;
#shadowRoot = null;
#internals = null;
constructor() {
super();
this.#shadowRoot = this.attachShadow({ mode: "open" });
this.#internals = this.attachInternals();
}
connectedCallback() {
this.#connected = true;
this.#shadowRoot.innerHTML = this.constructor.template;
}
disconnectedCallback() {
this.#connected = false;
}
attributeChangedCallback(name, oldValue, newValue) {
}
adoptedCallback() {
}
}
customElements.define("n-window", NWindow);
</script>
<main class="relative m-0 h-screen w-screen overflow-y-auto overflow-x-hidden overscroll-contain p-0 !opacity-100" style="opacity:0;transition:opacity 1s 0.25s ease-in">
<div class="mx-auto h-full max-w-4xl lg:max-w-5xl relative py-12 sm:py-8">
<n-window class="browser w-full">
<!--window-->
<div class="window">
<tab-container class="boundary" id="browser-window-1">
<!--top-bar-->
<div class="top-bar">
<!--navigator-->
<div class="navigator group">
<!--window-buttons-->
<div class="traffic-signal !flex-shrink">
<button class="btn close group-hover:!bg-opacity-20" aria-label="Close" title="Close Window" id="btn-close-window-1"></button>
<button class="btn minimize group-hover:!bg-opacity-20" aria-label="Minimize Window" title="Minimize Window" id="btn-minimize-window-1"></button>
<button class="btn zoom group-hover:!bg-opacity-20" aria-label="Toggle Fullscreen" title="Toggle Fullscreen" id="btn-maximize-window-1"></button>
</div>
<!--/window-buttons-->
...
CSS
/*!
* MIT License - https://nick.mit-license.org
* Copyright (c) 2023-2024 Nicholas Berlette. All rights reserved.
*/
:root {
color-scheme: light dark;
}
:is(html, body, :root > main):not(.light, .dark) {
color-scheme: light dark;
background: none;
background-color: light-dark(#eee, #111420);
}
.light {
color-scheme: light;
}
.dark {
color-scheme: dark;
}
:root {
--navigator-padding-x: 1rem;
--navigator-padding-y: 0.375rem;
--navigator-column-gap: 0.5rem;
--url-icon-size: 0.75rem;
--url-icon-color: 0;
--url-icon-color-hover: 0;
--btn-size: 0.625rem;
--btn-column-gap: 0.4rem;
--btn-background-color: light-dark(#64748b, #345); /*#94a3b8);*/
--img-border-style: dashed;
--img-border-color: rgb(
148 163 184 / calc(0.5 * var(--border-opacity-multipler, 1))
);
--img-size: 1rem;
--img-padding: 1px;
--tab-padding: 3px 0.625rem;
--tab-max-width: 10rem;
--tab-panel-margin-bottom: 2rem;
--tab-panel-padding-bottom: 1rem;
--border-opacity-multiplier: 1;
--tab-border-opacity: 1;
--tab-border-top-opacity: 1;
--tab-border-right-opacity: 1;
--tab-border-bottom-opacity: 1;
--tab-border-left-opacity: 0;
--tab-border-color: light-dark(
rgb(2 6 23 / calc(0.1 * var(--tab-border-opacity, 1))),
rgb(2 6 23 / calc(0.2 * var(--tab-border-opacity, 1)))
);
--tab-border-top-color: light-dark(
rgb(2 6 23 / calc(0.2 * var(--tab-border-top-opacity, 1))),
rgb(2 6 23 / calc(0.3 * var(--tab-border-top-opacity, 1)))
);
--tab-border-right-color: light-dark(
rgb(2 6 23 / calc(0.1 * var(--tab-border-right-opacity, 1))),
rgb(2 6 23 / calc(0.2 * var(--tab-border-right-opacity, 1)))
);
--tab-border-bottom-color: light-dark(
rgb(2 6 23 / calc(0.2 * var(--tab-border-bottom-opacity, 1))),
rgb(2 6 23 / calc(0.3 * var(--tab-border-bottom-opacity, 1)))
);
--tab-border-left-color: light-dark(
rgb(2 6 23 / calc(0.1 * var(--tab-border-left-opacity, 1))),
rgb(2 6 23 / calc(0.2 *...
TypeScript
/**
* @module n-browser
* @description A reusable web component library that implements a browser‑style window UI.
*
* Features:
* - A top bar with window controls (close, minimize, fullscreen), navigation buttons (back, forward, refresh) and URL input.
* - Each tab has its own iframe panel so that history, refresh and navigation can be controlled.
* - URL input “Enter” triggers navigation in the active tab; ctrl/cmd+enter opens a new tab and navigates it.
*/
/* Shared CSS (a trimmed version of your original styles to preserve visual look) */
const sharedStyles = /* css */ `
:host {
display: block;
font-family: sans-serif;
}
.browser {
border: 1px solid #ccc;
border-radius: 0.5rem;
overflow: hidden;
height: 40rem;
width: 100%;
background: white;
}
/* Topbar styles */
.top-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
background: linear-gradient(to bottom, #f3f4f6, #f8fafc);
}
.traffic-signal {
display: flex;
gap: 0.5rem;
}
.traffic-signal button {
width: 1rem;
height: 1rem;
border-radius: 50%;
border: none;
cursor: pointer;
}
.traffic-signal button.close { background: #e3342f; }
.traffic-signal button.minimize { background: #f4bf50; }
.traffic-signal button.maximize { background: #61c454; }
.navigator {
flex: 1;
display: flex;
align-items: center;
gap: 0.5rem;
}
.navigator button {
background: none;
border: none;
font-size: 1rem;
cursor: pointer;
}
.navigator input.url {
flex: 1;
padding: 0.25rem 0.5rem;
font-size: 0.9rem;
}
/* Tab and panel container */
.window {
position: relative;
height: calc(100% - 3rem);
}
`;
/* ===========================
<n-browser> Main Container
=========================== */
/**
* Main browser element.
*
* @example
* <n-browser>
* <n-browser-tabs>
* <n-browser-tab active...