diff --git a/examples/interactive-rating-component/.gitignore b/examples/interactive-rating-component/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/examples/interactive-rating-component/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/examples/interactive-rating-component/.npmrc b/examples/interactive-rating-component/.npmrc
new file mode 100644
index 00000000..6b5f38e8
--- /dev/null
+++ b/examples/interactive-rating-component/.npmrc
@@ -0,0 +1,2 @@
+save-exact = true
+package-lock = false
diff --git a/examples/interactive-rating-component/README.md b/examples/interactive-rating-component/README.md
new file mode 100644
index 00000000..fc42d8e9
--- /dev/null
+++ b/examples/interactive-rating-component/README.md
@@ -0,0 +1,23 @@
+# Frontend Mentor Interactive Rating Component
+
+Here is the implementation in [Bau.js](https://github.com/grucloud/bau) of the [Frontend Mentor Interactive Rating Component code challenge](https://www.frontendmentor.io/challenges/interactive-rating-component-koxpeBUmI)
+
+## Workflow
+
+Install the dependencies:
+
+```sh
+npm install
+```
+
+Start a development server:
+
+```sh
+npm run dev
+```
+
+Build a production version:
+
+```sh
+npm run build
+```
diff --git a/examples/interactive-rating-component/index.html b/examples/interactive-rating-component/index.html
new file mode 100644
index 00000000..79fae46c
--- /dev/null
+++ b/examples/interactive-rating-component/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ Interactive Rating Component | FrontendMentor
+
+
+
+
+
+
diff --git a/examples/interactive-rating-component/package.json b/examples/interactive-rating-component/package.json
new file mode 100644
index 00000000..c2bd4e2b
--- /dev/null
+++ b/examples/interactive-rating-component/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "frontendmentor-interactive-rating-component",
+ "private": true,
+ "version": "0.85.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "typescript": "^5.0.2",
+ "vite": "^5.2.11"
+ },
+ "dependencies": {
+ "@grucloud/bau": "^0.85.0",
+ "@grucloud/bau-css": "^0.85.0",
+ "@grucloud/bau-ui": "^0.85.0"
+ }
+}
diff --git a/examples/interactive-rating-component/public/assets/images/favicon-32x32.png b/examples/interactive-rating-component/public/assets/images/favicon-32x32.png
new file mode 100644
index 00000000..1e2df7f0
Binary files /dev/null and b/examples/interactive-rating-component/public/assets/images/favicon-32x32.png differ
diff --git a/examples/interactive-rating-component/public/assets/images/icon-star.svg b/examples/interactive-rating-component/public/assets/images/icon-star.svg
new file mode 100644
index 00000000..a15741e8
--- /dev/null
+++ b/examples/interactive-rating-component/public/assets/images/icon-star.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/interactive-rating-component/public/assets/images/illustration-thank-you.svg b/examples/interactive-rating-component/public/assets/images/illustration-thank-you.svg
new file mode 100644
index 00000000..bf22b942
--- /dev/null
+++ b/examples/interactive-rating-component/public/assets/images/illustration-thank-you.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/interactive-rating-component/src/interactiveRating.ts b/examples/interactive-rating-component/src/interactiveRating.ts
new file mode 100644
index 00000000..665c5c53
--- /dev/null
+++ b/examples/interactive-rating-component/src/interactiveRating.ts
@@ -0,0 +1,168 @@
+import { type Context } from "@grucloud/bau-ui/context";
+
+const ratingKey = "rating";
+const submittedKey = "submitted";
+
+export default function (context: Context) {
+ const { bau, css, window } = context;
+ const { h1, p, ul, li, button, form, img, picture, section, article, div } =
+ bau.tags;
+
+ const className = css`
+ max-width: 400px;
+ margin: 1rem;
+ background-color: var(--clr-dark-700);
+ border-radius: 1em;
+ .panel {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ padding: 2rem;
+ }
+
+ h1 {
+ margin: 0;
+ }
+ picture {
+ img {
+ background-color: var(--clr-dark-500);
+ border-radius: 50%;
+ padding: 1rem;
+ }
+ }
+ p {
+ color: var(--clr-neutral-300);
+ }
+ ul {
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+
+ li {
+ list-style: none;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ height: 3rem;
+ width: 3rem;
+ border-radius: 50%;
+ background-color: var(--clr-dark-500);
+ color: var(--clr-neutral-300);
+ cursor: pointer;
+
+ &:hover {
+ background-color: var(--clr-dark-300);
+ color: var(--clr-neutral-100);
+ }
+ &.active {
+ color: white;
+ background-color: var(--clr-primary);
+ }
+ }
+ }
+ button {
+ padding: 1rem 0;
+ width: 100%;
+ border-radius: 1rem;
+ border: none;
+ cursor: pointer;
+ background-color: var(--clr-primary);
+ color: var(--clr-neutral-100);
+ font-size: 1rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.2rem;
+ transition: all 0.5s;
+ &:hover {
+ background-color: var(--clr-neutral-100);
+ color: var(--clr-primary);
+ }
+ }
+
+ .thankyou {
+ align-items: center;
+ h1 {
+ font-size: 2rem;
+ }
+ .badge {
+ color: var(--clr-primary);
+ background-color: var(--clr-dark-500);
+ padding: 0.5rem 1rem;
+ border-radius: 1rem;
+ }
+ }
+ `;
+
+ const search = new URLSearchParams(window.location.search);
+ const ratingState = bau.state(Number(search.get(ratingKey)));
+ const submittedState = bau.state(!!search.get(submittedKey));
+
+ const onRating = (rating: number) => () => {
+ const search = new URLSearchParams(window.location.search);
+ search.set(ratingKey, String(rating));
+ window.history.pushState(
+ "",
+ "",
+ `?${search.toString()}${window.location.hash}`
+ );
+ ratingState.val = rating;
+ };
+
+ const onsubmit = (event: any) => {
+ event.preventDefault();
+ const search = new URLSearchParams(window.location.search);
+ search.set(submittedKey, "true");
+ window.history.replaceState(
+ "",
+ "",
+ `?${search.toString()}${window.location.hash}`
+ );
+ submittedState.val = true;
+ };
+
+ const InteractiveRatingContent = () =>
+ form(
+ { class: "panel", onsubmit },
+ picture(img({ src: "./assets/images/icon-star.svg", alt: "star" })),
+ h1("How did we do?"),
+ p(
+ "Please let us know how we did with your support request. All feedback is appreciated to help us improve our offering!"
+ ),
+ ul(
+ Array(5)
+ .fill("")
+ .map((_, i) =>
+ li(
+ {
+ class: () => i + 1 === ratingState.val && "active",
+ onclick: onRating(i + 1),
+ },
+ i + 1
+ )
+ )
+ ),
+ button({ type: "submit" }, "Submit")
+ );
+
+ const ThankYou = () =>
+ section(
+ { class: ["thankyou", "panel"] },
+ img({
+ src: "./assets/images/illustration-thank-you.svg",
+ alt: "Thank you",
+ }),
+ div({ class: "badge" }, "You selected ", ratingState.val, " out of 5"),
+ h1("Thank you"),
+ p(
+ "We appreciate you taking the time to give a rating. If you ever need more support, don’t hesitate to get in touch!"
+ )
+ );
+
+ return function interactiveRating() {
+ return article({ class: className }, () =>
+ submittedState.val ? ThankYou() : InteractiveRatingContent()
+ );
+ };
+}
diff --git a/examples/interactive-rating-component/src/main.ts b/examples/interactive-rating-component/src/main.ts
new file mode 100644
index 00000000..060bfa23
--- /dev/null
+++ b/examples/interactive-rating-component/src/main.ts
@@ -0,0 +1,20 @@
+import { createContext, type Context } from "@grucloud/bau-ui/context";
+import interactiveRating from "./interactiveRating";
+
+import "./style.css";
+
+const context = createContext();
+
+const app = (context: Context) => {
+ const { bau } = context;
+ const { main } = bau.tags;
+
+ const InteractiveRating = interactiveRating(context);
+
+ return function () {
+ return main(InteractiveRating());
+ };
+};
+
+const App = app(context);
+document.getElementById("app")?.replaceChildren(App());
diff --git a/examples/interactive-rating-component/src/style.css b/examples/interactive-rating-component/src/style.css
new file mode 100644
index 00000000..1408a821
--- /dev/null
+++ b/examples/interactive-rating-component/src/style.css
@@ -0,0 +1,31 @@
+@import url("https://fonts.googleapis.com/css2?family=Overpass:wght@400;700&display=swap");
+
+* {
+ margin: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ --clr-primary: hsl(25, 97%, 53%);
+
+ --clr-neutral-100: hsl(0, 0%, 100%);
+ --clr-neutral-300: hsl(217, 12%, 63%);
+ --clr-neutral-500: hsl(216, 12%, 54%);
+ --clr-dark-300: hsl(213, 19%, 50%);
+
+ --clr-dark-500: hsl(213, 19%, 30%);
+
+ --clr-dark-700: hsl(213, 19%, 18%);
+ --clr-dark-900: hsl(216, 12%, 8%);
+}
+
+body {
+ font-family: "Overpass", sans-serif;
+ min-height: 100vh;
+ display: grid;
+ place-items: center;
+ background-color: var(--clr-dark-900);
+ color: var(--clr-neutral-100);
+ @media (max-width: 600px) {
+ }
+}
diff --git a/examples/interactive-rating-component/src/vite-env.d.ts b/examples/interactive-rating-component/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/examples/interactive-rating-component/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/interactive-rating-component/tsconfig.json b/examples/interactive-rating-component/tsconfig.json
new file mode 100644
index 00000000..75abdef2
--- /dev/null
+++ b/examples/interactive-rating-component/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/examples/interactive-rating-component/vite.config.js b/examples/interactive-rating-component/vite.config.js
new file mode 100644
index 00000000..41713bec
--- /dev/null
+++ b/examples/interactive-rating-component/vite.config.js
@@ -0,0 +1,9 @@
+import { defineConfig } from "vite";
+
+export default defineConfig(({ command, mode, ssrBuild }) => {
+ return {
+ server: {
+ open: true,
+ },
+ };
+});