Original(japanese) https://zenn.dev/mizchi/articles/standalone-html-frontend
Mostly translated by deepl
Note: Do not use in production, tailwind is running in CDN mode and esm.sh builds scripts dynamically, so performance is not good.
It is a combination of NativeESM + importmaps + esm.sh, etc.
We are talking about combining this with the new v135 feature in esm.sh to bundle tsx.
https://github.com/esm-dev/esm.sh/releases/tag/v135
<!-- esm.sh/run -->
<script type="module" src="https://esm.sh/run" defer></script>
<!-- your code -->
<script type="text/babel">
// code here
</script>
It is the same as babel/standalone which has been around for a long time.
I can run tsx with text/tsx, but... I can run tsx with text/tsx... but <script type=text/tsx>
is not supported in vscode, so I can't highlight it, and there is currently no way to run inline TypeScript with type checking.
If all you want to do with this file scope is to expand JSX, text/babel is recommended since vscode supports text/babel.
You can also try it in Playground.
https://code.esm.sh/?template=run
A simple example
Set up the CDN tailwind and importmaps to pull react.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.tailwindcss.com"></script>
<script type="importmap">
{
"imports": {
"@jsxImportSource": "https://esm.sh/react@18",
"react": "https://esm.sh/react@18",
"react-dom/client": "https://esm.sh/react-dom@18/client"
}
}
</script>
<script type="module" src="https://esm.sh/run" defer></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
import { createRoot } from "react-dom/client";
const App = () => {
return <div className="bg-red-400">
Hello World
</div>;
};
const root = createRoot(document.querySelector('#root'));
root.render(<App />);
</script>
</body>
</html>
Save it as index.html in a suitable location and open it as a local file in Chrome. (e.g. open index.html
)
It works.
This seems to be enough for a quick test of the react library.
For a more complex real-world example, we embed the code generated by shadcn-ui/react, which contains Tailwind + radix-ui.
shadcn-ui/ui: Beautifully designed components built with Radix UI and Tailwind CSS.
Tailwind configuration is written in the global tailwind.config file equivalent to tailwind.config.js.
<script>
tailwind.config = {/**/}
</script>
Try Tailwind CSS using the Play CDN - Tailwind CSS
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.tailwindcss.com"></script>
<style type="text/tailwindcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
</style>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
}
}
</script>
<script type="importmap">
{
"imports": {
"@jsxImportSource": "https://esm.sh/react@18",
"@radix-ui/react-slot": "https://esm.sh/@radix-ui/react-slot",
"react": "https://esm.sh/react@18",
"react-dom/client": "https://esm.sh/react-dom@18/client",
"class-variance-authority": "https://esm.sh/class-variance-authority",
"clsx": "https://esm.sh/clsx",
"tailwind-merge": "https://esm.sh/tailwind-merge"
}
}
</script>
<script type="module" src="https://esm.sh/run" defer></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
import { forwardRef } from "react";
import { createRoot } from "react-dom/client";
import { cva } from "class-variance-authority";
import { Slot } from "@radix-ui/react-slot";
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs) {
return twMerge(clsx(inputs));
}
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export const Button = forwardRef(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
asChild={asChild}
{...props}
/>
);
},
);
Button.displayName = "Button";
const App = () => {
return <div>
<Button variant="outline">
Hello World
</Button>
</div>;
};
const root = createRoot(document.querySelector('#root'));
root.render(<App />);
</script>
</body>
</html>
It works.
It was quite complicated, but it still works. However, the preview speed is not that fast as far as I can see in the playground.
I had no problem this time, but there is no way for external libraries specified in plugins in tailwind.config.js to represent loading.
Now that we are here, we want to complete the development environment in the browser.
Mount yourself in the Source tab of Devtools.
Done. Rewrite it and update it manually by reloading. (Can you auto-reload with self-monitoring if you try hard enough?)
Unfortunately, the Devtools built-in editor supports <script type="text/typescript">
, but not <script type="text/tsx">
.
<script type="text/tsx" src=". /main.tsx"></script>
was also no good. If I had this, I could write in tsx (except for nested import destinations)...
I threw it on Twitter that I made a part other than esm.sh/run and the author of esm.sh told me about it. Thanks.
https://twitter.com/jexia_/status/1727069200072741122
Markdown のコードブロックでLSPを動かす VSCode 拡張を作った
Now if only I can find a way to make text/tsx highlighting not work in vscode, a way to make LSP work, and maybe even a way to format it, I can make the experience a little better. I think I can use my previous implementation of LSP in markdown code blocks.
I made a VSCode extension to run LSP in markdown code blocks
You may be able to use your own implementation of esm.sh/run to run LSP.